diff --git a/internal/discovery/discovery.go b/internal/discovery/discovery.go index 95b4631..d15fdc5 100644 --- a/internal/discovery/discovery.go +++ b/internal/discovery/discovery.go @@ -41,6 +41,17 @@ func DiscoverByAgent(agent string, maxAge time.Duration) []SessionInfo { } func decodeProjectDir(encoded string) string { + naive := naiveDecodeProjectDir(encoded) + if isDir(naive) { + return naive + } + if resolved := resolveEncodedProjectDir(encoded); resolved != "" { + return resolved + } + return naive +} + +func naiveDecodeProjectDir(encoded string) string { decoded := strings.TrimPrefix(encoded, "-") decoded = strings.ReplaceAll(decoded, "-", "/") if decoded != "" && decoded[0] != '/' { @@ -49,6 +60,41 @@ func decodeProjectDir(encoded string) string { return decoded } +// resolveEncodedProjectDir reconstructs a path that may contain literal hyphens. +// Claude Code encodes "/" as "-"; naive replace-all breaks names like "my-project". +// Prefer the longest filesystem prefix match at each segment. +func resolveEncodedProjectDir(encoded string) string { + trimmed := strings.TrimPrefix(encoded, "-") + if trimmed == "" { + return "" + } + parts := strings.Split(trimmed, "-") + path := "" + i := 0 + for i < len(parts) { + found := false + for j := len(parts); j > i; j-- { + segment := strings.Join(parts[i:j], "-") + candidate := path + "/" + segment + if isDir(candidate) { + path = candidate + i = j + found = true + break + } + } + if !found { + return "" + } + } + return path +} + +func isDir(path string) bool { + info, err := os.Stat(path) + return err == nil && info.IsDir() +} + func lastPathComponent(p string) string { return filepath.Base(p) } diff --git a/internal/discovery/discovery_test.go b/internal/discovery/discovery_test.go index e470b9a..463d455 100644 --- a/internal/discovery/discovery_test.go +++ b/internal/discovery/discovery_test.go @@ -1,6 +1,9 @@ package discovery import ( + "os" + "path/filepath" + "strings" "testing" ) @@ -21,3 +24,29 @@ func TestDecodeProjectDir(t *testing.T) { } } } + +func TestDecodeProjectDir_HyphenatedDirectory(t *testing.T) { + root := t.TempDir() + project := filepath.Join(root, "Users", "rfirke", "my-project") + if err := os.MkdirAll(project, 0o755); err != nil { + t.Fatal(err) + } + abs, err := filepath.Abs(project) + if err != nil { + t.Fatal(err) + } + + encoded := "-" + strings.ReplaceAll(strings.TrimPrefix(abs, "/"), "/", "-") + got := decodeProjectDir(encoded) + if got != abs { + t.Fatalf("decodeProjectDir(%q) = %q, want %q", encoded, got, abs) + } + + naive := naiveDecodeProjectDir(encoded) + if naive == abs { + t.Fatalf("expected naive decode to split hyphenated segment; got %q", naive) + } + if !strings.HasSuffix(naive, "/my/project") { + t.Fatalf("expected naive path to end with /my/project, got %q", naive) + } +}