diff --git a/site/src/components/ChatMessageInput/ChatMessageInput.tsx b/site/src/components/ChatMessageInput/ChatMessageInput.tsx index 02526da890..23ad9c3c31 100644 --- a/site/src/components/ChatMessageInput/ChatMessageInput.tsx +++ b/site/src/components/ChatMessageInput/ChatMessageInput.tsx @@ -27,6 +27,7 @@ import { useCallback, useEffect, useImperativeHandle, + useLayoutEffect, useMemo, useRef, } from "react"; @@ -221,6 +222,21 @@ interface ChatMessageInputProps "aria-label"?: string; } +// Keeps the Lexical editor's editable state in sync with the +// disabled prop so that the underlying contentEditable element +// becomes truly non-interactive when the input is disabled. +const EditableStatePlugin: FC<{ disabled: boolean }> = memo( + function EditableStatePlugin({ disabled }) { + const [editor] = useLexicalComposerContext(); + + useLayoutEffect(() => { + editor.setEditable(!disabled); + }, [editor, disabled]); + + return null; + }, +); + const ChatMessageInput = memo( ({ className, @@ -243,8 +259,9 @@ const ChatMessageInput = memo( }, onError: (error: Error) => console.error("Lexical error:", error), nodes: [], + editable: !disabled, }), - [], + [disabled], ); const style = useMemo( () => ({ @@ -380,6 +397,7 @@ const ChatMessageInput = memo( + {autoFocus && } diff --git a/site/src/pages/AgentsPage/AgentChatInput.stories.tsx b/site/src/pages/AgentsPage/AgentChatInput.stories.tsx index ee891280ff..7f3a287e6f 100644 --- a/site/src/pages/AgentsPage/AgentChatInput.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatInput.stories.tsx @@ -80,6 +80,13 @@ export const DisabledInput: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByRole("button", { name: "Send" })).toBeDisabled(); + + // The editor should be non-editable so users cannot click + // into it and type (e.g. archived chats). + const editor = canvas.getByTestId("chat-message-input"); + await waitFor(() => { + expect(editor).toHaveAttribute("contenteditable", "false"); + }); }, };