Skip to content

Commit 268ca62

Browse files
committed
feat: allow client to cancel prompt
1 parent ffd2566 commit 268ca62

File tree

7 files changed

+83
-11
lines changed

7 files changed

+83
-11
lines changed

apps/array/src/main/preload.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
163163
ipcRenderer.invoke("agent-prompt", sessionId, prompt),
164164
agentCancel: async (sessionId: string): Promise<boolean> =>
165165
ipcRenderer.invoke("agent-cancel", sessionId),
166+
agentCancelPrompt: async (sessionId: string): Promise<boolean> =>
167+
ipcRenderer.invoke("agent-cancel-prompt", sessionId),
166168
agentListSessions: async (
167169
taskId?: string,
168170
): Promise<

apps/array/src/main/services/session-manager.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,19 @@ export class SessionManager {
284284
}
285285
}
286286

287+
async cancelPrompt(taskRunId: string): Promise<boolean> {
288+
const session = this.sessions.get(taskRunId);
289+
if (!session) return false;
290+
291+
try {
292+
await session.connection.cancel({ sessionId: taskRunId });
293+
return true;
294+
} catch (err) {
295+
log.error("Failed to cancel prompt", { taskRunId, err });
296+
return false;
297+
}
298+
}
299+
287300
getSession(taskRunId: string): ManagedSession | undefined {
288301
return this.sessions.get(taskRunId);
289302
}
@@ -516,6 +529,13 @@ export function registerAgentIpc(
516529
},
517530
);
518531

