feat: add turn cancellation UI and main process wiring

Adds cancel-turn IPC channel, SidecarClient.cancelTurn(), TurnCancelledError,
EryxAppService.cancelSessionTurn(), finalizeCancelledTurn(), stop button in
ChatPane, and cancelled run status throughout the run timeline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-25 22:17:15 +01:00
co-authored by Copilot
parent d84b3021f2
commit 82fdadb312
13 changed files with 265 additions and 49 deletions
+18 -7
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { AlertCircle, ArrowUp, Bot, Circle, GitBranch, Loader2, ShieldAlert, User } from 'lucide-react';
import { AlertCircle, ArrowUp, Bot, Circle, GitBranch, Loader2, ShieldAlert, Square, User } from 'lucide-react';
import { MarkdownContent } from '@renderer/components/MarkdownContent';
import { MarkdownComposer, type MarkdownComposerHandle } from '@renderer/components/MarkdownComposer';
@@ -34,6 +34,7 @@ interface ChatPaneProps {
toolingSettings: WorkspaceToolingSettings;
runtimeTools?: ReadonlyArray<RuntimeToolDefinition>;
onSend: (content: string) => Promise<void>;
onCancelTurn?: () => void;
onResolveApproval?: (approvalId: string, decision: ApprovalDecision) => Promise<unknown>;
onUpdateScratchpadConfig?: (config: {
model: string;
@@ -51,6 +52,7 @@ export function ChatPane({
toolingSettings,
runtimeTools,
onSend,
onCancelTurn,
onResolveApproval,
onUpdateScratchpadConfig,
onUpdateSessionTooling,
@@ -432,16 +434,25 @@ export function ChatPane({
>
<button
className={`absolute bottom-2 right-2 flex size-8 items-center justify-center rounded-lg transition ${
canSubmitInput
? 'bg-indigo-600 text-white hover:bg-indigo-500'
: 'bg-zinc-800 text-zinc-600'
isSessionBusy
? 'bg-red-600/80 text-white hover:bg-red-500'
: canSubmitInput
? 'bg-indigo-600 text-white hover:bg-indigo-500'
: 'bg-zinc-800 text-zinc-600'
}`}
disabled={!canSubmitInput}
onClick={() => composerRef.current?.submit()}
disabled={!canSubmitInput && !isSessionBusy}
onClick={() => {
if (isSessionBusy) {
onCancelTurn?.();
} else {
composerRef.current?.submit();
}
}}
type="button"
aria-label={isSessionBusy ? 'Stop generating' : 'Send message'}
>
{isSessionBusy ? (
<Loader2 className="size-4 animate-spin" />
<Square className="size-3.5" fill="currentColor" />
) : (
<ArrowUp className="size-4" />
)}