feat(web): morphing send/stop/queue composer button

The composer's primary button now reflects generation state: Send when idle,
Stop while generating with an empty draft, and Queue while generating with a
draft typed (submitting queues it via the existing queue path). Stop is
click-only so a stray Enter never interrupts a run. ChatInput gains generating
+ onStop props.

BooChat: removes the separate centered "Stop generating" pill and wires
generating={streaming} + onStop={handleStop}. BooCoder: generating now keys on
sending || activeTaskId (the dispatch POST is too brief on its own), which also
fixes the queue gates that previously fired mid-run; onStop cancels the active
task via the new api.coder.cancelTask, and the input is no longer disabled while
a task runs so follow-ups can be queued.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 02:15:14 +00:00
parent 2fd7e5bf97
commit 08d6a8fa40
3 changed files with 65 additions and 33 deletions

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Pencil, Send, Square, X } from 'lucide-react';
import { Pencil, Send, X } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api/client';
import { useSessionStream } from '@/hooks/useSessionStream';
@@ -248,22 +248,6 @@ export function ChatPane({ sessionId, chatId, projectId, agentId, onAgentChange,
</div>
)}
{/* Stop button when streaming */}
{streaming && (
<div className="border-t py-1">
<div className="max-w-[1000px] mx-auto w-full flex justify-center">
<button
type="button"
onClick={() => void handleStop()}
className="flex items-center gap-1.5 text-xs px-3 py-1 rounded-full border hover:bg-muted text-muted-foreground hover:text-foreground max-md:min-h-[44px] max-md:px-5"
>
<Square size={10} className="fill-current" />
Stop generating
</button>
</div>
</div>
)}
{stale && streamingId && (
<StaleStreamBanner
onRetry={() => void handleRetryStale()}
@@ -280,6 +264,8 @@ export function ChatPane({ sessionId, chatId, projectId, agentId, onAgentChange,
webSearchEnabled={webSearchEnabled}
onSend={handleSend}
onForceSend={streaming ? handleForceSend : undefined}
generating={streaming}
onStop={handleStop}
onSlashCommand={handleSlashCommand}
chatId={chatId}
chatLabel={sessionChats?.find((c) => c.id === chatId)?.name ?? 'Chat'}