diff --git a/claudetool/bash.go b/claudetool/bash.go index 1b5ebc0b148fc78f9e6f22fcadd7cecadbbba42e..418dce745a36a6a0a1e7de811fe173d914d8111b 100644 --- a/claudetool/bash.go +++ b/claudetool/bash.go @@ -82,7 +82,7 @@ func (t *Timeouts) background() time.Duration { func (b *BashTool) Tool() *llm.Tool { return &llm.Tool{ Name: bashName, - Description: fmt.Sprintf(strings.TrimSpace(bashDescription), b.getWorkingDir()), + Description: strings.TrimSpace(bashDescription), InputSchema: llm.MustSchema(bashInputSchema), Run: b.Run, } @@ -95,8 +95,7 @@ func (b *BashTool) getWorkingDir() string { const ( bashName = "bash" - bashDescription = ` -Executes shell commands via bash -c, returning combined stdout/stderr. + bashDescription = `Executes shell commands via bash -c, returning combined stdout/stderr. Bash state changes (working dir, variables, aliases) don't persist between calls. With background=true, returns immediately, with output redirected to a file. @@ -109,8 +108,6 @@ To change the working directory persistently, use the change_dir tool. IMPORTANT: Keep commands concise. The command input must be less than 60k tokens. For complex scripts, write them to a file first and then execute the file. - -%s ` // If you modify this, update the termui template for prettier rendering. bashInputSchema = ` diff --git a/claudetool/changedir.go b/claudetool/changedir.go index 8299cd6fa8faa8fef9cbd8b09fa447d9b65ed2db..48e1620d458d26790db73c84a66f5bea3357ac0b 100644 --- a/claudetool/changedir.go +++ b/claudetool/changedir.go @@ -6,11 +6,20 @@ import ( "fmt" "os" "path/filepath" + "strings" "shelley.exe.dev/gitstate" "shelley.exe.dev/llm" ) +// tildeReplace replaces the home directory prefix with ~ for display. +func tildeReplace(path string) string { + if home, err := os.UserHomeDir(); err == nil && strings.HasPrefix(path, home) { + return "~" + path[len(home):] + } + return path +} + // ChangeDirTool changes the working directory for bash commands. type ChangeDirTool struct { // WorkingDir is the shared mutable working directory. @@ -101,7 +110,10 @@ func (c *ChangeDirTool) Run(ctx context.Context, m json.RawMessage) llm.ToolOut state := gitstate.GetGitState(targetPath) var resultText string if state.IsRepo { - resultText = fmt.Sprintf("Changed working directory to: %s\n\nGit repository detected (root: %s)", targetPath, state.Worktree) + resultText = fmt.Sprintf("Changed working directory to: %s\n\nGit repository detected (root: %s, branch: %s)", targetPath, tildeReplace(state.Worktree), state.Branch) + if state.Branch == "" { + resultText = fmt.Sprintf("Changed working directory to: %s\n\nGit repository detected (root: %s, detached HEAD)", targetPath, tildeReplace(state.Worktree)) + } } else { resultText = fmt.Sprintf("Changed working directory to: %s\n\nNot in a git repository.", targetPath) } diff --git a/gitstate/gitstate.go b/gitstate/gitstate.go index edbaab0593671e669dda4d4f37e7635ccd0342c1..e9fac54fd3a198c6055be8692d144bd603499161 100644 --- a/gitstate/gitstate.go +++ b/gitstate/gitstate.go @@ -2,8 +2,8 @@ package gitstate import ( + "os" "os/exec" - "path/filepath" "strings" ) @@ -95,6 +95,14 @@ func (g *GitState) Equal(other *GitState) bool { g.IsRepo == other.IsRepo } +// tildeReplace replaces the home directory prefix with ~ for display. +func tildeReplace(path string) string { + if home, err := os.UserHomeDir(); err == nil && strings.HasPrefix(path, home) { + return "~" + path[len(home):] + } + return path +} + // String returns a human-readable description of the git state change. // It's designed to be shown to users, not the LLM. func (g *GitState) String() string { @@ -102,11 +110,14 @@ func (g *GitState) String() string { return "" } - // Get just the worktree name (last path component) - worktreeName := filepath.Base(g.Worktree) + worktreePath := tildeReplace(g.Worktree) + subject := g.Subject + if len(subject) > 50 { + subject = subject[:47] + "..." + } if g.Branch != "" { - return worktreeName + "/" + g.Branch + " now at " + g.Commit + return worktreePath + " (" + g.Branch + ") now at " + g.Commit + " \"" + subject + "\"" } - return worktreeName + " (detached) now at " + g.Commit + return worktreePath + " (detached) now at " + g.Commit + " \"" + subject + "\"" } diff --git a/gitstate/gitstate_test.go b/gitstate/gitstate_test.go index 61b48c8be99705f87031d3eb045d3b3c4ea23637..dfb8105ee17e2ceea8aeef6ead3e67ca046cfdb9 100644 --- a/gitstate/gitstate_test.go +++ b/gitstate/gitstate_test.go @@ -184,8 +184,9 @@ func TestGitState_String(t *testing.T) { }{ {"nil state", nil, ""}, {"not a repo", &GitState{IsRepo: false}, ""}, - {"with branch", &GitState{Worktree: "/home/user/myrepo", Branch: "main", Commit: "abc1234", IsRepo: true}, "myrepo/main now at abc1234"}, - {"detached head", &GitState{Worktree: "/home/user/myrepo", Branch: "", Commit: "abc1234", IsRepo: true}, "myrepo (detached) now at abc1234"}, + {"with branch", &GitState{Worktree: "/srv/myrepo", Branch: "main", Commit: "abc1234", Subject: "fix bug", IsRepo: true}, `/srv/myrepo (main) now at abc1234 "fix bug"`}, + {"detached head", &GitState{Worktree: "/srv/myrepo", Branch: "", Commit: "abc1234", Subject: "add feature", IsRepo: true}, `/srv/myrepo (detached) now at abc1234 "add feature"`}, + {"long subject truncated", &GitState{Worktree: "/srv/myrepo", Branch: "main", Commit: "abc1234", Subject: "this is a very long commit message that should be truncated", IsRepo: true}, `/srv/myrepo (main) now at abc1234 "this is a very long commit message that should ..."`}, } for _, tt := range tests { @@ -197,6 +198,32 @@ func TestGitState_String(t *testing.T) { } } +func TestTildeReplace(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil { + t.Skip("no home directory") + } + + tests := []struct { + name string + path string + expected string + }{ + {"home dir", home, "~"}, + {"subdir of home", home + "/projects/foo", "~/projects/foo"}, + {"not in home", "/var/log", "/var/log"}, + {"root", "/", "/"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tildeReplace(tt.path); got != tt.expected { + t.Errorf("tildeReplace(%q) = %q, want %q", tt.path, got, tt.expected) + } + }) + } +} + func runGit(t *testing.T, dir string, args ...string) { t.Helper() // For commits, use --no-verify to skip hooks