diff --git a/electron-builder.config.cjs b/electron-builder.config.cjs index 8d69f01f88..75ceaa9f63 100644 --- a/electron-builder.config.cjs +++ b/electron-builder.config.cjs @@ -37,6 +37,7 @@ const config = { asarUnpack: [ "dist/bin/**/*", // wavesrv and wsh binaries "dist/docsite/**/*", // the static docsite + "dist/schema/**/*", // schema files for Monaco editor ], mac: { target: [ diff --git a/frontend/app/aipanel/aitooluse.tsx b/frontend/app/aipanel/aitooluse.tsx index 06a0264c12..b6d40efd3d 100644 --- a/frontend/app/aipanel/aitooluse.tsx +++ b/frontend/app/aipanel/aitooluse.tsx @@ -81,6 +81,8 @@ interface AIToolUseBatchProps { const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => { const [userApprovalOverride, setUserApprovalOverride] = useState(null); + const partsRef = useRef(parts); + partsRef.current = parts; // All parts in a batch have the same approval status (enforced by grouping logic in AIToolUseGroup) const firstTool = parts[0].data; @@ -91,13 +93,13 @@ const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => { if (!isStreaming || effectiveApproval !== "needs-approval") return; const interval = setInterval(() => { - parts.forEach((part) => { + partsRef.current.forEach((part) => { WaveAIModel.getInstance().toolUseKeepalive(part.data.toolcallid); }); }, 4000); return () => clearInterval(interval); - }, [isStreaming, effectiveApproval, parts]); + }, [isStreaming, effectiveApproval]); const handleApprove = () => { setUserApprovalOverride("user-approved"); @@ -231,6 +233,8 @@ const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => { const showRestoreModal = restoreModalToolCallId === toolData.toolcallid; const highlightTimeoutRef = useRef(null); const highlightedBlockIdRef = useRef(null); + const toolCallIdRef = useRef(toolData.toolcallid); + toolCallIdRef.current = toolData.toolcallid; const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•"; const statusColor = @@ -245,11 +249,11 @@ const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => { if (!isStreaming || effectiveApproval !== "needs-approval") return; const interval = setInterval(() => { - WaveAIModel.getInstance().toolUseKeepalive(toolData.toolcallid); + WaveAIModel.getInstance().toolUseKeepalive(toolCallIdRef.current); }, 4000); return () => clearInterval(interval); - }, [isStreaming, effectiveApproval, toolData.toolcallid]); + }, [isStreaming, effectiveApproval]); useEffect(() => { return () => { @@ -399,13 +403,17 @@ export const AIToolUseGroup = memo(({ parts, isStreaming }: AIToolUseGroupProps) const isFileOpPart = isFileOp(part); const partNeedsApproval = needsApproval(part); - if (isFileOpPart && partNeedsApproval && !addedApprovalBatch) { - groupedItems.push({ type: "batch", parts: readFileNeedsApproval }); - addedApprovalBatch = true; - } else if (isFileOpPart && !partNeedsApproval && !addedOtherBatch) { - groupedItems.push({ type: "batch", parts: readFileOther }); - addedOtherBatch = true; - } else if (!isFileOpPart) { + if (isFileOpPart && partNeedsApproval) { + if (!addedApprovalBatch) { + groupedItems.push({ type: "batch", parts: readFileNeedsApproval }); + addedApprovalBatch = true; + } + } else if (isFileOpPart && !partNeedsApproval) { + if (!addedOtherBatch) { + groupedItems.push({ type: "batch", parts: readFileOther }); + addedOtherBatch = true; + } + } else { groupedItems.push({ type: "single", part }); } } diff --git a/package-lock.json b/package-lock.json index e436dc9511..745d6963dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "waveterm", - "version": "0.12.2-beta.1", + "version": "0.12.2-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "waveterm", - "version": "0.12.2-beta.1", + "version": "0.12.2-beta.2", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ diff --git a/pkg/aiusechat/tools_readdir.go b/pkg/aiusechat/tools_readdir.go index 2716166fa5..4b90d664c0 100644 --- a/pkg/aiusechat/tools_readdir.go +++ b/pkg/aiusechat/tools_readdir.go @@ -5,10 +5,12 @@ package aiusechat import ( "fmt" + "os" "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" "github.com/wavetermdev/waveterm/pkg/util/fileutil" "github.com/wavetermdev/waveterm/pkg/util/utilfn" + "github.com/wavetermdev/waveterm/pkg/wavebase" ) const ReadDirDefaultMaxEntries = 500 @@ -50,6 +52,29 @@ func parseReadDirInput(input any) (*readDirParams, error) { return result, nil } +func verifyReadDirInput(input any, toolUseData *uctypes.UIMessageDataToolUse) error { + params, err := parseReadDirInput(input) + if err != nil { + return err + } + + expandedPath, err := wavebase.ExpandHomeDir(params.Path) + if err != nil { + return fmt.Errorf("failed to expand path: %w", err) + } + + fileInfo, err := os.Stat(expandedPath) + if err != nil { + return fmt.Errorf("failed to stat path: %w", err) + } + + if !fileInfo.IsDir() { + return fmt.Errorf("path is not a directory, cannot be read with the read_dir tool. use the read_text_file tool if available to read files") + } + + return nil +} + func readDirCallback(input any, toolUseData *uctypes.UIMessageDataToolUse) (any, error) { params, err := parseReadDirInput(input) if err != nil { @@ -129,5 +154,6 @@ func GetReadDirToolDefinition() uctypes.ToolDefinition { ToolApproval: func(input any) string { return uctypes.ApprovalNeedsApproval }, + ToolVerifyInput: verifyReadDirInput, } } diff --git a/pkg/aiusechat/tools_readfile.go b/pkg/aiusechat/tools_readfile.go index 1d0a21969b..423333c831 100644 --- a/pkg/aiusechat/tools_readfile.go +++ b/pkg/aiusechat/tools_readfile.go @@ -197,6 +197,33 @@ func isBlockedFile(expandedPath string) (bool, string) { return false, "" } +func verifyReadTextFileInput(input any, toolUseData *uctypes.UIMessageDataToolUse) error { + params, err := parseReadTextFileInput(input) + if err != nil { + return err + } + + expandedPath, err := wavebase.ExpandHomeDir(params.Filename) + if err != nil { + return fmt.Errorf("failed to expand path: %w", err) + } + + if blocked, reason := isBlockedFile(expandedPath); blocked { + return fmt.Errorf("access denied: potentially sensitive file: %s", reason) + } + + fileInfo, err := os.Stat(expandedPath) + if err != nil { + return fmt.Errorf("failed to stat file: %w", err) + } + + if fileInfo.IsDir() { + return fmt.Errorf("path is a directory, cannot be read with the read_text_file tool. use the read_dir tool if available to read directories") + } + + return nil +} + func readTextFileCallback(input any, toolUseData *uctypes.UIMessageDataToolUse) (any, error) { const ReadLimit = 1024 * 1024 * 1024 @@ -370,5 +397,6 @@ func GetReadTextFileToolDefinition() uctypes.ToolDefinition { ToolApproval: func(input any) string { return uctypes.ApprovalNeedsApproval }, + ToolVerifyInput: verifyReadTextFileInput, } }