@@ -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.
-
-<pwd>%s</pwd>
`
// If you modify this, update the termui template for prettier rendering.
bashInputSchema = `
@@ -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)
}
@@ -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 + "\""
}
@@ -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