diff --git a/packages/components/src/components/sessions/session-chat-interface.tsx b/packages/components/src/components/sessions/session-chat-interface.tsx
index c16dd80fc..bee5daa0f 100644
--- a/packages/components/src/components/sessions/session-chat-interface.tsx
+++ b/packages/components/src/components/sessions/session-chat-interface.tsx
@@ -1728,6 +1728,16 @@ interface SessionChatInterfaceProps {
subHeader?: React.ReactNode;
/** When true, hide the message area and input but keep the header and subHeader visible */
hideMessageArea?: boolean;
+ /**
+ * When true, this surface follows the session without driving it: the
+ * composer and the permission request's response buttons are not rendered.
+ *
+ * For a host that mounts a session the viewer may read but not write. It is
+ * presentation only — a viewer who cannot write is enforced wherever the
+ * writes are applied, not here — but a control that cannot work should not be
+ * offered.
+ */
+ readOnly?: boolean;
/** When false, keep the local doc mounted without holding the remote room subscription. */
syncEnabled?: boolean;
/**
@@ -1897,6 +1907,7 @@ export const SessionChatInterface = memo(
titleSyncing,
subHeader,
hideMessageArea = false,
+ readOnly = false,
syncEnabled = !hideMessageArea,
isVisible = true,
isExternalHistoryRefreshing = false,
@@ -5742,12 +5753,18 @@ export const SessionChatInterface = memo(
- {/* Floating permission request - shown when session is waiting for permission */}
-
+ {/* Floating permission request - shown when session is waiting for permission.
+ A read-only surface does not render it: its options are
+ answers, and an answer this viewer cannot write is a
+ button that does nothing. The request still appears in the
+ transcript. */}
+ {readOnly ? null : (
+
+ )}
{/* Notification permission prompt - shown when session becomes idle (turn completed) */}
{/* TODO(analytics): session/notification_prompt_shown|_permission_granted|_permission_denied.
@@ -5856,7 +5873,7 @@ export const SessionChatInterface = memo(
{/* Input area - isolated component to prevent full re-renders on typing.
Hidden while a permission is pending so the response buttons claim
the bottom surface; chat queue is bypassed for the same reason. */}
- {shouldReplaceComposerWithPermission ? null : (
+ {readOnly || shouldReplaceComposerWithPermission ? null : (
void;
+ /** Follow the session without driving it. Passed to every chat surface this
+ * page mounts; see `SessionChatInterfaceProps.readOnly`. */
+ readOnly?: boolean;
}) => {
const { t } = useTranslation();
const router = useRouter();
@@ -4923,6 +4927,7 @@ const SessionDetail = ({
workspaceSession={activeSession}
className="h-full"
hideHeader
+ readOnly={readOnly}
syncEnabled={isActive || pendingForkSourceId !== undefined}
isVisible={isActive}
isChildTab={tabSession.id !== sessionId}
@@ -5550,6 +5555,7 @@ const SessionDetail = ({
workspaceSession: activeSession,
className: 'h-full',
hideHeader: true,
+ readOnly,
syncEnabled: isActive || pendingForkSourceId !== undefined,
isVisible,
onFileDiffClick: handleOpenFileDiffForChat,