fix: use context-accurate cancel labels instead of hardcoded "Interrupted by user" (#598)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -1737,7 +1737,11 @@ export default function App({
|
||||
// If we're sending a new message, old pending state is no longer relevant
|
||||
// Pass false to avoid setting interrupted=true, which causes race conditions
|
||||
// with concurrent processConversation calls reading the flag
|
||||
markIncompleteToolsAsCancelled(buffersRef.current, false);
|
||||
markIncompleteToolsAsCancelled(
|
||||
buffersRef.current,
|
||||
false,
|
||||
"internal_cancel",
|
||||
);
|
||||
// Reset interrupted flag since we're starting a fresh stream
|
||||
buffersRef.current.interrupted = false;
|
||||
|
||||
@@ -2169,7 +2173,11 @@ export default function App({
|
||||
abortControllerRef.current?.signal.aborted
|
||||
) {
|
||||
setStreaming(false);
|
||||
markIncompleteToolsAsCancelled(buffersRef.current);
|
||||
markIncompleteToolsAsCancelled(
|
||||
buffersRef.current,
|
||||
true,
|
||||
"user_interrupt",
|
||||
);
|
||||
refreshDerived();
|
||||
return;
|
||||
}
|
||||
@@ -2422,7 +2430,11 @@ export default function App({
|
||||
queueApprovalResults(allResults, autoAllowedMetadata);
|
||||
}
|
||||
setStreaming(false);
|
||||
markIncompleteToolsAsCancelled(buffersRef.current);
|
||||
markIncompleteToolsAsCancelled(
|
||||
buffersRef.current,
|
||||
true,
|
||||
"user_interrupt",
|
||||
);
|
||||
refreshDerived();
|
||||
return;
|
||||
}
|
||||
@@ -2540,7 +2552,11 @@ export default function App({
|
||||
abortControllerRef.current?.signal.aborted
|
||||
) {
|
||||
setStreaming(false);
|
||||
markIncompleteToolsAsCancelled(buffersRef.current);
|
||||
markIncompleteToolsAsCancelled(
|
||||
buffersRef.current,
|
||||
true,
|
||||
"user_interrupt",
|
||||
);
|
||||
refreshDerived();
|
||||
return;
|
||||
}
|
||||
@@ -2762,7 +2778,11 @@ export default function App({
|
||||
llmApiErrorRetriesRef.current = 0;
|
||||
|
||||
// Mark incomplete tool calls as finished to prevent stuck blinking UI
|
||||
markIncompleteToolsAsCancelled(buffersRef.current);
|
||||
markIncompleteToolsAsCancelled(
|
||||
buffersRef.current,
|
||||
true,
|
||||
"stream_error",
|
||||
);
|
||||
|
||||
// Track the error in telemetry
|
||||
telemetry.trackError(
|
||||
@@ -2847,7 +2867,11 @@ export default function App({
|
||||
}
|
||||
} catch (e) {
|
||||
// Mark incomplete tool calls as cancelled to prevent stuck blinking UI
|
||||
markIncompleteToolsAsCancelled(buffersRef.current);
|
||||
markIncompleteToolsAsCancelled(
|
||||
buffersRef.current,
|
||||
true,
|
||||
e instanceof APIUserAbortError ? "user_interrupt" : "stream_error",
|
||||
);
|
||||
|
||||
// If using eager cancel and this is an abort error, silently ignore it
|
||||
// The user already got "Stream interrupted by user" feedback from handleInterrupt
|
||||
@@ -2979,7 +3003,11 @@ export default function App({
|
||||
// ALSO abort the main stream - don't leave it running
|
||||
buffersRef.current.abortGeneration =
|
||||
(buffersRef.current.abortGeneration || 0) + 1;
|
||||
const toolsCancelled = markIncompleteToolsAsCancelled(buffersRef.current);
|
||||
const toolsCancelled = markIncompleteToolsAsCancelled(
|
||||
buffersRef.current,
|
||||
true,
|
||||
"user_interrupt",
|
||||
);
|
||||
|
||||
// Mark any running subagents as interrupted
|
||||
interruptActiveSubagents(INTERRUPTED_BY_USER);
|
||||
@@ -3029,7 +3057,11 @@ export default function App({
|
||||
// This ensures onChunk and other guards see interrupted=true immediately.
|
||||
buffersRef.current.abortGeneration =
|
||||
(buffersRef.current.abortGeneration || 0) + 1;
|
||||
const toolsCancelled = markIncompleteToolsAsCancelled(buffersRef.current);
|
||||
const toolsCancelled = markIncompleteToolsAsCancelled(
|
||||
buffersRef.current,
|
||||
true,
|
||||
"user_interrupt",
|
||||
);
|
||||
|
||||
// Mark any running subagents as interrupted
|
||||
interruptActiveSubagents(INTERRUPTED_BY_USER);
|
||||
@@ -5931,7 +5963,11 @@ DO NOT respond to these messages or otherwise consider them in your response unl
|
||||
queueApprovalResults(queuedResults, autoAllowedMetadata);
|
||||
}
|
||||
setStreaming(false);
|
||||
markIncompleteToolsAsCancelled(buffersRef.current);
|
||||
markIncompleteToolsAsCancelled(
|
||||
buffersRef.current,
|
||||
true,
|
||||
"user_interrupt",
|
||||
);
|
||||
refreshDerived();
|
||||
return { submitted: false };
|
||||
}
|
||||
@@ -6174,7 +6210,11 @@ DO NOT respond to these messages or otherwise consider them in your response unl
|
||||
queueApprovalResults(queuedResults, autoAllowedMetadata);
|
||||
}
|
||||
setStreaming(false);
|
||||
markIncompleteToolsAsCancelled(buffersRef.current);
|
||||
markIncompleteToolsAsCancelled(
|
||||
buffersRef.current,
|
||||
true,
|
||||
"user_interrupt",
|
||||
);
|
||||
refreshDerived();
|
||||
return { submitted: false };
|
||||
}
|
||||
@@ -6783,7 +6823,7 @@ DO NOT respond to these messages or otherwise consider them in your response unl
|
||||
queueApprovalResults(denialResults);
|
||||
|
||||
// Mark the pending approval tool calls as cancelled in the buffers
|
||||
markIncompleteToolsAsCancelled(buffersRef.current);
|
||||
markIncompleteToolsAsCancelled(buffersRef.current, true, "approval_cancel");
|
||||
refreshDerived();
|
||||
|
||||
// Clear all approval state
|
||||
@@ -7340,7 +7380,11 @@ DO NOT respond to these messages or otherwise consider them in your response unl
|
||||
queueApprovalResults(denialResults);
|
||||
|
||||
// Mark tool as cancelled in buffers
|
||||
markIncompleteToolsAsCancelled(buffersRef.current);
|
||||
markIncompleteToolsAsCancelled(
|
||||
buffersRef.current,
|
||||
true,
|
||||
"internal_cancel",
|
||||
);
|
||||
refreshDerived();
|
||||
|
||||
// Clear all approval state (same as handleCancelApprovals)
|
||||
|
||||
@@ -272,9 +272,23 @@ export function markCurrentLineAsFinished(b: Buffers) {
|
||||
* with concurrent processConversation calls reading the flag.
|
||||
* @returns true if any tool calls were marked as cancelled
|
||||
*/
|
||||
export type CancelReason =
|
||||
| "user_interrupt"
|
||||
| "stream_error"
|
||||
| "internal_cancel"
|
||||
| "approval_cancel";
|
||||
|
||||
const CANCEL_REASON_TEXT: Record<CancelReason, string> = {
|
||||
user_interrupt: INTERRUPTED_BY_USER,
|
||||
stream_error: "Stream error",
|
||||
internal_cancel: "Cancelled",
|
||||
approval_cancel: "Approval cancelled",
|
||||
};
|
||||
|
||||
export function markIncompleteToolsAsCancelled(
|
||||
b: Buffers,
|
||||
setInterruptedFlag = true,
|
||||
reason: CancelReason = "internal_cancel",
|
||||
): boolean {
|
||||
// Mark buffer as interrupted to skip stale throttled refreshes
|
||||
// (only when actually interrupting, not when clearing stale state at startup)
|
||||
@@ -289,7 +303,7 @@ export function markIncompleteToolsAsCancelled(
|
||||
...line,
|
||||
phase: "finished" as const,
|
||||
resultOk: false,
|
||||
resultText: INTERRUPTED_BY_USER,
|
||||
resultText: CANCEL_REASON_TEXT[reason],
|
||||
};
|
||||
b.byId.set(id, updatedLine);
|
||||
anyToolsCancelled = true;
|
||||
|
||||
@@ -101,7 +101,7 @@ export async function drainStream(
|
||||
// Check if stream was aborted
|
||||
if (abortSignal?.aborted) {
|
||||
stopReason = "cancelled";
|
||||
markIncompleteToolsAsCancelled(buffers);
|
||||
markIncompleteToolsAsCancelled(buffers, true, "user_interrupt");
|
||||
queueMicrotask(refresh);
|
||||
break;
|
||||
}
|
||||
@@ -135,7 +135,7 @@ export async function drainStream(
|
||||
// Check abort signal before processing - don't add data after interrupt
|
||||
if (abortSignal?.aborted) {
|
||||
stopReason = "cancelled";
|
||||
markIncompleteToolsAsCancelled(buffers);
|
||||
markIncompleteToolsAsCancelled(buffers, true, "user_interrupt");
|
||||
queueMicrotask(refresh);
|
||||
break;
|
||||
}
|
||||
@@ -172,7 +172,7 @@ export async function drainStream(
|
||||
|
||||
// Set error stop reason so drainStreamWithResume can try to reconnect
|
||||
stopReason = "error";
|
||||
markIncompleteToolsAsCancelled(buffers);
|
||||
markIncompleteToolsAsCancelled(buffers, true, "stream_error");
|
||||
queueMicrotask(refresh);
|
||||
} finally {
|
||||
// Clean up abort listener
|
||||
@@ -189,7 +189,7 @@ export async function drainStream(
|
||||
// (SDK returns gracefully on abort), mark as cancelled
|
||||
if (abortedViaListener && !stopReason) {
|
||||
stopReason = "cancelled";
|
||||
markIncompleteToolsAsCancelled(buffers);
|
||||
markIncompleteToolsAsCancelled(buffers, true, "user_interrupt");
|
||||
queueMicrotask(refresh);
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ export async function drainStream(
|
||||
|
||||
// Mark incomplete tool calls as cancelled if stream was cancelled
|
||||
if (stopReason === "cancelled") {
|
||||
markIncompleteToolsAsCancelled(buffers);
|
||||
markIncompleteToolsAsCancelled(buffers, true, "user_interrupt");
|
||||
}
|
||||
|
||||
// Mark the final line as finished now that stream has ended
|
||||
|
||||
@@ -1460,7 +1460,7 @@ export async function handleHeadlessCommand(
|
||||
}
|
||||
|
||||
// Mark incomplete tool calls as cancelled to prevent stuck state
|
||||
markIncompleteToolsAsCancelled(buffers);
|
||||
markIncompleteToolsAsCancelled(buffers, true, "stream_error");
|
||||
|
||||
// Extract error details from buffers if available
|
||||
const errorLines = toLines(buffers).filter(
|
||||
@@ -1518,7 +1518,7 @@ export async function handleHeadlessCommand(
|
||||
}
|
||||
} catch (error) {
|
||||
// Mark incomplete tool calls as cancelled
|
||||
markIncompleteToolsAsCancelled(buffers);
|
||||
markIncompleteToolsAsCancelled(buffers, true, "stream_error");
|
||||
|
||||
// Use comprehensive error formatting (same as TUI mode)
|
||||
const errorDetails = formatErrorDetails(error, agent.id);
|
||||
|
||||
Reference in New Issue
Block a user