From 74fb4913b125143b40490f6b3846d4e7167ad47a Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 8 Apr 2026 19:08:33 -0500 Subject: [PATCH] fix(remote-questions): cancel local TUI when remote answer wins the race showInterviewRound now accepts an AbortSignal via opts.signal. When the remote channel wins the race, controller.abort() closes the local TUI modal instead of leaving an orphaned interactive prompt capturing input. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/ask-user-questions.ts | 2 +- src/resources/extensions/shared/interview-ui.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/ask-user-questions.ts b/src/resources/extensions/ask-user-questions.ts index ab7ff280a..3cb7e2ae1 100644 --- a/src/resources/extensions/ask-user-questions.ts +++ b/src/resources/extensions/ask-user-questions.ts @@ -254,7 +254,7 @@ export default function AskUserQuestions(pi: ExtensionAPI) { const raceResult = await raceRemoteAndLocal( () => tryRemoteQuestions(params.questions, raceSignal), - () => showInterviewRound(params.questions, {}, ctx as any), + () => showInterviewRound(params.questions, { signal: raceSignal }, ctx as any), raceController, params.questions, ); diff --git a/src/resources/extensions/shared/interview-ui.ts b/src/resources/extensions/shared/interview-ui.ts index c38833761..66771bc84 100644 --- a/src/resources/extensions/shared/interview-ui.ts +++ b/src/resources/extensions/shared/interview-ui.ts @@ -80,6 +80,12 @@ export interface InterviewRoundOptions { * Label for the Esc-confirm overlay header. Defaults to "End interview?". */ exitHeadline?: string; + /** + * Optional AbortSignal to cancel the interview externally (e.g. when racing + * against a remote question channel). When aborted, the TUI closes and the + * promise resolves with an empty answers object. + */ + signal?: AbortSignal; /** * Text for the "exit" hint shown in the review screen footer and exit confirm overlay. * Defaults to "end interview". @@ -207,6 +213,13 @@ export async function showInterviewRound( let exitCursor = 0; // 0 = keep going (default), 1 = end interview let cachedLines: string[] | undefined; + // External cancellation (e.g. remote channel won the race) + if (opts.signal) { + const onAbort = () => done({ endInterview: false, answers: {} }); + if (opts.signal.aborted) { onAbort(); } + else { opts.signal.addEventListener("abort", onAbort, { once: true }); } + } + // Editor is created once; editorTheme comes from the design system const editorRef = { current: null as Editor | null };