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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user