From e7be361148189f3dac45509a72ccdc564017c854 Mon Sep 17 00:00:00 2001 From: can1357 Date: Thu, 2 Jul 2026 02:40:08 +0200 Subject: [PATCH] fix(collab): isolated editor state to preserve draft input - Extracted `AskEditor` into a standalone, key-bound component keyed by `reqId`. - Preserved user-typed draft state across duplicate incoming host requests of the same ID. - Reset the editor draft state only when a genuinely new request ID arrives. - Simplified `Composer` autosizing logic and state orchestration by isolating editor-specific hooks. - Adjusted integration tests to match HTML structure changes of the submit action. --- packages/collab-web/CHANGELOG.md | 1 + .../src/components/shell/Composer.tsx | 122 ++++++++++-------- packages/collab-web/test/composer.test.tsx | 14 +- 3 files changed, 79 insertions(+), 58 deletions(-) diff --git a/packages/collab-web/CHANGELOG.md b/packages/collab-web/CHANGELOG.md index 9405d12f2..9e3bb3021 100644 --- a/packages/collab-web/CHANGELOG.md +++ b/packages/collab-web/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed an issue in the mobile collaboration web UI where 'ask' questions were displayed without response controls. +- Fixed a host re-send of the same editor 'ask' request clobbering a half-typed guest response; the draft now resets only when a new request arrives. - Fixed the agent transcript drawer hot-retrying forever when the host reports a terminal transcript error (such as an oversized row); the error now stops polling and is shown below any rows already loaded. - Fixed pre-welcome host `error` frames (such as a protocol-version rejection) being invisible until the welcome timeout; the session now ends immediately with the host's reason. diff --git a/packages/collab-web/src/components/shell/Composer.tsx b/packages/collab-web/src/components/shell/Composer.tsx index 282b47a83..5f3e45885 100644 --- a/packages/collab-web/src/components/shell/Composer.tsx +++ b/packages/collab-web/src/components/shell/Composer.tsx @@ -1,6 +1,6 @@ import { SendHorizontal, Square } from "lucide-react"; import type { KeyboardEvent, ReactNode } from "react"; -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; +import { useCallback, useLayoutEffect, useRef, useState } from "react"; import type { GuestClient, GuestSnapshot } from "../../lib/client"; export interface ComposerProps { @@ -13,39 +13,80 @@ const LINE_PX = 20; const PAD_Y = 16; const MAX_ROWS = 8; -function optionLabel(option: string | { label: string }): string { - return typeof option === "string" ? option : option.label; +function autosize(el: HTMLTextAreaElement | null): void { + if (!el) return; + el.style.height = "0px"; + const max = MAX_ROWS * LINE_PX + PAD_Y; + el.style.height = `${Math.max(LINE_PX + PAD_Y, Math.min(el.scrollHeight, max))}px`; + el.style.overflowY = el.scrollHeight > max ? "auto" : "hidden"; +} + +interface AskEditorProps { + prefill: string | undefined; + onSubmit(value: string): void; +} + +/** + * Editor ask input. Rendered with `key={reqId}` so a new request remounts it with a fresh + * draft seeded from `prefill`, while re-sends of the same request never clobber a half-typed + * draft. Submits verbatim — whitespace-only responses are intentional. + */ +function AskEditor({ prefill, onSubmit }: AskEditorProps): ReactNode { + const [draft, setDraft] = useState(prefill ?? ""); + const taRef = useRef(null); + + useLayoutEffect(() => { + autosize(taRef.current); + }, [draft]); + + const onKeyDown = (e: KeyboardEvent): void => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + onSubmit(draft); + } + }; + + return ( +
+