feat: extract project dir from file tool paths too, not just exec cd
Refactored extractProjectDir to accept any tool call. For read_file, edit_file etc., the file path is used to detect the project directory. Extracted shared logic into projectDirFromPath. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0b21bc6196
commit
6a43b564d5
2 changed files with 99 additions and 45 deletions
|
|
@ -956,37 +956,35 @@ var cdPrefixPattern = regexp.MustCompile(`^cd\s+(\S+)\s*&&\s*`)
|
|||
// argument (e.g. "-A 20") are kept because removing them would lose context.
|
||||
var optFlagPattern = regexp.MustCompile(`\s+--?\w[\w-]*(=\S*)?`)
|
||||
|
||||
// extractProjectDir extracts the project directory name from an exec command's
|
||||
// "cd <path> && ..." prefix by stripping the workspace prefix and taking the
|
||||
// first remaining path component.
|
||||
// Returns "" if no cd prefix or no deeper directory exists.
|
||||
func extractProjectDir(cmd, workspace string) string {
|
||||
m := cdPrefixPattern.FindStringSubmatch(cmd)
|
||||
if len(m) < 2 {
|
||||
// projectDirFromPath extracts the project directory name from an absolute path
|
||||
// by stripping the workspace prefix and returning the first meaningful component.
|
||||
// Generic directory names like "projects" or "repos" are skipped.
|
||||
// Returns "" if the path doesn't extend beyond workspace.
|
||||
func projectDirFromPath(absPath, workspace string) string {
|
||||
absPath = strings.TrimRight(absPath, "/\\")
|
||||
if absPath == "" {
|
||||
return ""
|
||||
}
|
||||
cdPath := strings.TrimRight(m[1], "/\\")
|
||||
if workspace == "" {
|
||||
if idx := strings.LastIndex(cdPath, "/"); idx >= 0 {
|
||||
return cdPath[idx+1:]
|
||||
if idx := strings.LastIndex(absPath, "/"); idx >= 0 {
|
||||
return absPath[idx+1:]
|
||||
}
|
||||
return cdPath
|
||||
return absPath
|
||||
}
|
||||
ws := strings.TrimRight(workspace, "/\\")
|
||||
rest := strings.TrimPrefix(cdPath, ws)
|
||||
if rest == cdPath {
|
||||
rest := strings.TrimPrefix(absPath, ws)
|
||||
if rest == absPath {
|
||||
// workspace not a prefix — fall back to last component
|
||||
if idx := strings.LastIndex(cdPath, "/"); idx >= 0 {
|
||||
return cdPath[idx+1:]
|
||||
if idx := strings.LastIndex(absPath, "/"); idx >= 0 {
|
||||
return absPath[idx+1:]
|
||||
}
|
||||
return cdPath
|
||||
return absPath
|
||||
}
|
||||
rest = strings.TrimLeft(rest, "/\\")
|
||||
if rest == "" {
|
||||
return ""
|
||||
}
|
||||
// Take first path component (e.g. "projects/my-app" → "projects")
|
||||
// But if it looks like a generic dir (projects, src, workspace), go deeper
|
||||
// Take first path component, skipping generic directory names
|
||||
parts := strings.SplitN(rest, "/", 3)
|
||||
if len(parts) >= 2 {
|
||||
first := strings.ToLower(parts[0])
|
||||
|
|
@ -997,6 +995,33 @@ func extractProjectDir(cmd, workspace string) string {
|
|||
return parts[0]
|
||||
}
|
||||
|
||||
// extractProjectDir extracts the project directory name from a tool call.
|
||||
// For exec: parses the "cd <path> && ..." prefix.
|
||||
// For file tools: uses the file path directly.
|
||||
// Returns "" if no project directory can be determined.
|
||||
func extractProjectDir(toolName string, args map[string]interface{}, workspace string) string {
|
||||
switch toolName {
|
||||
case "exec":
|
||||
cmd, _ := args["command"].(string)
|
||||
if cmd == "" {
|
||||
return ""
|
||||
}
|
||||
m := cdPrefixPattern.FindStringSubmatch(cmd)
|
||||
if len(m) < 2 {
|
||||
return ""
|
||||
}
|
||||
return projectDirFromPath(m[1], workspace)
|
||||
|
||||
case "read_file", "write_file", "edit_file", "append_file", "list_dir":
|
||||
path, _ := args["path"].(string)
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
return projectDirFromPath(path, workspace)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// buildArgsSnippet produces a human-friendly snippet for the tool log.
|
||||
// For exec: extracts the command and strips the leading "cd <workspace> && ".
|
||||
// For file tools: extracts the path and strips the workspace prefix.
|
||||
|
|
@ -1534,11 +1559,9 @@ func (al *AgentLoop) runLLMIteration(
|
|||
ArgsSnip: buildArgsSnippet(tc.Name, tc.Arguments, agent.Workspace),
|
||||
Result: "\u23F3",
|
||||
})
|
||||
// Detect project directory from exec cd prefix (once)
|
||||
if task.projectDir == "" && tc.Name == "exec" {
|
||||
if cmd, _ := tc.Arguments["command"].(string); cmd != "" {
|
||||
task.projectDir = extractProjectDir(cmd, agent.Workspace)
|
||||
}
|
||||
// Detect project directory from tool call args (once)
|
||||
if task.projectDir == "" {
|
||||
task.projectDir = extractProjectDir(tc.Name, tc.Arguments, agent.Workspace)
|
||||
}
|
||||
}
|
||||
task.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -1604,54 +1604,85 @@ func TestBuildRichStatus_ProjectDir(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractProjectDir(t *testing.T) {
|
||||
ws := "/home/user/.picoclaw/workspace"
|
||||
tests := []struct {
|
||||
name string
|
||||
cmd string
|
||||
toolName string
|
||||
args map[string]interface{}
|
||||
workspace string
|
||||
want string
|
||||
}{
|
||||
// exec: cd prefix
|
||||
{
|
||||
name: "cd into projects subdir",
|
||||
cmd: "cd /home/user/.picoclaw/workspace/projects/terra-py-form && pytest",
|
||||
workspace: "/home/user/.picoclaw/workspace",
|
||||
name: "exec cd into projects subdir",
|
||||
toolName: "exec",
|
||||
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/projects/terra-py-form && pytest"},
|
||||
workspace: ws,
|
||||
want: "terra-py-form",
|
||||
},
|
||||
{
|
||||
name: "cd into projects subdir with trailing slash",
|
||||
cmd: "cd /home/user/.picoclaw/workspace/projects/terra-py-form && ls",
|
||||
workspace: "/home/user/.picoclaw/workspace/",
|
||||
name: "exec cd with trailing slash workspace",
|
||||
toolName: "exec",
|
||||
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/projects/terra-py-form && ls"},
|
||||
workspace: ws + "/",
|
||||
want: "terra-py-form",
|
||||
},
|
||||
{
|
||||
name: "cd into direct subdir",
|
||||
cmd: "cd /home/user/.picoclaw/workspace/my-app && make build",
|
||||
workspace: "/home/user/.picoclaw/workspace",
|
||||
name: "exec cd into direct subdir",
|
||||
toolName: "exec",
|
||||
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/my-app && make build"},
|
||||
workspace: ws,
|
||||
want: "my-app",
|
||||
},
|
||||
{
|
||||
name: "cd to workspace itself",
|
||||
cmd: "cd /home/user/.picoclaw/workspace && ls",
|
||||
workspace: "/home/user/.picoclaw/workspace",
|
||||
name: "exec cd to workspace itself",
|
||||
toolName: "exec",
|
||||
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace && ls"},
|
||||
workspace: ws,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "no cd prefix",
|
||||
cmd: "pytest tests/",
|
||||
workspace: "/home/user/.picoclaw/workspace",
|
||||
name: "exec no cd prefix",
|
||||
toolName: "exec",
|
||||
args: map[string]interface{}{"command": "pytest tests/"},
|
||||
workspace: ws,
|
||||
want: "",
|
||||
},
|
||||
// file tools
|
||||
{
|
||||
name: "cd to unrelated path",
|
||||
cmd: "cd /tmp/build && make",
|
||||
workspace: "/home/user/.picoclaw/workspace",
|
||||
want: "build",
|
||||
name: "read_file in projects subdir",
|
||||
toolName: "read_file",
|
||||
args: map[string]interface{}{"path": "/home/user/.picoclaw/workspace/projects/terra-py-form/src/main.py"},
|
||||
workspace: ws,
|
||||
want: "terra-py-form",
|
||||
},
|
||||
{
|
||||
name: "edit_file in direct subdir",
|
||||
toolName: "edit_file",
|
||||
args: map[string]interface{}{"path": "/home/user/.picoclaw/workspace/my-app/README.md"},
|
||||
workspace: ws,
|
||||
want: "my-app",
|
||||
},
|
||||
{
|
||||
name: "write_file at workspace root",
|
||||
toolName: "write_file",
|
||||
args: map[string]interface{}{"path": "/home/user/.picoclaw/workspace/notes.txt"},
|
||||
workspace: ws,
|
||||
want: "notes.txt",
|
||||
},
|
||||
{
|
||||
name: "unknown tool returns empty",
|
||||
toolName: "web_search",
|
||||
args: map[string]interface{}{"query": "test"},
|
||||
workspace: ws,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := extractProjectDir(tt.cmd, tt.workspace)
|
||||
got := extractProjectDir(tt.toolName, tt.args, tt.workspace)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractProjectDir(%q, %q) = %q, want %q", tt.cmd, tt.workspace, got, tt.want)
|
||||
t.Errorf("extractProjectDir(%q, args, %q) = %q, want %q", tt.toolName, tt.workspace, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue