fix(ui): keep the input box usable while a response is streaming

While a turn was in flight (isThinking or streamingText), the ChatInput was
unmounted and replaced with a static "Waiting for response…" box — so the user
couldn't type ahead during output, the input field just vanished.

Now ChatInput stays mounted the whole time: the user can type and edit while
output streams in. A one-line status above it ("Waiting… esc to interrupt — type
ahead, Enter sends when done.") replaces the old full-width box. Submitting a
second turn mid-stream is still prevented — handleSubmit now guards on
isThinking/streamingText and returns early, so Enter is a no-op until the
in-flight turn finishes (the typed text stays in the box). The original race this
swap was protecting against (overlapping turns on the same session) is preserved
by that guard.

Verified: typecheck clean, build 274.76 KB, 301 tests pass.
This commit is contained in:
kim
2026-08-21 14:10:21 +09:00
parent dad0915d62
commit dfaf8d1551
+16 -18
View File
@@ -776,6 +776,11 @@ export function App({
}
async function handleSubmit(raw: string) {
// A turn is already in flight (streaming output or waiting for the first chunk). Keep the
// input box mounted so the user can type ahead, but ignore Enter so a second turn can't
// overlap onto the same session. The in-flight turn is the source of truth; the typed text
// stays in the box for when the turn finishes.
if (isThinking || streamingText !== null) return;
setInputValue("");
const trimmed = raw.trim();
if (!trimmed) return;
@@ -1198,25 +1203,18 @@ export function App({
gitInfo={gitInfo}
/>
)}
{isThinking || streamingText !== null ? (
// Swapped in for ChatInput while a turn is in flight. Closes a real (if rare) race:
// handleSubmit had no guard against firing while a turn was already running, so
// typing and hitting Enter mid-stream could submit a second overlapping turn onto
// the same session.
<Box borderStyle="round" borderColor={ACCENT_HEX} paddingX={1} width="100%">
<Text dimColor>Waiting for response… (esc to interrupt)</Text>
</Box>
) : (
<ChatInput
value={inputValue}
onChange={setInputValue}
onSubmit={handleSubmit}
cwd={cwd}
history={history}
availableColumns={chatColumns}
isActive={!filePanelFocused}
/>
{(isThinking || streamingText !== null) && (
<Text dimColor> Waiting for response… (esc to interrupt) — type ahead, Enter sends when done.</Text>
)}
<ChatInput
value={inputValue}
onChange={setInputValue}
onSubmit={handleSubmit}
cwd={cwd}
history={history}
availableColumns={chatColumns}
isActive={!filePanelFocused}
/>
</>
)}
</Box>