diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 00fb22a..1edae82 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -369,7 +369,7 @@ export default function App({ // Message queue state for queueing messages during streaming const [messageQueue, setMessageQueue] = useState([]); - // Queue cancellation: when queue length > 1, we send cancel and wait for natural stream end + // Queue cancellation: when any message is queued, we send cancel and wait for stream to end const waitingForQueueCancelRef = useRef(false); const queueSnapshotRef = useRef([]); const [restoreQueueOnCancel, setRestoreQueueOnCancel] = useState(false); @@ -736,6 +736,57 @@ export default function App({ return; } + // If in quietCancel mode (user queued messages), auto-reject all approvals + // and send denials + queued messages together + if (waitingForQueueCancelRef.current) { + if (restoreQueueOnCancelRef.current) { + // User hit ESC during queue cancel - abort the auto-send + setRestoreQueueOnCancel(false); + // Don't clear queue, don't send - let dequeue effect handle them one by one + } else { + // Create denial results for all approvals + const denialResults = approvalsToProcess.map( + (approvalItem) => ({ + type: "approval" as const, + tool_call_id: approvalItem.toolCallId, + approve: false, + reason: "User cancelled - new message queued", + }), + ); + + // Update buffers to show tools as cancelled + for (const approvalItem of approvalsToProcess) { + onChunk(buffersRef.current, { + message_type: "tool_return_message", + id: "dummy", + date: new Date().toISOString(), + tool_call_id: approvalItem.toolCallId, + tool_return: "Cancelled - user sent new message", + status: "error", + }); + } + refreshDerived(); + + // Queue denial results to be sent with the queued message + setQueuedApprovalResults(denialResults); + + // Get queued messages and clear queue + const concatenatedMessage = queueSnapshotRef.current.join("\n"); + setMessageQueue([]); + + // Send via onSubmit which will combine queuedApprovalResults + message + if (concatenatedMessage.trim()) { + onSubmitRef.current(concatenatedMessage); + } + } + + // Reset flags + waitingForQueueCancelRef.current = false; + queueSnapshotRef.current = []; + setStreaming(false); + return; + } + // Check permissions for all approvals (including fancy UI tools) const approvalResults = await Promise.all( approvalsToProcess.map(async (approvalItem) => { @@ -863,10 +914,6 @@ export default function App({ return; } - // Rotate to a new thinking message - setThinkingMessage(getRandomThinkingMessage()); - refreshDerived(); - // Combine auto-allowed results + auto-denied responses const allResults = [ ...autoAllowedResults.map((ar) => ({ @@ -885,6 +932,37 @@ export default function App({ })), ]; + // Check if user queued messages during auto-allowed tool execution + if (waitingForQueueCancelRef.current) { + if (restoreQueueOnCancelRef.current) { + // User hit ESC during queue cancel - abort the auto-send + setRestoreQueueOnCancel(false); + } else { + // Queue results to be sent with the queued message + setQueuedApprovalResults(allResults); + + // Get queued messages and clear queue + const concatenatedMessage = + queueSnapshotRef.current.join("\n"); + setMessageQueue([]); + + // Send via onSubmit + if (concatenatedMessage.trim()) { + onSubmitRef.current(concatenatedMessage); + } + } + + // Reset flags + waitingForQueueCancelRef.current = false; + queueSnapshotRef.current = []; + setStreaming(false); + return; + } + + // Rotate to a new thinking message + setThinkingMessage(getRandomThinkingMessage()); + refreshDerived(); + await processConversation([ { type: "approval", @@ -894,6 +972,68 @@ export default function App({ return; } + // Check again if user queued messages during auto-allowed tool execution + if (waitingForQueueCancelRef.current) { + if (restoreQueueOnCancelRef.current) { + // User hit ESC during queue cancel - abort the auto-send + setRestoreQueueOnCancel(false); + } else { + // Create denial results for tools that need user input + const denialResults = needsUserInput.map((ac) => ({ + type: "approval" as const, + tool_call_id: ac.approval.toolCallId, + approve: false, + reason: "User cancelled - new message queued", + })); + + // Update buffers to show tools as cancelled + for (const ac of needsUserInput) { + onChunk(buffersRef.current, { + message_type: "tool_return_message", + id: "dummy", + date: new Date().toISOString(), + tool_call_id: ac.approval.toolCallId, + tool_return: "Cancelled - user sent new message", + status: "error", + }); + } + refreshDerived(); + + // Combine with auto-handled results and queue for sending + const allResults = [ + ...autoAllowedResults.map((ar) => ({ + type: "tool" as const, + tool_call_id: ar.toolCallId, + tool_return: ar.result.toolReturn, + status: ar.result.status, + })), + ...autoDeniedResults.map((ad) => ({ + type: "approval" as const, + tool_call_id: ad.approval.toolCallId, + approve: false, + reason: ad.reason, + })), + ...denialResults, + ]; + setQueuedApprovalResults(allResults); + + // Get queued messages and clear queue + const concatenatedMessage = queueSnapshotRef.current.join("\n"); + setMessageQueue([]); + + // Send via onSubmit + if (concatenatedMessage.trim()) { + onSubmitRef.current(concatenatedMessage); + } + } + + // Reset flags + waitingForQueueCancelRef.current = false; + queueSnapshotRef.current = []; + setStreaming(false); + return; + } + // Show approval dialog for tools that need user input setPendingApprovals(needsUserInput.map((ac) => ac.approval)); setApprovalContexts( @@ -1102,11 +1242,11 @@ export default function App({ setMessageQueue((prev) => { const newQueue = [...prev, msg]; - // If queue grows to 2+ messages and we're not already waiting for cancel, - // send cancel request and capture snapshot - if (newQueue.length > 1 && !waitingForQueueCancelRef.current) { - // Capture snapshot of queue right now - queueSnapshotRef.current = [...newQueue]; + // Always update snapshot to include ALL queued messages + queueSnapshotRef.current = [...newQueue]; + + // If this is the first queued message, send cancel request + if (!waitingForQueueCancelRef.current) { waitingForQueueCancelRef.current = true; // Send cancel request to backend (fire-and-forget)