From d02597fe8a2fa0b89824aa369ca1f39b316ec6c4 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Oct 2025 18:41:33 -0700 Subject: [PATCH 1/6] working on batch approval of read-file tools... --- frontend/app/aipanel/aimessage.tsx | 180 ++++++++++++++++++++++++- frontend/app/aipanel/aipanel.tsx | 8 +- pkg/aiusechat/openai/openai-backend.go | 2 - pkg/aiusechat/usechat.go | 8 ++ 4 files changed, 187 insertions(+), 11 deletions(-) diff --git a/frontend/app/aipanel/aimessage.tsx b/frontend/app/aipanel/aimessage.tsx index 1a11805d28..09f9338df9 100644 --- a/frontend/app/aipanel/aimessage.tsx +++ b/frontend/app/aipanel/aimessage.tsx @@ -68,12 +68,107 @@ const UserMessageFiles = memo(({ fileParts }: UserMessageFilesProps) => { UserMessageFiles.displayName = "UserMessageFiles"; +interface AIToolUseBatchProps { + parts: Array; + isStreaming: boolean; +} + +const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => { + const [userApprovalOverride, setUserApprovalOverride] = useState(null); + + if (parts.length === 0) return null; + + const firstTool = parts[0].data; + const baseApproval = userApprovalOverride || firstTool.approval; + const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval; + const allNeedApproval = parts.every(p => (userApprovalOverride || p.data.approval) === "needs-approval"); + + useEffect(() => { + if (!isStreaming || effectiveApproval !== "needs-approval") return; + + const interval = setInterval(() => { + parts.forEach(part => { + RpcApi.WaveAIToolApproveCommand(TabRpcClient, { + toolcallid: part.data.toolcallid, + keepalive: true, + }); + }); + }, 4000); + + return () => clearInterval(interval); + }, [isStreaming, effectiveApproval, parts]); + + const handleApprove = () => { + setUserApprovalOverride("user-approved"); + parts.forEach(part => { + RpcApi.WaveAIToolApproveCommand(TabRpcClient, { + toolcallid: part.data.toolcallid, + approval: "user-approved", + }); + }); + }; + + const handleDeny = () => { + setUserApprovalOverride("user-denied"); + parts.forEach(part => { + RpcApi.WaveAIToolApproveCommand(TabRpcClient, { + toolcallid: part.data.toolcallid, + approval: "user-denied", + }); + }); + }; + + const groupTitle = firstTool.toolname === "read_text_file" ? "Reading Files" : "Listing Directories"; + + return ( +
+
+
{groupTitle}
+
+ {parts.map((part, idx) => { + const statusIcon = part.data.status === "completed" ? "✓" : part.data.status === "error" ? "✗" : "•"; + const statusColor = part.data.status === "completed" ? "text-success" : part.data.status === "error" ? "text-error" : "text-gray-400"; + const effectiveErrorMessage = part.data.errormessage || (effectiveApproval === "timeout" ? "Not approved" : null); + return ( +
+ {statusIcon} + {part.data.tooldesc} + {effectiveErrorMessage && ( +
{effectiveErrorMessage}
+ )} +
+ ); + })} +
+ {allNeedApproval && effectiveApproval === "needs-approval" && ( +
+ + +
+ )} +
+
+ ); +}); + +AIToolUseBatch.displayName = "AIToolUseBatch"; + interface AIToolUseProps { part: WaveUIMessagePart & { type: "data-tooluse" }; isStreaming: boolean; } -const AIToolUse = memo(({ part }: AIToolUseProps) => { +const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => { const toolData = part.data; const [userApprovalOverride, setUserApprovalOverride] = useState(null); @@ -81,10 +176,11 @@ const AIToolUse = memo(({ part }: AIToolUseProps) => { const statusColor = toolData.status === "completed" ? "text-success" : toolData.status === "error" ? "text-error" : "text-gray-400"; - const effectiveApproval = userApprovalOverride || toolData.approval; + const baseApproval = userApprovalOverride || toolData.approval; + const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval; useEffect(() => { - if (effectiveApproval !== "needs-approval") return; + if (!isStreaming || effectiveApproval !== "needs-approval") return; const interval = setInterval(() => { RpcApi.WaveAIToolApproveCommand(TabRpcClient, { @@ -94,7 +190,7 @@ const AIToolUse = memo(({ part }: AIToolUseProps) => { }, 4000); return () => clearInterval(interval); - }, [effectiveApproval, toolData.toolcallid]); + }, [isStreaming, effectiveApproval, toolData.toolcallid]); const handleApprove = () => { setUserApprovalOverride("user-approved"); @@ -118,7 +214,11 @@ const AIToolUse = memo(({ part }: AIToolUseProps) => {
{toolData.toolname}
{toolData.tooldesc &&
{toolData.tooldesc}
} - {toolData.errormessage &&
{toolData.errormessage}
} + {(toolData.errormessage || effectiveApproval === "timeout") && ( +
+ {toolData.errormessage || "Not approved"} +
+ )} {effectiveApproval === "needs-approval" && (
+ +
+ ); +}); + +AIToolApprovalButtons.displayName = "AIToolApprovalButtons"; + +interface AIToolUseBatchItemProps { + part: WaveUIMessagePart & { type: "data-tooluse" }; + effectiveApproval: string; +} + +const AIToolUseBatchItem = memo(({ part, effectiveApproval }: AIToolUseBatchItemProps) => { + const statusIcon = part.data.status === "completed" ? "✓" : part.data.status === "error" ? "✗" : "•"; + const statusColor = + part.data.status === "completed" + ? "text-success" + : part.data.status === "error" + ? "text-error" + : "text-gray-400"; + const effectiveErrorMessage = part.data.errormessage || (effectiveApproval === "timeout" ? "Not approved" : null); + + return ( +
+ {statusIcon} +
+ {part.data.tooldesc} + {effectiveErrorMessage &&
{effectiveErrorMessage}
} +
+
+ ); +}); + +AIToolUseBatchItem.displayName = "AIToolUseBatchItem"; + interface AIToolUseBatchProps { parts: Array; isStreaming: boolean; @@ -118,50 +176,17 @@ const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => { }); }; - const groupTitle = firstTool.toolname === "read_text_file" ? "Reading Files" : "Listing Directories"; - return (
-
{groupTitle}
+
Reading Files
- {parts.map((part, idx) => { - const statusIcon = - part.data.status === "completed" ? "✓" : part.data.status === "error" ? "✗" : "•"; - const statusColor = - part.data.status === "completed" - ? "text-success" - : part.data.status === "error" - ? "text-error" - : "text-gray-400"; - const effectiveErrorMessage = - part.data.errormessage || (effectiveApproval === "timeout" ? "Not approved" : null); - return ( -
- {statusIcon} - {part.data.tooldesc} - {effectiveErrorMessage && ( -
{effectiveErrorMessage}
- )} -
- ); - })} + {parts.map((part, idx) => ( + + ))}
{allNeedApproval && effectiveApproval === "needs-approval" && ( -
- - -
+ )}
@@ -225,20 +250,7 @@ const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => {
{toolData.errormessage || "Not approved"}
)} {effectiveApproval === "needs-approval" && ( -
- - -
+ )}
@@ -382,33 +394,20 @@ const getThinkingMessage = (parts: WaveUIMessagePart[], isStreaming: boolean, ro return "Waiting for Tool Approvals..."; } - // Find the last "step-start" marker - let lastStartStepIndex = -1; - for (let i = parts.length - 1; i >= 0; i--) { - if (parts[i].type === "step-start") { - lastStartStepIndex = i; - break; - } - } - - // Get parts after the last start-step (or all parts if no start-step) - const partsAfterLastStep = lastStartStepIndex !== -1 ? parts.slice(lastStartStepIndex + 1) : parts; - - // Check if there's content after the last step - const hasContentAfterStep = partsAfterLastStep.some( - (part) => (part.type === "text" && part.text) || part.type.startsWith("tool-") || part.type === "data-tooluse" - ); - - if (hasContentAfterStep) { - return null; - } + const lastPart = parts[parts.length - 1]; // Check if the last part is a reasoning part - const lastPart = parts[parts.length - 1]; if (lastPart?.type === "reasoning") { return "AI is thinking..."; } + // Only hide thinking indicator if the last part is text and not empty + // (this means text is actively streaming) + if (lastPart?.type === "text" && lastPart.text) { + return null; + } + + // For all other cases (including finish-step, tooluse, etc.), show dots return ""; }; From a73226726ecc2195a4dddb8e2256d90bdc137152 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Oct 2025 23:27:28 -0700 Subject: [PATCH 6/6] fix out of order hook issue, and hack for windows paths in vite manual chunks... --- electron.vite.config.ts | 11 ++++++----- frontend/app/aipanel/aimessage.tsx | 4 +--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 43eeb869d8..ae5de1887c 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -132,14 +132,15 @@ export default defineConfig({ }, output: { manualChunks(id) { - if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco"; - if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid")) + const p = id.replace(/\\/g, "/"); + if (p.includes("node_modules/monaco") || p.includes("node_modules/@monaco")) return "monaco"; + if (p.includes("node_modules/mermaid") || p.includes("node_modules/@mermaid")) return "mermaid"; - if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex"; - if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) { + if (p.includes("node_modules/katex") || p.includes("node_modules/@katex")) return "katex"; + if (p.includes("node_modules/shiki") || p.includes("node_modules/@shiki")) { return "shiki"; } - if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape")) + if (p.includes("node_modules/cytoscape") || p.includes("node_modules/@cytoscape")) return "cytoscape"; return undefined; }, diff --git a/frontend/app/aipanel/aimessage.tsx b/frontend/app/aipanel/aimessage.tsx index e860cf4304..02ccb5d947 100644 --- a/frontend/app/aipanel/aimessage.tsx +++ b/frontend/app/aipanel/aimessage.tsx @@ -127,15 +127,13 @@ const AIToolUseBatchItem = memo(({ part, effectiveApproval }: AIToolUseBatchItem AIToolUseBatchItem.displayName = "AIToolUseBatchItem"; interface AIToolUseBatchProps { - parts: Array; + parts: Array; // parts must not be empty isStreaming: boolean; } const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => { const [userApprovalOverride, setUserApprovalOverride] = useState(null); - if (parts.length === 0) return null; - const firstTool = parts[0].data; const baseApproval = userApprovalOverride || firstTool.approval; const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval;