Add tool confirmation UI with Accept/Decline for write operations

Before executing any write tool (create/update/delete), the backend now
pauses with an asyncio.Future and emits a tool_pending SSE event. The
frontend displays a ToolConfirmCard with Accept and Decline buttons.
Clicking Accept resolves the Future and proceeds; Decline records a
declined tool_call chip and falls through to regular streaming. Typing
single-word yes/no responses (e.g. "yes", "cancel") also works as
confirmation. 120s timeout auto-declines if no response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 20:43:47 -05:00
parent 1b63371bb3
commit 815eed2574
8 changed files with 304 additions and 43 deletions
+40
View File
@@ -17,6 +17,7 @@ import type {
OllamaStatus,
SendMessageResponse,
ToolCallRecord,
ToolPendingRecord,
} from "@/types/chat";
export const useChatStore = defineStore("chat", () => {
@@ -28,6 +29,7 @@ export const useChatStore = defineStore("chat", () => {
const streamingContent = ref("");
const streamingToolCalls = ref<ToolCallRecord[]>([]);
const streamingStatus = ref("");
const streamingPendingTool = ref<ToolPendingRecord | null>(null);
const lastContextMeta = ref<ContextMeta | null>(null);
const ollamaStatus = ref<"checking" | "available" | "unavailable">("checking");
const modelStatus = ref<"checking" | "loaded" | "cold" | "not_found">("checking");
@@ -124,6 +126,22 @@ export const useChatStore = defineStore("chat", () => {
const convId = currentConversation.value.id;
lastContextMeta.value = null;
// If a write tool is waiting for confirmation, intercept single-word yes/no
// responses rather than sending them as a new message.
if (streamingPendingTool.value) {
const lower = content.trim().toLowerCase();
const YES = new Set(["yes", "y", "ok", "sure", "proceed", "accept", "confirm", "do it", "go ahead", "yeah", "yep"]);
const NO = new Set(["no", "n", "cancel", "decline", "stop", "nope", "abort", "don't"]);
if (YES.has(lower)) {
await confirmTool(true);
return;
}
if (NO.has(lower)) {
await confirmTool(false);
return;
}
}
// Add optimistic user message
const userMsg: Message = {
id: -Date.now(), // Temporary ID
@@ -188,7 +206,12 @@ export const useChatStore = defineStore("chat", () => {
streamingContent.value += event.data.chunk as string;
streamingStatus.value = "";
break;
case "tool_pending":
streamingPendingTool.value = event.data.tool_pending as ToolPendingRecord;
break;
case "tool_call":
// Receiving a tool_call clears the pending confirmation card
streamingPendingTool.value = null;
streamingToolCalls.value = [
...streamingToolCalls.value,
event.data.tool_call as ToolCallRecord,
@@ -215,6 +238,7 @@ export const useChatStore = defineStore("chat", () => {
streamingContent.value = "";
streamingToolCalls.value = [];
streamingStatus.value = "";
streamingPendingTool.value = null;
streaming.value = false;
// Update conversation in list
@@ -231,6 +255,7 @@ export const useChatStore = defineStore("chat", () => {
streamingContent.value = "";
streamingToolCalls.value = [];
streamingStatus.value = "";
streamingPendingTool.value = null;
useToastStore().show(
"Chat error: " + (event.data.error as string),
"error",
@@ -269,6 +294,19 @@ export const useChatStore = defineStore("chat", () => {
streamingContent.value = "";
streamingToolCalls.value = [];
streamingStatus.value = "";
streamingPendingTool.value = null;
}
async function confirmTool(confirmed: boolean) {
const convId = currentConversation.value?.id;
if (!convId) return;
// Optimistically clear so buttons disappear immediately
streamingPendingTool.value = null;
try {
await apiPost(`/api/chat/conversations/${convId}/generation/confirm`, { confirmed });
} catch {
// Server will auto-decline on timeout — no action needed
}
}
async function cancelGeneration() {
@@ -353,6 +391,7 @@ export const useChatStore = defineStore("chat", () => {
streamingContent,
streamingToolCalls,
streamingStatus,
streamingPendingTool,
lastContextMeta,
ollamaStatus,
modelStatus,
@@ -364,6 +403,7 @@ export const useChatStore = defineStore("chat", () => {
deleteConversation,
updateTitle,
sendMessage,
confirmTool,
cancelGeneration,
saveMessageAsNote,
summarizeAsNote,