mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
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:
@@ -246,6 +246,7 @@ export default function App() {
|
||||
content = (
|
||||
<ChatPane
|
||||
onSend={(c) => api.sendSessionMessage({ sessionId: selectedSession.id, content: c })}
|
||||
onCancelTurn={() => { void api.cancelSessionTurn({ sessionId: selectedSession.id }); }}
|
||||
onResolveApproval={(approvalId, decision) =>
|
||||
api.resolveSessionApproval({ sessionId: selectedSession.id, approvalId, decision })
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
)}
|
||||
|
||||
@@ -43,6 +43,7 @@ const modeAccent: Record<OrchestrationMode, { dot: string; ring: string; text: s
|
||||
const runStatusStyles: Record<SessionRunRecord['status'], { icon: ReactNode; className: string }> = {
|
||||
running: { icon: <CircleDot className="size-3" />, className: 'text-blue-400' },
|
||||
completed: { icon: <CheckCircle2 className="size-3" />, className: 'text-emerald-400' },
|
||||
cancelled: { icon: <XCircle className="size-3" />, className: 'text-zinc-400' },
|
||||
error: { icon: <XCircle className="size-3" />, className: 'text-red-400' },
|
||||
};
|
||||
|
||||
@@ -65,6 +66,8 @@ function EventIcon({ kind, status }: { kind: RunTimelineEventRecord['kind']; sta
|
||||
return <MessageSquare className={`${base} ${status === 'running' ? 'text-blue-400 animate-pulse' : status === 'error' ? 'text-red-400' : 'text-emerald-400'}`} />;
|
||||
case 'run-completed':
|
||||
return <CheckCircle2 className={`${base} text-emerald-400`} />;
|
||||
case 'run-cancelled':
|
||||
return <XCircle className={`${base} text-zinc-400`} />;
|
||||
case 'run-failed':
|
||||
return <AlertTriangle className={`${base} text-red-400`} />;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ export function formatRunStatusLabel(status: SessionRunStatus): string {
|
||||
return 'Running';
|
||||
case 'completed':
|
||||
return 'Completed';
|
||||
case 'cancelled':
|
||||
return 'Cancelled';
|
||||
case 'error':
|
||||
return 'Failed';
|
||||
}
|
||||
@@ -91,6 +93,8 @@ export function formatEventLabel(event: RunTimelineEventRecord): string {
|
||||
return event.agentName ?? 'Response';
|
||||
case 'run-completed':
|
||||
return 'Completed';
|
||||
case 'run-cancelled':
|
||||
return 'Cancelled';
|
||||
case 'run-failed':
|
||||
return 'Failed';
|
||||
}
|
||||
@@ -144,11 +148,12 @@ const eventKindOrder: Record<RunTimelineEventKind, number> = {
|
||||
'approval': 4,
|
||||
'message': 5,
|
||||
'run-completed': 6,
|
||||
'run-cancelled': 6,
|
||||
'run-failed': 6,
|
||||
};
|
||||
|
||||
export function isTerminalEvent(kind: RunTimelineEventKind): boolean {
|
||||
return kind === 'run-completed' || kind === 'run-failed' || kind === 'run-started';
|
||||
return kind === 'run-completed' || kind === 'run-cancelled' || kind === 'run-failed' || kind === 'run-started';
|
||||
}
|
||||
|
||||
export function eventSortKey(event: RunTimelineEventRecord): number {
|
||||
|
||||
Reference in New Issue
Block a user