From 0f3a1c84959b00baa8e9188ca7e177af255e324c Mon Sep 17 00:00:00 2001 From: Tim Biggs Date: Sat, 14 Nov 2026 08:35:24 +0000 Subject: [PATCH] fix(web): view-only connections lock the composer and show an amber notice The composer stayed editable on read-only connections, and the view-only note sat as small text under it. The editor is now read-only (including type-to-focus and paste-to-focus insertion), and the note is a warning banner tab above the composer like the other composer notices. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/chat/ChatComposer.tsx | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index c71634cd4edce..28a3c573eb1f0 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -236,7 +236,7 @@ import { observeResize } from "~/lib/observeResize"; const isEnvironmentRequestInvalidError = Schema.is(EnvironmentRequestInvalidError); import { toastManager } from "../ui/toast"; -import { BuildingIcon, PlayIcon, ReplyIcon } from "lucide-react"; +import { BuildingIcon, EyeIcon, PlayIcon, ReplyIcon } from "lucide-react"; import { useNavigate } from "@tanstack/react-router"; import { assistantCitationLabel } from "@t3tools/shared/assistantCitations"; import { assistantCitationNavigation } from "../../lib/assistantCitationNavigation"; @@ -2506,11 +2506,27 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }, ] : []; + // Read scope also proves the session has loaded, so this doesn't flash while connecting. + const viewOnly = + useEnvironmentScope(environmentId, AuthOrchestrationReadScope) && !canOperateThread; + const viewOnlyBannerItems: readonly ComposerBannerStackItem[] = viewOnly + ? [ + { + id: "view-only", + variant: "warning", + group: "environment", + icon: , + title: "View only", + description: "This connection can't send messages or answer requests.", + }, + ] + : []; const bannerItems: readonly ComposerBannerStackItem[] = runtimeModeCeilingDenial === undefined || activePendingProgress - ? [...props.bannerItems, ...replyBannerItems] + ? [...props.bannerItems, ...viewOnlyBannerItems, ...replyBannerItems] : [ ...props.bannerItems, + ...viewOnlyBannerItems, ...replyBannerItems, { id: "managed-runtime-mode", @@ -2550,6 +2566,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ): boolean => { if ( text.length === 0 || + viewOnly || activePendingIsResponding || (pendingUserInputs.length > 0 && !activePendingProgress?.activeQuestion) || projectSelectionRequired || @@ -3282,10 +3299,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) focusComposer={focusComposer} /> ) : null; - // Read scope also proves the session has loaded, so this doesn't flash while connecting. - const viewOnly = - useEnvironmentScope(environmentId, AuthOrchestrationReadScope) && !canOperateThread; - return (
- {viewOnly ? ( -

- This connection can only view threads. You can't send messages or answer requests here. -

- ) : null}
); });