refactor: remove hardcoded dir skip list from project dir extraction

Replaced assumption-based logic (skipping "projects", "repos", "src")
with dynamic extraction: exec uses basename of cd target, file tools
use the first path component after workspace. No conventions assumed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-22 06:44:45 +09:00
parent 6297d3679e
commit e6e8482613
2 changed files with 53 additions and 61 deletions

View file

@ -956,49 +956,10 @@ var cdPrefixPattern = regexp.MustCompile(`^cd\s+(\S+)\s*&&\s*`)
// argument (e.g. "-A 20") are kept because removing them would lose context. // argument (e.g. "-A 20") are kept because removing them would lose context.
var optFlagPattern = regexp.MustCompile(`\s+--?\w[\w-]*(=\S*)?`) var optFlagPattern = regexp.MustCompile(`\s+--?\w[\w-]*(=\S*)?`)
// projectDirFromPath extracts the project directory name from an absolute path // extractProjectDir extracts a working directory name from a tool call.
// by stripping the workspace prefix and returning the first meaningful component. // For exec: uses the basename of the cd target (the directory AI actually works in).
// Generic directory names like "projects" or "repos" are skipped. // For file tools: strips the workspace prefix and takes the first path component.
// Returns "" if the path doesn't extend beyond workspace. // No assumptions are made about directory naming conventions.
func projectDirFromPath(absPath, workspace string) string {
absPath = strings.TrimRight(absPath, "/\\")
if absPath == "" {
return ""
}
if workspace == "" {
if idx := strings.LastIndex(absPath, "/"); idx >= 0 {
return absPath[idx+1:]
}
return absPath
}
ws := strings.TrimRight(workspace, "/\\")
rest := strings.TrimPrefix(absPath, ws)
if rest == absPath {
// workspace not a prefix — fall back to last component
if idx := strings.LastIndex(absPath, "/"); idx >= 0 {
return absPath[idx+1:]
}
return absPath
}
rest = strings.TrimLeft(rest, "/\\")
if rest == "" {
return ""
}
// Take first path component, skipping generic directory names
parts := strings.SplitN(rest, "/", 3)
if len(parts) >= 2 {
first := strings.ToLower(parts[0])
if first == "projects" || first == "repos" || first == "src" {
return parts[1]
}
}
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 { func extractProjectDir(toolName string, args map[string]interface{}, workspace string) string {
switch toolName { switch toolName {
case "exec": case "exec":
@ -1010,14 +971,38 @@ func extractProjectDir(toolName string, args map[string]interface{}, workspace s
if len(m) < 2 { if len(m) < 2 {
return "" return ""
} }
return projectDirFromPath(m[1], workspace) // basename of cd target — the directory the AI actually cd's into
cdPath := strings.TrimRight(m[1], "/\\")
if idx := strings.LastIndex(cdPath, "/"); idx >= 0 {
return cdPath[idx+1:]
}
if idx := strings.LastIndex(cdPath, "\\"); idx >= 0 {
return cdPath[idx+1:]
}
return cdPath
case "read_file", "write_file", "edit_file", "append_file", "list_dir": case "read_file", "write_file", "edit_file", "append_file", "list_dir":
path, _ := args["path"].(string) path, _ := args["path"].(string)
if path == "" { if path == "" {
return "" return ""
} }
return projectDirFromPath(path, workspace) ws := strings.TrimRight(workspace, "/\\")
if ws == "" {
return ""
}
rest := strings.TrimPrefix(path, ws)
if rest == path {
return "" // path not under workspace
}
rest = strings.TrimLeft(rest, "/\\")
if rest == "" {
return ""
}
// first component after workspace
if idx := strings.IndexAny(rest, "/\\"); idx >= 0 {
return rest[:idx]
}
return rest
} }
return "" return ""
} }

View file

@ -1612,34 +1612,34 @@ func TestExtractProjectDir(t *testing.T) {
workspace string workspace string
want string want string
}{ }{
// exec: cd prefix // exec: basename of cd target
{ {
name: "exec cd into projects subdir", name: "exec cd deep path",
toolName: "exec", toolName: "exec",
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/projects/terra-py-form && pytest"}, args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/projects/terra-py-form && pytest"},
workspace: ws, workspace: ws,
want: "terra-py-form", want: "terra-py-form",
}, },
{ {
name: "exec cd with trailing slash workspace", name: "exec cd direct subdir",
toolName: "exec",
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/projects/terra-py-form && ls"},
workspace: ws + "/",
want: "terra-py-form",
},
{
name: "exec cd into direct subdir",
toolName: "exec", toolName: "exec",
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/my-app && make build"}, args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/my-app && make build"},
workspace: ws, workspace: ws,
want: "my-app", want: "my-app",
}, },
{
name: "exec cd trailing slash target",
toolName: "exec",
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/my-app/ && ls"},
workspace: ws,
want: "my-app",
},
{ {
name: "exec cd to workspace itself", name: "exec cd to workspace itself",
toolName: "exec", toolName: "exec",
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace && ls"}, args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace && ls"},
workspace: ws, workspace: ws,
want: "", want: "workspace",
}, },
{ {
name: "exec no cd prefix", name: "exec no cd prefix",
@ -1648,16 +1648,16 @@ func TestExtractProjectDir(t *testing.T) {
workspace: ws, workspace: ws,
want: "", want: "",
}, },
// file tools // file tools: first component after workspace
{ {
name: "read_file in projects subdir", name: "read_file first component is projects",
toolName: "read_file", toolName: "read_file",
args: map[string]interface{}{"path": "/home/user/.picoclaw/workspace/projects/terra-py-form/src/main.py"}, args: map[string]interface{}{"path": "/home/user/.picoclaw/workspace/projects/terra-py-form/src/main.py"},
workspace: ws, workspace: ws,
want: "terra-py-form", want: "projects",
}, },
{ {
name: "edit_file in direct subdir", name: "edit_file direct subdir",
toolName: "edit_file", toolName: "edit_file",
args: map[string]interface{}{"path": "/home/user/.picoclaw/workspace/my-app/README.md"}, args: map[string]interface{}{"path": "/home/user/.picoclaw/workspace/my-app/README.md"},
workspace: ws, workspace: ws,
@ -1671,7 +1671,14 @@ func TestExtractProjectDir(t *testing.T) {
want: "notes.txt", want: "notes.txt",
}, },
{ {
name: "unknown tool returns empty", name: "file outside workspace",
toolName: "read_file",
args: map[string]interface{}{"path": "/tmp/foo.txt"},
workspace: ws,
want: "",
},
{
name: "unknown tool",
toolName: "web_search", toolName: "web_search",
args: map[string]interface{}{"query": "test"}, args: map[string]interface{}{"query": "test"},
workspace: ws, workspace: ws,