160
src/cli/App.tsx
160
src/cli/App.tsx
@@ -369,7 +369,7 @@ export default function App({
|
||||
// Message queue state for queueing messages during streaming
|
||||
const [messageQueue, setMessageQueue] = useState<string[]>([]);
|
||||
|
||||
// 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<string[]>([]);
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user