fix(agent): Properly handle ESC interrupt in TUI with centralized event emission

Fixed the interrupt mechanism to show "[Interrupted by user]" message when ESC is pressed:
- Removed duplicate UI cleanup from ESC key handler that interfered with event processing
- Added centralized interrupted event emission in exception handler when abort signal is detected
- Removed duplicate event emissions from API call methods to prevent multiple messages
- Added abort signal support to preflight reasoning check for proper cancellation
- Simplified abort detection to only check signal state, not error messages
This commit is contained in:
Mario Zechner 2025-08-11 12:21:13 +02:00
parent 1f9d10cab0
commit 1d9b77298c
5 changed files with 218 additions and 22 deletions

View file

@ -178,7 +178,13 @@ async function checkReasoningSupport(
model: string,
api: "completions" | "responses",
baseURL?: string,
signal?: AbortSignal,
): Promise<boolean> {
// Check if already aborted
if (signal?.aborted) {
throw new Error("Interrupted");
}
// Check cache first
const cacheKey = model;
const cached = modelReasoningSupport.get(cacheKey);
@ -200,7 +206,7 @@ async function checkReasoningSupport(
effort: "low", // Use low instead of minimal to ensure we get summaries
},
};
await client.responses.create(testRequest);
await client.responses.create(testRequest, { signal });
supportsReasoning = true;
} catch (error) {
supportsReasoning = false;
@ -234,7 +240,7 @@ async function checkReasoningSupport(
testRequest.reasoning_effort = "minimal";
}
await client.chat.completions.create(testRequest);
await client.chat.completions.create(testRequest, { signal });
supportsReasoning = true;
} catch (error) {
supportsReasoning = false;
@ -263,7 +269,6 @@ export async function callModelResponsesApi(
while (!conversationDone) {
// Check if we've been interrupted
if (signal?.aborted) {
await eventReceiver?.on({ type: "interrupted" });
throw new Error("Interrupted");
}
@ -340,7 +345,6 @@ export async function callModelResponsesApi(
case "function_call": {
if (signal?.aborted) {
await eventReceiver?.on({ type: "interrupted" });
throw new Error("Interrupted");
}
@ -406,7 +410,6 @@ export async function callModelChatCompletionsApi(
while (!assistantResponded) {
if (signal?.aborted) {
await eventReceiver?.on({ type: "interrupted" });
throw new Error("Interrupted");
}
@ -456,7 +459,6 @@ export async function callModelChatCompletionsApi(
for (const toolCall of message.tool_calls) {
// Check if interrupted before executing tool
if (signal?.aborted) {
await eventReceiver?.on({ type: "interrupted" });
throw new Error("Interrupted");
}
@ -576,6 +578,7 @@ export class Agent {
this.config.model,
this.config.api,
this.config.baseURL,
this.abortController.signal,
);
}
@ -601,9 +604,10 @@ export class Agent {
);
}
} catch (e) {
// Check if this was an interruption
const errorMessage = e instanceof Error ? e.message : String(e);
if (errorMessage === "Interrupted" || this.abortController.signal.aborted) {
// Check if this was an interruption by checking the abort signal
if (this.abortController.signal.aborted) {
// Emit interrupted event so UI can clean up properly
await this.comboReceiver?.on({ type: "interrupted" });
return;
}
throw e;

View file

@ -113,19 +113,8 @@ export class TuiRenderer implements AgentEventReceiver {
this.onInterruptCallback();
}
// Stop the loading animation immediately
if (this.currentLoadingAnimation) {
this.currentLoadingAnimation.stop();
this.statusContainer.clear();
this.currentLoadingAnimation = null;
}
// Don't show message here - the interrupted event will handle it
// Re-enable editor submission
this.editor.disableSubmit = false;
this.ui.requestRender();
// Don't do any UI cleanup here - let the interrupted event handle it
// This avoids race conditions and ensures the interrupted message is shown
// Don't forward to editor
return false;
@ -280,6 +269,8 @@ export class TuiRenderer implements AgentEventReceiver {
this.chatContainer.addChild(new TextComponent(chalk.red("[Interrupted by user]"), { bottom: 1 }));
// Re-enable editor submission
this.editor.disableSubmit = false;
// Explicitly request render to ensure message is displayed
this.ui.requestRender();
break;
}