shelley: show working directory in bash tool output

Philip Zeyliger created

Prompt: change the bash tool output (at least in the UI) to indicate the working diretory the tool was run from.

Add working directory to bash tool display in the UI:
- Backend: Add BashDisplayData struct with WorkingDir field, include it
  in ToolOut.Display for both foreground and background commands
- Frontend: Show working directory in collapsed header (truncated with
  ellipsis) and in expanded details view
- CSS: Add styling for .bash-tool-cwd class

The working directory is shown as 'in /path/to/dir' after the command
in the collapsed view, and as a separate 'Working Directory:' section
when expanded.

Change summary

claudetool/bash.go             | 11 +++++++++--
claudetool/bash_test.go        |  9 +++++++++
ui/src/components/BashTool.tsx | 35 ++++++++++++++++++++++++++++++++++-
ui/src/styles.css              | 13 +++++++++++++
4 files changed, 65 insertions(+), 3 deletions(-)

Detailed changes

claudetool/bash.go 🔗

@@ -141,6 +141,11 @@ type bashInput struct {
 	Background bool   `json:"background,omitempty"`
 }
 
+// BashDisplayData is the display data sent to the UI for bash tool results.
+type BashDisplayData struct {
+	WorkingDir string `json:"workingDir"`
+}
+
 type BackgroundResult struct {
 	PID     int
 	OutFile string
@@ -203,13 +208,15 @@ func (b *BashTool) Run(ctx context.Context, m json.RawMessage) llm.ToolOut {
 
 	timeout := req.timeout(b.Timeouts)
 
+	display := BashDisplayData{WorkingDir: wd}
+
 	// If Background is set to true, use executeBackgroundBash
 	if req.Background {
 		result, err := b.executeBackgroundBash(ctx, req, timeout)
 		if err != nil {
 			return llm.ErrorToolOut(err)
 		}
-		return llm.ToolOut{LLMContent: llm.TextContent(result.XMLish())}
+		return llm.ToolOut{LLMContent: llm.TextContent(result.XMLish()), Display: display}
 	}
 
 	// For foreground commands, use executeBash
@@ -217,7 +224,7 @@ func (b *BashTool) Run(ctx context.Context, m json.RawMessage) llm.ToolOut {
 	if execErr != nil {
 		return llm.ErrorToolOut(execErr)
 	}
-	return llm.ToolOut{LLMContent: llm.TextContent(out)}
+	return llm.ToolOut{LLMContent: llm.TextContent(out), Display: display}
 }
 
 const maxBashOutputLength = 131072

claudetool/bash_test.go 🔗

@@ -87,6 +87,15 @@ func TestBashTool(t *testing.T) {
 		if len(result) == 0 || result[0].Text != expected {
 			t.Errorf("Expected %q, got %q", expected, result[0].Text)
 		}
+
+		// Verify Display data contains working directory
+		display, ok := toolOut.Display.(BashDisplayData)
+		if !ok {
+			t.Fatalf("Expected Display to be BashDisplayData, got %T", toolOut.Display)
+		}
+		if display.WorkingDir != "/" {
+			t.Errorf("Expected WorkingDir to be '/', got %q", display.WorkingDir)
+		}
 	})
 
 	// Test with arguments

ui/src/components/BashTool.tsx 🔗

@@ -1,6 +1,11 @@
 import React, { useState } from "react";
 import { LLMContent } from "../types";
 
+// Display data from the bash tool backend
+interface BashDisplayData {
+  workingDir: string;
+}
+
 interface BashToolProps {
   // For tool_use (pending state)
   toolInput?: unknown;
@@ -10,11 +15,28 @@ interface BashToolProps {
   toolResult?: LLMContent[];
   hasError?: boolean;
   executionTime?: string;
+  display?: unknown;
 }
 
-function BashTool({ toolInput, isRunning, toolResult, hasError, executionTime }: BashToolProps) {
+function BashTool({
+  toolInput,
+  isRunning,
+  toolResult,
+  hasError,
+  executionTime,
+  display,
+}: BashToolProps) {
   const [isExpanded, setIsExpanded] = useState(false);
 
+  // Extract working directory from display data
+  const displayData: BashDisplayData | null =
+    display &&
+    typeof display === "object" &&
+    "workingDir" in display &&
+    typeof display.workingDir === "string"
+      ? (display as BashDisplayData)
+      : null;
+
   // Extract command from toolInput
   const command =
     typeof toolInput === "object" &&
@@ -51,6 +73,11 @@ function BashTool({ toolInput, isRunning, toolResult, hasError, executionTime }:
         <div className="bash-tool-summary">
           <span className={`bash-tool-emoji ${isRunning ? "running" : ""}`}>🛠️</span>
           <span className="bash-tool-command">{displayCommand}</span>
+          {displayData?.workingDir && (
+            <span className="bash-tool-cwd" title={displayData.workingDir}>
+              in {displayData.workingDir}
+            </span>
+          )}
           {isComplete && isCancelled && <span className="bash-tool-cancelled">✗ cancelled</span>}
           {isComplete && hasError && !isCancelled && <span className="bash-tool-error">✗</span>}
           {isComplete && !hasError && <span className="bash-tool-success">✓</span>}
@@ -84,6 +111,12 @@ function BashTool({ toolInput, isRunning, toolResult, hasError, executionTime }:
 
       {isExpanded && (
         <div className="bash-tool-details">
+          {displayData?.workingDir && (
+            <div className="bash-tool-section">
+              <div className="bash-tool-label">Working Directory:</div>
+              <pre className="bash-tool-code bash-tool-code-cwd">{displayData.workingDir}</pre>
+            </div>
+          )}
           <div className="bash-tool-section">
             <div className="bash-tool-label">Command:</div>
             <pre className="bash-tool-code">{command}</pre>

ui/src/styles.css 🔗

@@ -1004,6 +1004,19 @@ button {
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
+  flex-shrink: 1;
+  min-width: 0;
+}
+
+.bash-tool-cwd {
+  font-family: var(--font-mono);
+  font-size: 0.75rem;
+  color: var(--text-tertiary);
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  flex-shrink: 0;
+  max-width: 30%;
 }
 
 .bash-tool-running {