refactor(tools): remove touch tool; allow empty write content

vorticalbox created

Change summary

internal/agent/coordinator.go      |   1 
internal/agent/tools/touch.go      | 160 -----------------------------
internal/agent/tools/touch.md      |  27 ----
internal/agent/tools/touch_test.go | 174 --------------------------------
internal/agent/tools/write.go      |   4 
internal/agent/tools/write.md      |   2 
internal/agent/tools/write_test.go |  49 +++++++++
internal/config/config.go          |   1 
internal/config/load_test.go       |   4 
internal/proto/permission.go       |   6 -
internal/proto/tools.go            |  19 ---
internal/ui/chat/file.go           |  49 ---------
internal/ui/chat/tools.go          |  25 ----
internal/ui/dialog/permissions.go  |  16 --
14 files changed, 54 insertions(+), 483 deletions(-)

Detailed changes

internal/agent/coordinator.go 🔗

@@ -488,7 +488,6 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent, isSubA
 		tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Config().Tools.Ls),
 		tools.NewSourcegraphTool(nil),
 		tools.NewTodosTool(c.sessions),
-		tools.NewTouchTool(c.lspManager, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()),
 		tools.NewViewTool(c.lspManager, c.permissions, c.filetracker, c.skillTracker, c.cfg.WorkingDir(), c.cfg.Config().Options.SkillsPaths...),
 		tools.NewWriteTool(c.lspManager, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()),
 	)

internal/agent/tools/touch.go 🔗

@@ -1,160 +0,0 @@
-package tools
-
-import (
-	"context"
-	_ "embed"
-	"fmt"
-	"os"
-	"path/filepath"
-	"strings"
-
-	"charm.land/fantasy"
-	"github.com/charmbracelet/crush/internal/filepathext"
-	"github.com/charmbracelet/crush/internal/filetracker"
-	"github.com/charmbracelet/crush/internal/fsext"
-	"github.com/charmbracelet/crush/internal/history"
-	"github.com/charmbracelet/crush/internal/lsp"
-	"github.com/charmbracelet/crush/internal/permission"
-)
-
-//go:embed touch.md
-var touchDescription []byte
-
-type TouchParams struct {
-	FilePath string `json:"file_path" description:"The path to the empty file to create"`
-}
-
-type TouchPermissionsParams struct {
-	FilePath   string `json:"file_path"`
-	OldContent string `json:"old_content,omitempty"`
-	NewContent string `json:"new_content,omitempty"`
-}
-
-type TouchResponseMetadata struct {
-	FilePath string `json:"file_path"`
-}
-
-const TouchToolName = "touch"
-
-func NewTouchTool(
-	lspManager *lsp.Manager,
-	permissions permission.Service,
-	files history.Service,
-	filetracker filetracker.Service,
-	workingDir string,
-) fantasy.AgentTool {
-	return fantasy.NewAgentTool(
-		TouchToolName,
-		FirstLineDescription(touchDescription),
-		func(ctx context.Context, params TouchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
-			if params.FilePath == "" {
-				return fantasy.NewTextErrorResponse("file_path is required"), nil
-			}
-
-			sessionID := GetSessionFromContext(ctx)
-			if sessionID == "" {
-				return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")
-			}
-
-			filePath := filepathext.SmartJoin(workingDir, params.FilePath)
-
-			absWorkingDir, err := filepath.Abs(workingDir)
-			if err != nil {
-				return fantasy.ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
-			}
-			absFilePath, err := filepath.Abs(filePath)
-			if err != nil {
-				return fantasy.ToolResponse{}, fmt.Errorf("error resolving file path: %w", err)
-			}
-			relPath, relErr := filepath.Rel(absWorkingDir, absFilePath)
-			isOutsideWorkDir := relErr != nil || strings.HasPrefix(relPath, "..")
-
-			if isOutsideWorkDir {
-				granted, permReqErr := permissions.Request(ctx,
-					permission.CreatePermissionRequest{
-						SessionID:   sessionID,
-						Path:        absFilePath,
-						ToolCallID:  call.ID,
-						ToolName:    TouchToolName,
-						Action:      "write",
-						Description: fmt.Sprintf("Create empty file outside working directory: %s", absFilePath),
-						Params: TouchPermissionsParams{
-							FilePath: absFilePath,
-						},
-					},
-				)
-				if permReqErr != nil {
-					return fantasy.ToolResponse{}, permReqErr
-				}
-				if !granted {
-					return NewPermissionDeniedResponse(), nil
-				}
-			}
-
-			fileInfo, err := os.Stat(absFilePath)
-			if err == nil {
-				if fileInfo.IsDir() {
-					return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", absFilePath)), nil
-				}
-				return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", absFilePath)), nil
-			} else if !os.IsNotExist(err) {
-				return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
-			}
-
-			dir := filepath.Dir(absFilePath)
-			if err = os.MkdirAll(dir, 0o755); err != nil {
-				return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
-			}
-
-			p, err := permissions.Request(ctx,
-				permission.CreatePermissionRequest{
-					SessionID:   sessionID,
-					Path:        fsext.PathOrPrefix(absFilePath, absWorkingDir),
-					ToolCallID:  call.ID,
-					ToolName:    TouchToolName,
-					Action:      "write",
-					Description: fmt.Sprintf("Create empty file %s", absFilePath),
-					Params: TouchPermissionsParams{
-						FilePath:   absFilePath,
-						OldContent: "",
-						NewContent: "",
-					},
-				},
-			)
-			if err != nil {
-				return fantasy.ToolResponse{}, err
-			}
-			if !p {
-				return NewPermissionDeniedResponse(), nil
-			}
-
-			file, err := os.OpenFile(absFilePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644)
-			if err != nil {
-				if os.IsExist(err) {
-					return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", absFilePath)), nil
-				}
-				return fantasy.ToolResponse{}, fmt.Errorf("error creating file: %w", err)
-			}
-			if err = file.Close(); err != nil {
-				return fantasy.ToolResponse{}, fmt.Errorf("error closing file: %w", err)
-			}
-
-			_, err = files.Create(ctx, sessionID, absFilePath, "")
-			if err != nil {
-				return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
-			}
-
-			filetracker.RecordRead(ctx, sessionID, absFilePath)
-
-			notifyLSPs(ctx, lspManager, absFilePath)
-
-			result := fmt.Sprintf("Empty file successfully created: %s", absFilePath)
-			result = fmt.Sprintf("<result>\n%s\n</result>", result)
-			result += getDiagnostics(absFilePath, lspManager)
-			return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
-				TouchResponseMetadata{
-					FilePath: absFilePath,
-				},
-			), nil
-		})
-}

