feat: post-review backlog hardening (cancel/parser/stall/history/9502)

Five independent items from the post-review backlog. F1: Stop on an external
agent task now aborts the running child via a per-task AbortController registry
reachable from the cancel route, and finalizes the assistant message as
cancelled (fixing two latent bugs — catch blocks left the message streaming,
and warm success-paths wrote complete on an aborted turn); warm pools/worktrees
are preserved and the native path is unchanged. F2/F3: prune the tool-call
parser to its two load-bearing exports (unexport eight zero-caller symbols, add
a gate test for the <invoke>-as-text fallback) and route placeholder-rejection
logging through pino. F6: a 90s per-chunk stall-timeout wraps native inference's
fullStream via AbortSignal.any so a hung stream finalizes the message instead of
hanging — no retry (a pure classifyStreamError helper is added). F7: a read-only
view_session_history MCP tool (newest-N, chronological). F9: retire the unused
apps/coder/web :9502 fallback SPA, keeping every API/WS/health/MCP route.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 02:23:11 +00:00
parent 9a139633b8
commit f32fd928b3
48 changed files with 1014 additions and 2254 deletions

View File

@@ -10,7 +10,8 @@ export interface CoderMessageWire {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
status?: 'streaming' | 'complete' | 'failed';
// F1: 'cancelled' — a user Stop / stall finalized the turn (renders "Stopped").
status?: 'streaming' | 'complete' | 'failed' | 'cancelled';
model?: string | null;
reasoning_text?: string;
tool_calls?: CoderToolCallWire[];

View File

@@ -29,7 +29,8 @@ interface CoderMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
status?: 'streaming' | 'complete' | 'failed';
// F1: 'cancelled' — a user Stop / stall finalized the turn (renders "Stopped").
status?: 'streaming' | 'complete' | 'failed' | 'cancelled';
// model-attribution: which model produced this assistant message (chip).
model?: string | null;
reasoning_text?: string;
@@ -296,7 +297,10 @@ function useCoderMessages(sessionId: string, chatId: string | undefined, handler
m.id === frame.message_id && m.role !== 'tool'
? {
...m,
status: 'complete' as const,
// F1 (D-8): the terminal frame carries an optional status —
// 'cancelled' on a Stop/stall, 'failed' on error. Absent on the
// normal path → defaults to 'complete'.
status: ((frame as any).status ?? 'complete') as CoderMessage['status'],
model: (frame as any).model ?? (m as any).model ?? null,
ctx_used: (frame as any).ctx_used ?? (m as any).ctx_used ?? null,
ctx_max: (frame as any).ctx_max ?? (m as any).ctx_max ?? null,
@@ -669,6 +673,9 @@ export function CoderPane({
onAgentLabelChange?.(parts.join(' · '));
}, [agentConfig.provider, agentConfig.model, onAgentLabelChange]);
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
// F1: true while the Stop POST is in flight — disables the Stop button and makes
// a rapid double-click a no-op (the abort is idempotent server-side regardless).
const [stopping, setStopping] = useState(false);
const [permissionPrompt, setPermissionPrompt] = useState<PermissionPrompt | null>(null);
const [permissionBusy, setPermissionBusy] = useState(false);
const [providerCommands, setProviderCommands] = useState<AgentCommand[]>([]);
@@ -986,14 +993,17 @@ export function CoderPane({
const handleStop = useCallback(async () => {
const taskId = activeTaskId;
if (!taskId) return;
if (!taskId || stopping) return; // ignore a second Stop while the POST is in flight
setStopping(true);
try {
await api.coder.cancelTask(taskId);
setActiveTaskId(null); // optimistic; WS/poll terminal-state also clears it
} catch (err) {
toast.error(err instanceof Error ? err.message : 'stop failed');
} finally {
setStopping(false);
}
}, [activeTaskId]);
}, [activeTaskId, stopping]);
// write-edit-robustness #4: reset the worktree to a message's checkpoint and
// trim the transcript past it. The confirm lives in MessageBubble's ActionRow
@@ -1125,6 +1135,7 @@ export function CoderPane({
onSend={handleChatInputSend}
generating={generating}
onStop={handleStop}
stopDisabled={stopping}
onSlashCommand={handleChatInputSlash}
slashGroups={slashGroups}
chatId={chatId ?? undefined}