fix: patch queueing (#201)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2025-12-12 22:24:14 -08:00
committed by GitHub
parent 28094b6cac
commit 7a46cde1da

View File

@@ -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)