internal/agent/tools/touch.md 🔗

@@ -1,27 +0,0 @@
-Create an empty file; auto-creates parent dirs. Fails if the file already exists.
-
-<usage>
-- Provide file path to create
-- Tool creates necessary parent directories automatically
-</usage>
-
-<features>
-- Creates new empty files
-- Auto-creates parent directories if missing
-- Refuses to overwrite existing files
-</features>
-
-<limitations>
-- Cannot write content
-- Cannot update modification times for existing files
-- Cannot create directories
-</limitations>
-
-<cross_platform>
-- Use forward slashes (/) for compatibility
-</cross_platform>
-
-<tips>
-- Use Write tool when the file should contain content
-- Use LS tool to verify location when creating new files
-</tips>

internal/agent/tools/touch_test.go 🔗

@@ -1,174 +0,0 @@
-package tools
-
-import (
-	"context"
-	"encoding/json"
-	"os"
-	"path/filepath"
-	"strings"
-	"testing"
-	"time"
-
-	"charm.land/fantasy"
-	"github.com/charmbracelet/crush/internal/permission"
-	"github.com/charmbracelet/crush/internal/pubsub"
-	"github.com/stretchr/testify/require"
-)
-
-// recordingPermissionService captures permission requests and answers them
-// according to a configurable response.
-type recordingPermissionService struct {
-	*pubsub.Broker[permission.PermissionRequest]
-	requests []permission.CreatePermissionRequest
-	grant    bool
-}
-
-func (m *recordingPermissionService) Request(ctx context.Context, req permission.CreatePermissionRequest) (bool, error) {
-	m.requests = append(m.requests, req)
-	return m.grant, nil
-}
-
-func (m *recordingPermissionService) Grant(req permission.PermissionRequest)           {}
-func (m *recordingPermissionService) Deny(req permission.PermissionRequest)            {}
-func (m *recordingPermissionService) GrantPersistent(req permission.PermissionRequest) {}
-func (m *recordingPermissionService) AutoApproveSession(sessionID string)              {}
-func (m *recordingPermissionService) SetSkipRequests(skip bool)                        {}
-func (m *recordingPermissionService) SkipRequests() bool                               { return false }
-func (m *recordingPermissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[permission.PermissionNotification] {
-	return make(<-chan pubsub.Event[permission.PermissionNotification])
-}
-
-type mockFileTrackerService struct{}
-
-func (m mockFileTrackerService) RecordRead(ctx context.Context, sessionID, path string) {}
-
-func (m mockFileTrackerService) LastReadTime(ctx context.Context, sessionID, path string) time.Time {
-	return time.Now()
-}
-
-func (m mockFileTrackerService) ListReadFiles(ctx context.Context, sessionID string) ([]string, error) {
-	return nil, nil
-}
-
-func TestTouchToolCreatesEmptyFile(t *testing.T) {
-	t.Parallel()
-
-	workingDir := t.TempDir()
-	tool := NewTouchTool(nil, &mockPermissionService{}, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
-	ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
-
-	resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "nested/empty.txt"})
-	require.False(t, resp.IsError)
-
-	filePath := filepath.Join(workingDir, "nested", "empty.txt")
-	info, err := os.Stat(filePath)
-	require.NoError(t, err)
-	require.False(t, info.IsDir())
-	require.Zero(t, info.Size())
-}
-
-func TestTouchToolRefusesExistingFile(t *testing.T) {
-	t.Parallel()
-
-	workingDir := t.TempDir()
-	filePath := filepath.Join(workingDir, "existing.txt")
-	require.NoError(t, os.WriteFile(filePath, []byte("content"), 0o644))
-
-	tool := NewTouchTool(nil, &mockPermissionService{}, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
-	ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
-
-	resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "existing.txt"})
-	require.True(t, resp.IsError)
-	require.Contains(t, resp.Content, "File already exists")
-
-	content, err := os.ReadFile(filePath)
-	require.NoError(t, err)
-	require.Equal(t, "content", string(content))
-}
-
-func TestTouchToolStaysInsideWorkingDir(t *testing.T) {
-	t.Parallel()
-
-	workingDir := t.TempDir()
-	perms := &recordingPermissionService{grant: true}
-	tool := NewTouchTool(nil, perms, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
-	ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
-
-	resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "inside.txt"})
-	require.False(t, resp.IsError)
-
-	for _, req := range perms.requests {
-		require.NotContains(t, req.Description, "outside working directory",
-			"inside-workingDir touch should not trigger an outside-workingDir permission prompt")
-	}
-
-	_, err := os.Stat(filepath.Join(workingDir, "inside.txt"))
-	require.NoError(t, err)
-}
-
-func TestTouchToolOutsideWorkingDirRequiresPermission(t *testing.T) {
-	t.Parallel()
-
-	parent := t.TempDir()
-	workingDir := filepath.Join(parent, "wd")
-	require.NoError(t, os.MkdirAll(workingDir, 0o755))
-
-	// Denied: file outside workingDir must not be created.
-	deny := &recordingPermissionService{grant: false}
-	tool := NewTouchTool(nil, deny, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
-	ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
-
-	resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "../escape.txt"})
-	require.True(t, resp.IsError)
-
-	require.Len(t, deny.requests, 1)
-	require.True(t, strings.Contains(deny.requests[0].Description, "outside working directory"),
-		"expected outside-working-directory permission prompt, got %q", deny.requests[0].Description)
-
-	_, err := os.Stat(filepath.Join(parent, "escape.txt"))
-	require.True(t, os.IsNotExist(err), "denied permission should not create the file")
-
-	// Granted: same path now succeeds.
-	grant := &recordingPermissionService{grant: true}
-	tool = NewTouchTool(nil, grant, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
-	resp = runTouchTool(t, tool, ctx, TouchParams{FilePath: "../escape.txt"})
-	require.False(t, resp.IsError)
-	require.GreaterOrEqual(t, len(grant.requests), 1)
-	require.Contains(t, grant.requests[0].Description, "outside working directory")
-
-	_, err = os.Stat(filepath.Join(parent, "escape.txt"))
-	require.NoError(t, err)
-}
-
-func TestWriteToolEmptyContentPointsToTouch(t *testing.T) {
-	t.Parallel()
-
-	tool := NewWriteTool(nil, nil, nil, nil, t.TempDir())
-
-	input, err := json.Marshal(WriteParams{FilePath: "empty.txt"})
-	require.NoError(t, err)
-
-	resp, err := tool.Run(context.Background(), fantasy.ToolCall{
-		ID:    "test-call",
-		Name:  WriteToolName,
-		Input: string(input),
-	})
-	require.NoError(t, err)
-	require.True(t, resp.IsError)
-	require.Equal(t, `content is required. use the "touch" tool to create an empty file`, resp.Content)
-}
-
-func runTouchTool(t *testing.T, tool fantasy.AgentTool, ctx context.Context, params TouchParams) fantasy.ToolResponse {
-	t.Helper()
-
-	input, err := json.Marshal(params)
-	require.NoError(t, err)
-
-	resp, err := tool.Run(ctx, fantasy.ToolCall{
-		ID:    "test-call",
-		Name:  TouchToolName,
-		Input: string(input),
-	})
-	require.NoError(t, err)
-	return resp
-}

