chore: improve shell output and summary to include cwd

Kujtim Hoxha created

Change summary

internal/llm/agent/agent.go                       |  3 +
internal/llm/tools/bash.go                        | 33 ++++++++--------
internal/tui/components/chat/messages/renderer.go | 13 +++++-
3 files changed, 30 insertions(+), 19 deletions(-)

Detailed changes

internal/llm/agent/agent.go 🔗

@@ -22,6 +22,7 @@ import (
 	"github.com/charmbracelet/crush/internal/permission"
 	"github.com/charmbracelet/crush/internal/pubsub"
 	"github.com/charmbracelet/crush/internal/session"
+	"github.com/charmbracelet/crush/internal/shell"
 )
 
 // Common errors
@@ -762,6 +763,8 @@ func (a *agent) Summarize(ctx context.Context, sessionID string) error {
 			a.Publish(pubsub.CreatedEvent, event)
 			return
 		}
+		shell := shell.GetPersistentShell(config.Get().WorkingDir())
+		summary += "\n\n**Current working directory of the persistent shell**\n\n" + shell.GetWorkingDir()
 		event = AgentEvent{
 			Type:     AgentEventTypeSummarize,
 			Progress: "Creating new session...",

internal/llm/tools/bash.go 🔗

@@ -4,7 +4,6 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"log/slog"
 	"strings"
 	"time"
 
@@ -23,8 +22,10 @@ type BashPermissionsParams struct {
 }
 
 type BashResponseMetadata struct {
-	StartTime int64 `json:"start_time"`
-	EndTime   int64 `json:"end_time"`
+	StartTime        int64  `json:"start_time"`
+	EndTime          int64  `json:"end_time"`
+	Output           string `json:"output"`
+	WorkingDirectory string `json:"working_directory"`
 }
 type bashTool struct {
 	permissions permission.Service
@@ -146,6 +147,7 @@ Before executing the command, please follow these steps:
 5. Return Result:
  - Provide the processed output of the command.
  - If any errors occurred during execution, include those in the output.
+ - The result will also have metadata like the cwd (current working directory) at the end, included with <cwd></cwd> tags.
 
 Usage notes:
 - The command argument is required.
@@ -389,9 +391,12 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
 		ctx, cancel = context.WithTimeout(ctx, time.Duration(params.Timeout)*time.Millisecond)
 		defer cancel()
 	}
-	stdout, stderr, err := shell.
-		GetPersistentShell(b.workingDir).
-		Exec(ctx, params.Command)
+
+	persistentShell := shell.GetPersistentShell(b.workingDir)
+	stdout, stderr, err := persistentShell.Exec(ctx, params.Command)
+
+	// Get the current working directory after command execution
+	currentWorkingDir := persistentShell.GetWorkingDir()
 	interrupted := shell.IsInterrupt(err)
 	exitCode := shell.ExitCode(err)
 	if exitCode == 0 && !interrupted && err != nil {
@@ -401,15 +406,6 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
 	stdout = truncateOutput(stdout)
 	stderr = truncateOutput(stderr)
 
-	slog.Info("Bash command executed",
-		"command", params.Command,
-		"stdout", stdout,
-		"stderr", stderr,
-		"exit_code", exitCode,
-		"interrupted", interrupted,
-		"err", err,
-	)
-
 	errorMessage := stderr
 	if errorMessage == "" && err != nil {
 		errorMessage = err.Error()
@@ -438,9 +434,12 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
 	}
 
 	metadata := BashResponseMetadata{
-		StartTime: startTime.UnixMilli(),
-		EndTime:   time.Now().UnixMilli(),
+		StartTime:        startTime.UnixMilli(),
+		EndTime:          time.Now().UnixMilli(),
+		Output:           stdout,
+		WorkingDirectory: currentWorkingDir,
 	}
+	stdout += fmt.Sprintf("\n\n<cwd>%s</cwd>", currentWorkingDir)
 	if stdout == "" {
 		return WithResponseMetadata(NewTextResponse(BashNoOutput), metadata), nil
 	}

internal/tui/components/chat/messages/renderer.go 🔗

@@ -212,10 +212,19 @@ func (br bashRenderer) Render(v *toolCallCmp) string {
 	args := newParamBuilder().addMain(cmd).build()
 
 	return br.renderWithParams(v, "Bash", args, func() string {
-		if v.result.Content == tools.BashNoOutput {
+		var meta tools.BashResponseMetadata
+		if err := br.unmarshalParams(v.result.Metadata, &meta); err != nil {
+			return renderPlainContent(v, v.result.Content)
+		}
+		// for backwards compatibility with older tool calls.
+		if meta.Output == "" && v.result.Content != tools.BashNoOutput {
+			meta.Output = v.result.Content
+		}
+
+		if meta.Output == "" {
 			return ""
 		}
-		return renderPlainContent(v, v.result.Content)
+		return renderPlainContent(v, meta.Output)
 	})
 }