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");
+ });
},
};