internal/agent/tools/write.go 🔗

@@ -58,10 +58,6 @@ func NewWriteTool(
 				return fantasy.NewTextErrorResponse("file_path is required"), nil
 			}
 
-			if params.Content == "" {
-				return fantasy.NewTextErrorResponse(`content is required. use the "touch" tool to create an empty file`), nil
-			}
-
 			sessionID := GetSessionFromContext(ctx)
 			if sessionID == "" {
 				return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")

internal/agent/tools/write.md 🔗

@@ -2,7 +2,7 @@ Create or overwrite a file with given content; auto-creates parent dirs. Cannot
 
 <usage>
 - Provide file path to write
-- Include content to write to file
+- Include content to write to file (may be empty for a new empty file)
 - Tool creates necessary parent directories automatically
 </usage>
 

internal/agent/tools/write_test.go 🔗

@@ -0,0 +1,49 @@
+package tools
+
+import (
+	"context"
+	"encoding/json"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"charm.land/fantasy"
+	"github.com/stretchr/testify/require"
+)
+
+type mockFileTrackerService struct{}
+
+func (m mockFileTrackerService) RecordRead(ctx context.Context, sessionID, path string) {}
+
+func (m mockFileTrackerService) LastReadTime(ctx context.Context, sessionID, path string) time.Time {
+	return time.Now()
+}
+
+func (m mockFileTrackerService) ListReadFiles(ctx context.Context, sessionID string) ([]string, error) {
+	return nil, nil
+}
+
+func TestWriteToolWritesEmptyNewFile(t *testing.T) {
+	t.Parallel()
+
+	workingDir := t.TempDir()
+	ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
+
+	tool := NewWriteTool(nil, &mockPermissionService{}, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
+
+	input, err := json.Marshal(WriteParams{FilePath: "empty.txt", Content: ""})
+	require.NoError(t, err)
+
+	resp, err := tool.Run(ctx, fantasy.ToolCall{
+		ID:    "test-call",
+		Name:  WriteToolName,
+		Input: string(input),
+	})
+	require.NoError(t, err)
+	require.False(t, resp.IsError)
+
+	b, err := os.ReadFile(filepath.Join(workingDir, "empty.txt"))
+	require.NoError(t, err)
+	require.Equal(t, "", string(b))
+}

internal/config/config.go 🔗

@@ -674,7 +674,6 @@ func allToolNames() []string {
 		"ls",
 		"sourcegraph",
 		"todos",
-		"touch",
 		"view",
 		"write",
 		"list_mcp_resources",

internal/config/load_test.go 🔗

@@ -490,7 +490,7 @@ func TestConfig_setupAgentsWithDisabledTools(t *testing.T) {
 	coderAgent, ok := cfg.Agents[AgentCoder]
 	require.True(t, ok)
 
-	assert.Equal(t, []string{"agent", "bash", "crush_info", "crush_logs", "job_output", "job_kill", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "glob", "ls", "sourcegraph", "todos", "touch", "view", "write", "list_mcp_resources", "read_mcp_resource"}, coderAgent.AllowedTools)
+	assert.Equal(t, []string{"agent", "bash", "crush_info", "crush_logs", "job_output", "job_kill", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "glob", "ls", "sourcegraph", "todos", "view", "write", "list_mcp_resources", "read_mcp_resource"}, coderAgent.AllowedTools)
 
 	taskAgent, ok := cfg.Agents[AgentTask]
 	require.True(t, ok)
@@ -513,7 +513,7 @@ func TestConfig_setupAgentsWithEveryReadOnlyToolDisabled(t *testing.T) {
 	cfg.SetupAgents()
 	coderAgent, ok := cfg.Agents[AgentCoder]
 	require.True(t, ok)
-	assert.Equal(t, []string{"agent", "bash", "crush_info", "crush_logs", "job_output", "job_kill", "download", "edit", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "todos", "touch", "write", "list_mcp_resources", "read_mcp_resource"}, coderAgent.AllowedTools)
+	assert.Equal(t, []string{"agent", "bash", "crush_info", "crush_logs", "job_output", "job_kill", "download", "edit", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "todos", "write", "list_mcp_resources", "read_mcp_resource"}, coderAgent.AllowedTools)
 
 	taskAgent, ok := cfg.Agents[AgentTask]
 	require.True(t, ok)

internal/proto/permission.go 🔗

@@ -100,12 +100,6 @@ func unmarshalToolParams(toolName string, raw json.RawMessage) (any, error) {
 			return nil, err
 		}
 		return params, nil
-	case TouchToolName:
-		var params TouchPermissionsParams
-		if err := json.Unmarshal(raw, &params); err != nil {
-			return nil, err
-		}
-		return params, nil
 	case WriteToolName:
 		var params WritePermissionsParams
 		if err := json.Unmarshal(raw, &params); err != nil {

internal/proto/tools.go 🔗

@@ -227,25 +227,6 @@ type ViewResponseMetadata struct {
 	Content  string `json:"content"`
 }
 
-const TouchToolName = "touch"
-
-// TouchParams represents the parameters for the touch tool.
-type TouchParams struct {
-	FilePath string `json:"file_path"`
-}
-
-// TouchPermissionsParams represents the permission parameters for the touch tool.
-type TouchPermissionsParams struct {
-	FilePath   string `json:"file_path"`
-	OldContent string `json:"old_content,omitempty"`
-	NewContent string `json:"new_content,omitempty"`
-}
-
-// TouchResponseMetadata represents the metadata for the touch tool response.
-type TouchResponseMetadata struct {
-	FilePath string `json:"file_path"`
-}
-
 const WriteToolName = "write"
 
 // WriteParams represents the parameters for the write tool.

internal/ui/chat/file.go 🔗

@@ -97,55 +97,6 @@ func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *
 	return joinToolParts(header, body)
 }
 
-// -----------------------------------------------------------------------------
-// Touch Tool
-// -----------------------------------------------------------------------------
-
-// TouchToolMessageItem is a message item that represents a touch tool call.
-type TouchToolMessageItem struct {
-	*baseToolMessageItem
-}
-
-var _ ToolMessageItem = (*TouchToolMessageItem)(nil)
-
-// NewTouchToolMessageItem creates a new [TouchToolMessageItem].
-func NewTouchToolMessageItem(
-	sty *styles.Styles,
-	toolCall message.ToolCall,
-	result *message.ToolResult,
-	canceled bool,
-) ToolMessageItem {
-	return newBaseToolMessageItem(sty, toolCall, result, &TouchToolRenderContext{}, canceled)
-}
-
-// TouchToolRenderContext renders touch tool messages.
-type TouchToolRenderContext struct{}
-
-// RenderTool implements the [ToolRenderer] interface.
-func (t *TouchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
-	cappedWidth := cappedMessageWidth(width)
-	if opts.IsPending() {
-		return pendingTool(sty, "Touch", opts.Anim, opts.Compact)
-	}
-
-	var params tools.TouchParams
-	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
-		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
-	}
-
-	file := fsext.PrettyPath(params.FilePath)
-	header := toolHeader(sty, opts.Status, "Touch", cappedWidth, opts.Compact, file)
-	if opts.Compact {
-		return header
-	}
-
-	if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
-		return joinToolParts(header, earlyState)
-	}
-
-	return header
-}
-
 // -----------------------------------------------------------------------------
 // Write Tool
 // -----------------------------------------------------------------------------

internal/ui/chat/tools.go 🔗

@@ -221,8 +221,6 @@ func NewToolMessageItem(
 		item = NewJobKillToolMessageItem(sty, toolCall, result, canceled)
 	case tools.ViewToolName:
 		item = NewViewToolMessageItem(sty, toolCall, result, canceled)
-	case tools.TouchToolName:
-		item = NewTouchToolMessageItem(sty, toolCall, result, canceled)
 	case tools.WriteToolName:
 		item = NewWriteToolMessageItem(sty, toolCall, result, canceled)
 	case tools.EditToolName:
@@ -1078,11 +1076,6 @@ func (t *baseToolMessageItem) formatParametersForCopy() string {
 			parts = append(parts, fmt.Sprintf("**Edits:** %d", len(params.Edits)))
 			return strings.Join(parts, "\n")
 		}
-	case tools.TouchToolName:
-		var params tools.TouchParams
-		if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
-			return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
-		}
 	case tools.WriteToolName:
 		var params tools.WriteParams
 		if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
@@ -1224,8 +1217,6 @@ func (t *baseToolMessageItem) formatResultForCopy() string {
 		return t.formatEditResultForCopy()
 	case tools.MultiEditToolName:
 		return t.formatMultiEditResultForCopy()
-	case tools.TouchToolName:
-		return t.formatTouchResultForCopy()
 	case tools.WriteToolName:
 		return t.formatWriteResultForCopy()
 	case tools.FetchToolName:
@@ -1399,20 +1390,6 @@ func (t *baseToolMessageItem) formatMultiEditResultForCopy() string {
 	return result.String()
 }
 
-// formatTouchResultForCopy formats touch tool results for clipboard.
-func (t *baseToolMessageItem) formatTouchResultForCopy() string {
-	if t.result == nil {
-		return ""
-	}
-
-	var params tools.TouchParams
-	if json.Unmarshal([]byte(t.toolCall.Input), &params) != nil {
-		return t.result.Content
-	}
-
-	return fmt.Sprintf("File: %s\n```\n```", fsext.PrettyPath(params.FilePath))
-}
-
 // formatWriteResultForCopy formats write tool results for clipboard.
 func (t *baseToolMessageItem) formatWriteResultForCopy() string {
 	if t.result == nil {
@@ -1598,8 +1575,6 @@ func prettifyToolName(name string) string {
 		return "Sourcegraph"
 	case tools.TodosToolName:
 		return "To-Do"
-	case tools.TouchToolName:
-		return "Touch"
 	case tools.ViewToolName:
 		return "View"
 	case tools.WriteToolName:

internal/ui/dialog/permissions.go 🔗

@@ -316,7 +316,7 @@ func (p *Permissions) respond(action PermissionAction) tea.Msg {
 
 func (p *Permissions) hasDiffView() bool {
 	switch p.permission.ToolName {
-	case tools.EditToolName, tools.TouchToolName, tools.WriteToolName, tools.MultiEditToolName:
+	case tools.EditToolName, tools.WriteToolName, tools.MultiEditToolName:
 		return true
 	}
 	return false
@@ -463,13 +463,11 @@ func (p *Permissions) renderHeader(contentWidth int) string {
 			lines = append(lines, p.renderKeyValue("URL", params.URL, contentWidth))
 			lines = append(lines, p.renderKeyValue("File", fsext.PrettyPath(params.FilePath), contentWidth))
 		}
-	case tools.EditToolName, tools.TouchToolName, tools.WriteToolName, tools.MultiEditToolName, tools.ViewToolName:
+	case tools.EditToolName, tools.WriteToolName, tools.MultiEditToolName, tools.ViewToolName:
 		var filePath string
 		switch params := p.permission.Params.(type) {
 		case tools.EditPermissionsParams:
 			filePath = params.FilePath
-		case tools.TouchPermissionsParams:
-			filePath = params.FilePath
 		case tools.WritePermissionsParams:
 			filePath = params.FilePath
 		case tools.MultiEditPermissionsParams:
@@ -529,8 +527,6 @@ func (p *Permissions) renderContent(width int) string {
 		return p.renderBashContent(width)
 	case tools.EditToolName:
 		return p.renderEditContent(width)
-	case tools.TouchToolName:
-		return p.renderTouchContent(width)
 	case tools.WriteToolName:
 		return p.renderWriteContent(width)
 	case tools.MultiEditToolName:
@@ -567,14 +563,6 @@ func (p *Permissions) renderEditContent(contentWidth int) string {
 	return p.renderDiff(params.FilePath, params.OldContent, params.NewContent, contentWidth)
 }
 
-func (p *Permissions) renderTouchContent(contentWidth int) string {
-	params, ok := p.permission.Params.(tools.TouchPermissionsParams)
-	if !ok {
-		return ""
-	}
-	return p.renderDiff(params.FilePath, params.OldContent, params.NewContent, contentWidth)
-}
-
 func (p *Permissions) renderWriteContent(contentWidth int) string {
 	params, ok := p.permission.Params.(tools.WritePermissionsParams)
 	if !ok {