Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions electron-builder.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
30 changes: 19 additions & 11 deletions frontend/app/aipanel/aitooluse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ interface AIToolUseBatchProps {

const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => {
const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(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;
Expand All @@ -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");
Expand Down Expand Up @@ -231,6 +233,8 @@ const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => {
const showRestoreModal = restoreModalToolCallId === toolData.toolcallid;
const highlightTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const highlightedBlockIdRef = useRef<string | null>(null);
const toolCallIdRef = useRef(toolData.toolcallid);
toolCallIdRef.current = toolData.toolcallid;

const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
const statusColor =
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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 });
}
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions pkg/aiusechat/tools_readdir.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -129,5 +154,6 @@ func GetReadDirToolDefinition() uctypes.ToolDefinition {
ToolApproval: func(input any) string {
return uctypes.ApprovalNeedsApproval
},
ToolVerifyInput: verifyReadDirInput,
}
}
28 changes: 28 additions & 0 deletions pkg/aiusechat/tools_readfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -370,5 +397,6 @@ func GetReadTextFileToolDefinition() uctypes.ToolDefinition {
ToolApproval: func(input any) string {
return uctypes.ApprovalNeedsApproval
},
ToolVerifyInput: verifyReadTextFileInput,
}
}
Loading