532+
ipcMain.handle(
533+
"agent-cancel-prompt",
534+
async (_event: IpcMainInvokeEvent, sessionId: string) => {
535+
return sessionManager.cancelPrompt(sessionId);
536+
},
537+
);
538+
519539
ipcMain.handle(
520540
"agent-list-sessions",
521541
async (

apps/array/src/renderer/features/logs/components/LogView.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface LogViewProps {
2626
isRunning: boolean;
2727
isPromptPending?: boolean;
2828
onSendPrompt?: (text: string) => Promise<void>;
29+
onCancelPrompt?: () => void;
2930
onCancelSession?: () => void;
3031
onStartSession?: () => void;
3132
}
@@ -62,6 +63,7 @@ export function LogView({
6263
isRunning,
6364
isPromptPending = false,
6465
onSendPrompt,
66+
onCancelPrompt,
6567
onCancelSession,
6668
onStartSession,
6769
}: LogViewProps) {
@@ -93,8 +95,12 @@ export function LogView({
9395
e.preventDefault();
9496
handleSend();
9597
}
98+
if (e.key === "Escape" && isPromptPending && onCancelPrompt) {
99+
e.preventDefault();
100+
onCancelPrompt();
101+
}
96102
},
97-
[handleSend],
103+
[handleSend, isPromptPending, onCancelPrompt],
98104
);
99105

100106
const handleCopyLogs = () => {
@@ -282,15 +288,23 @@ export function LogView({
282288
style={{ resize: "none" }}
283289
/>
284290
</Box>
285-
<Tooltip content="Send message (Enter)">
286-
<IconButton
287-
size="3"
288-
onClick={handleSend}
289-
disabled={!inputValue.trim() || isPromptPending || !isRunning}
290-
>
291-
<SendIcon size={20} />
292-
</IconButton>
293-
</Tooltip>
291+
{isPromptPending ? (
292+
<Tooltip content="Cancel (Esc)">
293+
<IconButton size="3" color="red" onClick={onCancelPrompt}>
294+
<StopIcon size={20} weight="fill" />
295+
</IconButton>
296+
</Tooltip>
297+
) : (
298+
<Tooltip content="Send message (Enter)">
299+
<IconButton
300+
size="3"
301+
onClick={handleSend}
302+
disabled={!inputValue.trim() || !isRunning}
303+
>
304+
<SendIcon size={20} />
305+
</IconButton>
306+
</Tooltip>
307+
)}
294308
</Flex>
295309
</Box>
296310
)}

apps/array/src/renderer/features/sessions/stores/sessionStore.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ interface SessionStore {
6262
prompt: string | ContentBlock[],
6363
) => Promise<{ stopReason: string }>;
6464

65+
// Cancel ongoing prompt without terminating session
66+
cancelPrompt: (taskId: string) => Promise<boolean>;
67+
6568
// Internal: subscribe to IPC events
6669
_subscribeToChannel: (
6770
taskRunId: string,
@@ -333,6 +336,18 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
333336
}
334337
},
335338

339+
cancelPrompt: async (taskId) => {
340+
const session = get().getSessionForTask(taskId);
341+
if (!session) return false;
342+
343+
try {
344+
return await window.electronAPI.agentCancelPrompt(session.taskRunId);
345+
} catch (error) {
346+
log.error("Failed to cancel prompt", error);
347+
return false;
348+
}
349+
},
350+
336351
_subscribeToChannel: (taskRunId, _taskId, channel) => {
337352
if (subscriptions.has(taskRunId)) {
338353
return;

apps/array/src/renderer/features/task-detail/components/TaskLogsPanel.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
3030
(state) => state.disconnectFromTask,
3131
);
3232
const sendPrompt = useSessionStore((state) => state.sendPrompt);
33+
const cancelPrompt = useSessionStore((state) => state.cancelPrompt);
3334

3435
const isRunning =
3536
session?.status === "connected" || session?.status === "connecting";
@@ -83,6 +84,11 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
8384
log.info("Agent session cancelled");
8485
}, [taskId, disconnectFromTask]);
8586

87+
const handleCancelPrompt = useCallback(async () => {
88+
const result = await cancelPrompt(taskId);
89+
log.info("Prompt cancelled", { success: result });
90+
}, [taskId, cancelPrompt]);
91+
8692
return (
8793
<BackgroundWrapper>
8894
<Box height="100%" width="100%">
@@ -92,6 +98,7 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
9298
isRunning={isRunning}
9399
isPromptPending={session?.isPromptPending}
94100
onSendPrompt={handleSendPrompt}
101+
onCancelPrompt={handleCancelPrompt}
95102
onCancelSession={handleCancelSession}
96103
onStartSession={handleStartSession}
97104
/>

apps/array/src/renderer/features/task-list/components/TaskItem.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { RenameTaskDialog } from "@components/RenameTaskDialog";
22
import { useTaskStore } from "@features/tasks/stores/taskStore";
33
import { useTaskContextMenu } from "@hooks/useTaskContextMenu";
4-
import { GitPullRequest } from "@phosphor-icons/react";
4+
import { Cloud, GitPullRequest } from "@phosphor-icons/react";
55
import { Badge, Box, Code, Flex, Text } from "@radix-ui/themes";
66
import type { Task } from "@shared/types";
77
import { differenceInHours, format, formatDistanceToNow } from "date-fns";
@@ -42,6 +42,7 @@ function TaskItemComponent({
4242
const prUrl = task.latest_run?.output?.pr_url as string | undefined;
4343
const hasPR = !!prUrl;
4444
const status = hasPR ? "completed" : task.latest_run?.status || "backlog";
45+
const isCloudTask = task.latest_run?.environment === "cloud";
4546

4647
const handleOpenPR = (e: React.MouseEvent) => {
4748
e.stopPropagation();
@@ -128,6 +129,18 @@ function TaskItemComponent({
128129
>
129130
{task.title}
130131
</Text>
132+
{isCloudTask && (
133+
<Flex
134+
align="center"
135+
gap="1"
136+
style={{ flexShrink: 0, opacity: 0.7 }}
137+
>
138+
<Cloud size={12} className="text-gray-10" />
139+
<Text size="1" color="gray">
140+
Cloud
141+
</Text>
142+
</Flex>
143+
)}
131144
{worktreeName && (
132145
<Text
133146
size="1"

apps/array/src/renderer/types/electron.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ declare global {
119119
prompt: ContentBlock[],
120120
) => Promise<{ stopReason: string }>;
121121
agentCancel: (sessionId: string) => Promise<boolean>;
122+
agentCancelPrompt: (sessionId: string) => Promise<boolean>;
122123
agentListSessions: (taskId?: string) => Promise<
123124
Array<{
124125
sessionId: string;

0 commit comments

Comments
 (0)