From 1f819be818d49e6ed423a25ce9ea5631fa29ce99 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Wed, 3 Jun 2026 02:55:32 +0800 Subject: [PATCH 1/2] refactor(app): extract abort + pending registry from submit.ts Move the AbortSource/PendingPrompt types, the shared pending map, and the abort closure out of prompt-input/submit.ts into a new submit-abort.ts as a createAbort(deps) factory. Pure extraction, no behavior change; submit.ts imports the pending registry and wires createAbort with its existing deps. --- .../components/prompt-input/submit-abort.ts | 68 +++++++++++++++++++ .../app/src/components/prompt-input/submit.ts | 66 +++--------------- 2 files changed, 76 insertions(+), 58 deletions(-) create mode 100644 packages/app/src/components/prompt-input/submit-abort.ts diff --git a/packages/app/src/components/prompt-input/submit-abort.ts b/packages/app/src/components/prompt-input/submit-abort.ts new file mode 100644 index 000000000..b8b0a552f --- /dev/null +++ b/packages/app/src/components/prompt-input/submit-abort.ts @@ -0,0 +1,68 @@ +import type { useSDK } from "@/context/sdk" +import { emitRendererDiagnostic, sessionAbortDiagnosticEvent } from "@/context/renderer-diagnostics" +import { rendererAbortDiagnosticSource, type RendererAbortSource } from "@/session/abort-source" + +export type AbortSource = Extract + +export type PendingPrompt = { + abort: AbortController + cleanup: VoidFunction +} + +export const pending = new Map() + +const emitAbortDiagnostic = (input: { + routeSessionID?: string + visibleSessionID?: string + timelineSessionID?: string + source: AbortSource + result: "aborted" | "ignored_awaiting_question" +}) => { + void emitRendererDiagnostic(sessionAbortDiagnosticEvent(input)).catch(() => undefined) +} + +export function createAbort(deps: { + abortReady: () => boolean + sessionID: () => string | undefined + onAbort?: () => void + client: ReturnType["client"] +}) { + return async (source: AbortSource = "stopButton") => { + if (!deps.abortReady()) return Promise.resolve() + + const activeSessionID = deps.sessionID() + if (!activeSessionID) return Promise.resolve() + + deps.onAbort?.() + + const queued = pending.get(activeSessionID) + if (queued) { + queued.abort.abort() + queued.cleanup() + pending.delete(activeSessionID) + emitAbortDiagnostic({ + routeSessionID: activeSessionID, + visibleSessionID: activeSessionID, + timelineSessionID: activeSessionID, + source, + result: "aborted", + }) + return Promise.resolve() + } + return deps.client.session + .abort({ + sessionID: activeSessionID, + source: rendererAbortDiagnosticSource({ sessionID: activeSessionID, source }), + }) + .then((result) => { + emitAbortDiagnostic({ + routeSessionID: activeSessionID, + visibleSessionID: activeSessionID, + timelineSessionID: activeSessionID, + source, + result: result.data === false ? "ignored_awaiting_question" : "aborted", + }) + }) + .catch(() => {}) + } +} diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 21ab45780..19c123b3a 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -10,13 +10,12 @@ import { useLayout } from "@/context/layout" import { useLocal } from "@/context/local" import { usePermission } from "@/context/permission" import { type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt" -import { emitRendererDiagnostic, sessionAbortDiagnosticEvent } from "@/context/renderer-diagnostics" +import { emitRendererDiagnostic } from "@/context/renderer-diagnostics" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { promptProbe } from "@/testing/prompt" import { Identifier } from "@/utils/id" import { Worktree as WorktreeState } from "@/utils/worktree" -import { rendererAbortDiagnosticSource, type RendererAbortSource } from "@/session/abort-source" import { setCursorPosition } from "./editor-dom" import { reportInvariantBreach } from "./invariant" import { formatServerError } from "@/utils/server-errors" @@ -28,14 +27,7 @@ import type { ResolvedMention } from "./mention-metadata" import type { FollowupDraft } from "./followup-draft" import { detectSubmitOwnership, type SubmitOwnership } from "./submit-ownership" import { sendFollowupDraft } from "./send-followup-draft" - -type PendingPrompt = { - abort: AbortController - cleanup: VoidFunction -} - -const pending = new Map() -type AbortSource = Extract +import { createAbort, pending } from "./submit-abort" type PromptSubmitInput = { sessionID?: Accessor @@ -102,54 +94,12 @@ export function createPromptSubmit(input: PromptSubmitInput) { return language.t("common.requestFailed") } - const emitAbortDiagnostic = (input: { - routeSessionID?: string - visibleSessionID?: string - timelineSessionID?: string - source: AbortSource - result: "aborted" | "ignored_awaiting_question" - }) => { - void emitRendererDiagnostic(sessionAbortDiagnosticEvent(input)).catch(() => undefined) - } - - const abort = async (source: AbortSource = "stopButton") => { - if (!abortReady()) return Promise.resolve() - - const activeSessionID = sessionID() - if (!activeSessionID) return Promise.resolve() - - input.onAbort?.() - - const queued = pending.get(activeSessionID) - if (queued) { - queued.abort.abort() - queued.cleanup() - pending.delete(activeSessionID) - emitAbortDiagnostic({ - routeSessionID: activeSessionID, - visibleSessionID: activeSessionID, - timelineSessionID: activeSessionID, - source, - result: "aborted", - }) - return Promise.resolve() - } - return sdk.client.session - .abort({ - sessionID: activeSessionID, - source: rendererAbortDiagnosticSource({ sessionID: activeSessionID, source }), - }) - .then((result) => { - emitAbortDiagnostic({ - routeSessionID: activeSessionID, - visibleSessionID: activeSessionID, - timelineSessionID: activeSessionID, - source, - result: result.data === false ? "ignored_awaiting_question" : "aborted", - }) - }) - .catch(() => {}) - } + const abort = createAbort({ + abortReady, + sessionID, + onAbort: input.onAbort, + client: sdk.client, + }) const restoreCommentItems = (items: CommentItem[]) => { for (const item of items) { From 81233c01c3c4a68dafcb515b018475a522a46098 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Wed, 3 Jun 2026 03:00:20 +0800 Subject: [PATCH 2/2] refactor(app): pass sdk client to createAbort as a getter sdk.client is a directory-dependent getter, not a stable reference; capturing it once at factory time would let a long-lived submit instance abort against a stale workspace client. Read it lazily via () => sdk.client so the abort call resolves the current client each invocation, matching the pre-extraction behavior exactly. --- packages/app/src/components/prompt-input/submit-abort.ts | 4 ++-- packages/app/src/components/prompt-input/submit.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/prompt-input/submit-abort.ts b/packages/app/src/components/prompt-input/submit-abort.ts index b8b0a552f..063c20bcd 100644 --- a/packages/app/src/components/prompt-input/submit-abort.ts +++ b/packages/app/src/components/prompt-input/submit-abort.ts @@ -25,7 +25,7 @@ export function createAbort(deps: { abortReady: () => boolean sessionID: () => string | undefined onAbort?: () => void - client: ReturnType["client"] + client: () => ReturnType["client"] }) { return async (source: AbortSource = "stopButton") => { if (!deps.abortReady()) return Promise.resolve() @@ -49,7 +49,7 @@ export function createAbort(deps: { }) return Promise.resolve() } - return deps.client.session + return deps.client().session .abort({ sessionID: activeSessionID, source: rendererAbortDiagnosticSource({ sessionID: activeSessionID, source }), diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 19c123b3a..731204b7d 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -98,7 +98,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { abortReady, sessionID, onAbort: input.onAbort, - client: sdk.client, + client: () => sdk.client, }) const restoreCommentItems = (items: CommentItem[]) => {