on main help sign in
refactor(tools): remove touch tool; allow empty write content
7da38davorticalbox
internal/agent/coordinator.go−1
@@ -488,7 +488,6 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent, isSubA
488 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Config().Tools.Ls),
488 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Config().Tools.Ls),
489 tools.NewSourcegraphTool(nil),
489 tools.NewSourcegraphTool(nil),
490 tools.NewTodosTool(c.sessions),
490 tools.NewTodosTool(c.sessions),
491− tools.NewTouchTool(c.lspManager, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()),
492 tools.NewViewTool(c.lspManager, c.permissions, c.filetracker, c.skillTracker, c.cfg.WorkingDir(), c.cfg.Config().Options.SkillsPaths...),
491 tools.NewViewTool(c.lspManager, c.permissions, c.filetracker, c.skillTracker, c.cfg.WorkingDir(), c.cfg.Config().Options.SkillsPaths...),
493 tools.NewWriteTool(c.lspManager, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()),
492 tools.NewWriteTool(c.lspManager, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()),
494 )
493 )
internal/agent/tools/touch.godeleted−160
@@ -1,160 +0,0 @@
1−package tools
2−
3−import (
4− "context"
5− _ "embed"
6− "fmt"
7− "os"
8− "path/filepath"
9− "strings"
10−
11− "charm.land/fantasy"
12− "github.com/charmbracelet/crush/internal/filepathext"
13− "github.com/charmbracelet/crush/internal/filetracker"
14− "github.com/charmbracelet/crush/internal/fsext"
15− "github.com/charmbracelet/crush/internal/history"
16− "github.com/charmbracelet/crush/internal/lsp"
17− "github.com/charmbracelet/crush/internal/permission"
18−)
19−
20−//go:embed touch.md
21−var touchDescription []byte
22−
23−type TouchParams struct {
24− FilePath string `json:"file_path" description:"The path to the empty file to create"`
25−}
26−
27−type TouchPermissionsParams struct {
28− FilePath string `json:"file_path"`
29− OldContent string `json:"old_content,omitempty"`
30− NewContent string `json:"new_content,omitempty"`
31−}
32−
33−type TouchResponseMetadata struct {
34− FilePath string `json:"file_path"`
35−}
36−
37−const TouchToolName = "touch"
38−
39−func NewTouchTool(
40− lspManager *lsp.Manager,
41− permissions permission.Service,
42− files history.Service,
43− filetracker filetracker.Service,
44− workingDir string,
45−) fantasy.AgentTool {
46− return fantasy.NewAgentTool(
47− TouchToolName,
48− FirstLineDescription(touchDescription),
49− func(ctx context.Context, params TouchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
50− if params.FilePath == "" {
51− return fantasy.NewTextErrorResponse("file_path is required"), nil
52− }
53−
54− sessionID := GetSessionFromContext(ctx)
55− if sessionID == "" {
56− return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")
57− }
58−
59− filePath := filepathext.SmartJoin(workingDir, params.FilePath)
60−
61− absWorkingDir, err := filepath.Abs(workingDir)
62− if err != nil {
63− return fantasy.ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
64− }
65− absFilePath, err := filepath.Abs(filePath)
66− if err != nil {
67− return fantasy.ToolResponse{}, fmt.Errorf("error resolving file path: %w", err)
68− }
69− relPath, relErr := filepath.Rel(absWorkingDir, absFilePath)
70− isOutsideWorkDir := relErr != nil || strings.HasPrefix(relPath, "..")
71−
72− if isOutsideWorkDir {
73− granted, permReqErr := permissions.Request(ctx,
74− permission.CreatePermissionRequest{
75− SessionID: sessionID,
76− Path: absFilePath,
77− ToolCallID: call.ID,
78− ToolName: TouchToolName,
79− Action: "write",
80− Description: fmt.Sprintf("Create empty file outside working directory: %s", absFilePath),
81− Params: TouchPermissionsParams{
82− FilePath: absFilePath,
83− },
84− },
85− )
86− if permReqErr != nil {
87− return fantasy.ToolResponse{}, permReqErr
88− }
89− if !granted {
90− return NewPermissionDeniedResponse(), nil
91− }
92− }
93−
94− fileInfo, err := os.Stat(absFilePath)
95− if err == nil {
96− if fileInfo.IsDir() {
97− return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", absFilePath)), nil
98− }
99− return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", absFilePath)), nil
100− } else if !os.IsNotExist(err) {
101− return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
102− }
103−
104− dir := filepath.Dir(absFilePath)
105− if err = os.MkdirAll(dir, 0o755); err != nil {
106− return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
107− }
108−
109− p, err := permissions.Request(ctx,
110− permission.CreatePermissionRequest{
111− SessionID: sessionID,
112− Path: fsext.PathOrPrefix(absFilePath, absWorkingDir),
113− ToolCallID: call.ID,
114− ToolName: TouchToolName,
115− Action: "write",
116− Description: fmt.Sprintf("Create empty file %s", absFilePath),
117− Params: TouchPermissionsParams{
118− FilePath: absFilePath,
119− OldContent: "",
120− NewContent: "",
121− },
122− },
123− )
124− if err != nil {
125− return fantasy.ToolResponse{}, err
126− }
127− if !p {
128− return NewPermissionDeniedResponse(), nil
129− }
130−
131− file, err := os.OpenFile(absFilePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644)
132− if err != nil {
133− if os.IsExist(err) {
134− return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", absFilePath)), nil
135− }
136− return fantasy.ToolResponse{}, fmt.Errorf("error creating file: %w", err)
137− }
138− if err = file.Close(); err != nil {
139− return fantasy.ToolResponse{}, fmt.Errorf("error closing file: %w", err)
140− }
141−
142− _, err = files.Create(ctx, sessionID, absFilePath, "")
143− if err != nil {
144− return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
145− }
146−
147− filetracker.RecordRead(ctx, sessionID, absFilePath)
148−
149− notifyLSPs(ctx, lspManager, absFilePath)
150−
151− result := fmt.Sprintf("Empty file successfully created: %s", absFilePath)
152− result = fmt.Sprintf("<result>\n%s\n</result>", result)
153− result += getDiagnostics(absFilePath, lspManager)
154− return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
155− TouchResponseMetadata{
156− FilePath: absFilePath,
157− },
158− ), nil
159− })
160−}
internal/agent/tools/touch.mddeleted−27
@@ -1,27 +0,0 @@
1−Create an empty file; auto-creates parent dirs. Fails if the file already exists.
2−
3−<usage>
4−- Provide file path to create
5−- Tool creates necessary parent directories automatically
6−</usage>
7−
8−<features>
9−- Creates new empty files
10−- Auto-creates parent directories if missing
11−- Refuses to overwrite existing files
12−</features>
13−
14−<limitations>
15−- Cannot write content
16−- Cannot update modification times for existing files
17−- Cannot create directories
18−</limitations>
19−
20−<cross_platform>
21−- Use forward slashes (/) for compatibility
22−</cross_platform>
23−
24−<tips>
25−- Use Write tool when the file should contain content
26−- Use LS tool to verify location when creating new files
27−</tips>
internal/agent/tools/touch_test.godeleted−174
@@ -1,174 +0,0 @@
1−package tools
2−
3−import (
4− "context"
5− "encoding/json"
6− "os"
7− "path/filepath"
8− "strings"
9− "testing"
10− "time"
11−
12− "charm.land/fantasy"
13− "github.com/charmbracelet/crush/internal/permission"
14− "github.com/charmbracelet/crush/internal/pubsub"
15− "github.com/stretchr/testify/require"
16−)
17−
18−// recordingPermissionService captures permission requests and answers them
19−// according to a configurable response.
20−type recordingPermissionService struct {
21− *pubsub.Broker[permission.PermissionRequest]
22− requests []permission.CreatePermissionRequest
23− grant bool
24−}
25−
26−func (m *recordingPermissionService) Request(ctx context.Context, req permission.CreatePermissionRequest) (bool, error) {
27− m.requests = append(m.requests, req)
28− return m.grant, nil
29−}
30−
31−func (m *recordingPermissionService) Grant(req permission.PermissionRequest) {}
32−func (m *recordingPermissionService) Deny(req permission.PermissionRequest) {}
33−func (m *recordingPermissionService) GrantPersistent(req permission.PermissionRequest) {}
34−func (m *recordingPermissionService) AutoApproveSession(sessionID string) {}
35−func (m *recordingPermissionService) SetSkipRequests(skip bool) {}
36−func (m *recordingPermissionService) SkipRequests() bool { return false }
37−func (m *recordingPermissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[permission.PermissionNotification] {
38− return make(<-chan pubsub.Event[permission.PermissionNotification])
39−}
40−
41−type mockFileTrackerService struct{}
42−
43−func (m mockFileTrackerService) RecordRead(ctx context.Context, sessionID, path string) {}
44−
45−func (m mockFileTrackerService) LastReadTime(ctx context.Context, sessionID, path string) time.Time {
46− return time.Now()
47−}
48−
49−func (m mockFileTrackerService) ListReadFiles(ctx context.Context, sessionID string) ([]string, error) {
50− return nil, nil
51−}
52−
53−func TestTouchToolCreatesEmptyFile(t *testing.T) {
54− t.Parallel()
55−
56− workingDir := t.TempDir()
57− tool := NewTouchTool(nil, &mockPermissionService{}, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
58− ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
59−
60− resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "nested/empty.txt"})
61− require.False(t, resp.IsError)
62−
63− filePath := filepath.Join(workingDir, "nested", "empty.txt")
64− info, err := os.Stat(filePath)
65− require.NoError(t, err)
66− require.False(t, info.IsDir())
67− require.Zero(t, info.Size())
68−}
69−
70−func TestTouchToolRefusesExistingFile(t *testing.T) {
71− t.Parallel()
72−
73− workingDir := t.TempDir()
74− filePath := filepath.Join(workingDir, "existing.txt")
75− require.NoError(t, os.WriteFile(filePath, []byte("content"), 0o644))
76−
77− tool := NewTouchTool(nil, &mockPermissionService{}, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
78− ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
79−
80− resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "existing.txt"})
81− require.True(t, resp.IsError)
82− require.Contains(t, resp.Content, "File already exists")
83−
84− content, err := os.ReadFile(filePath)
85− require.NoError(t, err)
86− require.Equal(t, "content", string(content))
87−}
88−
89−func TestTouchToolStaysInsideWorkingDir(t *testing.T) {
90− t.Parallel()
91−
92− workingDir := t.TempDir()
93− perms := &recordingPermissionService{grant: true}
94− tool := NewTouchTool(nil, perms, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
95− ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
96−
97− resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "inside.txt"})
98− require.False(t, resp.IsError)
99−
100− for _, req := range perms.requests {
101− require.NotContains(t, req.Description, "outside working directory",
102− "inside-workingDir touch should not trigger an outside-workingDir permission prompt")
103− }
104−
105− _, err := os.Stat(filepath.Join(workingDir, "inside.txt"))
106− require.NoError(t, err)
107−}
108−
109−func TestTouchToolOutsideWorkingDirRequiresPermission(t *testing.T) {
110− t.Parallel()
111−
112− parent := t.TempDir()
113− workingDir := filepath.Join(parent, "wd")
114− require.NoError(t, os.MkdirAll(workingDir, 0o755))
115−
116− // Denied: file outside workingDir must not be created.
117− deny := &recordingPermissionService{grant: false}
118− tool := NewTouchTool(nil, deny, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
119− ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
120−
121− resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "../escape.txt"})
122− require.True(t, resp.IsError)
123−
124− require.Len(t, deny.requests, 1)
125− require.True(t, strings.Contains(deny.requests[0].Description, "outside working directory"),
126− "expected outside-working-directory permission prompt, got %q", deny.requests[0].Description)
127−
128− _, err := os.Stat(filepath.Join(parent, "escape.txt"))
129− require.True(t, os.IsNotExist(err), "denied permission should not create the file")
130−
131− // Granted: same path now succeeds.
132− grant := &recordingPermissionService{grant: true}
133− tool = NewTouchTool(nil, grant, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
134− resp = runTouchTool(t, tool, ctx, TouchParams{FilePath: "../escape.txt"})
135− require.False(t, resp.IsError)
136− require.GreaterOrEqual(t, len(grant.requests), 1)
137− require.Contains(t, grant.requests[0].Description, "outside working directory")
138−
139− _, err = os.Stat(filepath.Join(parent, "escape.txt"))
140− require.NoError(t, err)
141−}
142−
143−func TestWriteToolEmptyContentPointsToTouch(t *testing.T) {
144− t.Parallel()
145−
146− tool := NewWriteTool(nil, nil, nil, nil, t.TempDir())
147−
148− input, err := json.Marshal(WriteParams{FilePath: "empty.txt"})
149− require.NoError(t, err)
150−
151− resp, err := tool.Run(context.Background(), fantasy.ToolCall{
152− ID: "test-call",
153− Name: WriteToolName,
154− Input: string(input),
155− })
156− require.NoError(t, err)
157− require.True(t, resp.IsError)
158− require.Equal(t, `content is required. use the "touch" tool to create an empty file`, resp.Content)
159−}
160−
161−func runTouchTool(t *testing.T, tool fantasy.AgentTool, ctx context.Context, params TouchParams) fantasy.ToolResponse {
162− t.Helper()
163−
164− input, err := json.Marshal(params)
165− require.NoError(t, err)
166−
167− resp, err := tool.Run(ctx, fantasy.ToolCall{
168− ID: "test-call",
169− Name: TouchToolName,
170− Input: string(input),
171− })
172− require.NoError(t, err)
173− return resp
174−}
internal/agent/tools/write.go−4
@@ -58,10 +58,6 @@ func NewWriteTool(
58 return fantasy.NewTextErrorResponse("file_path is required"), nil
58 return fantasy.NewTextErrorResponse("file_path is required"), nil
59 }
59 }
60
60
61− if params.Content == "" {
62− return fantasy.NewTextErrorResponse(`content is required. use the "touch" tool to create an empty file`), nil
63− }
64−
65 sessionID := GetSessionFromContext(ctx)
61 sessionID := GetSessionFromContext(ctx)
66 if sessionID == "" {
62 if sessionID == "" {
67 return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")
63 return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")
internal/agent/tools/write.md+1−1
@@ -2,7 +2,7 @@ Create or overwrite a file with given content; auto-creates parent dirs. Cannot
2
2
3<usage>
3<usage>
4- Provide file path to write
4- Provide file path to write
5−- Include content to write to file
5+- Include content to write to file (may be empty for a new empty file)
6- Tool creates necessary parent directories automatically
6- Tool creates necessary parent directories automatically
7</usage>
7</usage>
8
8
internal/agent/tools/write_test.gonew file+49
@@ -0,0 +1,49 @@
1+package tools
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "os"
7+ "path/filepath"
8+ "testing"
9+ "time"
10+
11+ "charm.land/fantasy"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+type mockFileTrackerService struct{}
16+
17+func (m mockFileTrackerService) RecordRead(ctx context.Context, sessionID, path string) {}
18+
19+func (m mockFileTrackerService) LastReadTime(ctx context.Context, sessionID, path string) time.Time {
20+ return time.Now()
21+}
22+
23+func (m mockFileTrackerService) ListReadFiles(ctx context.Context, sessionID string) ([]string, error) {
24+ return nil, nil
25+}
26+
27+func TestWriteToolWritesEmptyNewFile(t *testing.T) {
28+ t.Parallel()
29+
30+ workingDir := t.TempDir()
31+ ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
32+
33+ tool := NewWriteTool(nil, &mockPermissionService{}, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
34+
35+ input, err := json.Marshal(WriteParams{FilePath: "empty.txt", Content: ""})
36+ require.NoError(t, err)
37+
38+ resp, err := tool.Run(ctx, fantasy.ToolCall{
39+ ID: "test-call",
40+ Name: WriteToolName,
41+ Input: string(input),
42+ })
43+ require.NoError(t, err)
44+ require.False(t, resp.IsError)
45+
46+ b, err := os.ReadFile(filepath.Join(workingDir, "empty.txt"))
47+ require.NoError(t, err)
48+ require.Equal(t, "", string(b))
49+}
internal/config/config.go−1
@@ -674,7 +674,6 @@ func allToolNames() []string {
674 "ls",
674 "ls",
675 "sourcegraph",
675 "sourcegraph",
676 "todos",
676 "todos",
677− "touch",
678 "view",
677 "view",
679 "write",
678 "write",
680 "list_mcp_resources",
679 "list_mcp_resources",
internal/config/load_test.go+2−2
@@ -490,7 +490,7 @@ func TestConfig_setupAgentsWithDisabledTools(t *testing.T) {
490 coderAgent, ok := cfg.Agents[AgentCoder]
490 coderAgent, ok := cfg.Agents[AgentCoder]
491 require.True(t, ok)
491 require.True(t, ok)
492
492
493− 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)
493+ 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)
494
494
495 taskAgent, ok := cfg.Agents[AgentTask]
495 taskAgent, ok := cfg.Agents[AgentTask]
496 require.True(t, ok)
496 require.True(t, ok)
@@ -513,7 +513,7 @@ func TestConfig_setupAgentsWithEveryReadOnlyToolDisabled(t *testing.T) {
513 cfg.SetupAgents()
513 cfg.SetupAgents()
514 coderAgent, ok := cfg.Agents[AgentCoder]
514 coderAgent, ok := cfg.Agents[AgentCoder]
515 require.True(t, ok)
515 require.True(t, ok)
516− 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)
516+ 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)
517
517
518 taskAgent, ok := cfg.Agents[AgentTask]
518 taskAgent, ok := cfg.Agents[AgentTask]
519 require.True(t, ok)
519 require.True(t, ok)
internal/proto/permission.go−6
@@ -100,12 +100,6 @@ func unmarshalToolParams(toolName string, raw json.RawMessage) (any, error) {
100 return nil, err
100 return nil, err
101 }
101 }
102 return params, nil
102 return params, nil
103− case TouchToolName:
104− var params TouchPermissionsParams
105− if err := json.Unmarshal(raw, &params); err != nil {
106− return nil, err
107− }
108− return params, nil
109 case WriteToolName:
103 case WriteToolName:
110 var params WritePermissionsParams
104 var params WritePermissionsParams
111 if err := json.Unmarshal(raw, &params); err != nil {
105 if err := json.Unmarshal(raw, &params); err != nil {
internal/proto/tools.go−19
@@ -227,25 +227,6 @@ type ViewResponseMetadata struct {
227 Content string `json:"content"`
227 Content string `json:"content"`
228}
228}
229
229
230−const TouchToolName = "touch"
231−
232−// TouchParams represents the parameters for the touch tool.
233−type TouchParams struct {
234− FilePath string `json:"file_path"`
235−}
236−
237−// TouchPermissionsParams represents the permission parameters for the touch tool.
238−type TouchPermissionsParams struct {
239− FilePath string `json:"file_path"`
240− OldContent string `json:"old_content,omitempty"`
241− NewContent string `json:"new_content,omitempty"`
242−}
243−
244−// TouchResponseMetadata represents the metadata for the touch tool response.
245−type TouchResponseMetadata struct {
246− FilePath string `json:"file_path"`
247−}
248−
249const WriteToolName = "write"
230const WriteToolName = "write"
250
231
251// WriteParams represents the parameters for the write tool.
232// WriteParams represents the parameters for the write tool.
internal/ui/chat/file.go−49
@@ -97,55 +97,6 @@ func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *
97 return joinToolParts(header, body)
97 return joinToolParts(header, body)
98}
98}
99
99
100−// -----------------------------------------------------------------------------
101−// Touch Tool
102−// -----------------------------------------------------------------------------
103−
104−// TouchToolMessageItem is a message item that represents a touch tool call.
105−type TouchToolMessageItem struct {
106− *baseToolMessageItem
107−}
108−
109−var _ ToolMessageItem = (*TouchToolMessageItem)(nil)
110−
111−// NewTouchToolMessageItem creates a new [TouchToolMessageItem].
112−func NewTouchToolMessageItem(
113− sty *styles.Styles,
114− toolCall message.ToolCall,
115− result *message.ToolResult,
116− canceled bool,
117−) ToolMessageItem {
118− return newBaseToolMessageItem(sty, toolCall, result, &TouchToolRenderContext{}, canceled)
119−}
120−
121−// TouchToolRenderContext renders touch tool messages.
122−type TouchToolRenderContext struct{}
123−
124−// RenderTool implements the [ToolRenderer] interface.
125−func (t *TouchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
126− cappedWidth := cappedMessageWidth(width)
127− if opts.IsPending() {
128− return pendingTool(sty, "Touch", opts.Anim, opts.Compact)
129− }
130−
131− var params tools.TouchParams
132− if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
133− return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
134− }
135−
136− file := fsext.PrettyPath(params.FilePath)
137− header := toolHeader(sty, opts.Status, "Touch", cappedWidth, opts.Compact, file)
138− if opts.Compact {
139− return header
140− }
141−
142− if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
143− return joinToolParts(header, earlyState)
144− }
145−
146− return header
147−}
148−
149// -----------------------------------------------------------------------------
100// -----------------------------------------------------------------------------
150// Write Tool
101// Write Tool
151// -----------------------------------------------------------------------------
102// -----------------------------------------------------------------------------
internal/ui/chat/tools.go−25
@@ -221,8 +221,6 @@ func NewToolMessageItem(
221 item = NewJobKillToolMessageItem(sty, toolCall, result, canceled)
221 item = NewJobKillToolMessageItem(sty, toolCall, result, canceled)
222 case tools.ViewToolName:
222 case tools.ViewToolName:
223 item = NewViewToolMessageItem(sty, toolCall, result, canceled)
223 item = NewViewToolMessageItem(sty, toolCall, result, canceled)
224− case tools.TouchToolName:
225− item = NewTouchToolMessageItem(sty, toolCall, result, canceled)
226 case tools.WriteToolName:
224 case tools.WriteToolName:
227 item = NewWriteToolMessageItem(sty, toolCall, result, canceled)
225 item = NewWriteToolMessageItem(sty, toolCall, result, canceled)
228 case tools.EditToolName:
226 case tools.EditToolName:
@@ -1078,11 +1076,6 @@ func (t *baseToolMessageItem) formatParametersForCopy() string {
1078 parts = append(parts, fmt.Sprintf("**Edits:** %d", len(params.Edits)))
1076 parts = append(parts, fmt.Sprintf("**Edits:** %d", len(params.Edits)))
1079 return strings.Join(parts, "\n")
1077 return strings.Join(parts, "\n")
1080 }
1078 }
1081− case tools.TouchToolName:
1082− var params tools.TouchParams
1083− if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
1084− return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
1085− }
1086 case tools.WriteToolName:
1079 case tools.WriteToolName:
1087 var params tools.WriteParams
1080 var params tools.WriteParams
1088 if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
1081 if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
@@ -1224,8 +1217,6 @@ func (t *baseToolMessageItem) formatResultForCopy() string {
1224 return t.formatEditResultForCopy()
1217 return t.formatEditResultForCopy()
1225 case tools.MultiEditToolName:
1218 case tools.MultiEditToolName:
1226 return t.formatMultiEditResultForCopy()
1219 return t.formatMultiEditResultForCopy()
1227− case tools.TouchToolName:
1228− return t.formatTouchResultForCopy()
1229 case tools.WriteToolName:
1220 case tools.WriteToolName:
1230 return t.formatWriteResultForCopy()
1221 return t.formatWriteResultForCopy()
1231 case tools.FetchToolName:
1222 case tools.FetchToolName:
@@ -1399,20 +1390,6 @@ func (t *baseToolMessageItem) formatMultiEditResultForCopy() string {
1399 return result.String()
1390 return result.String()
1400}
1391}
1401
1392
1402−// formatTouchResultForCopy formats touch tool results for clipboard.
1403−func (t *baseToolMessageItem) formatTouchResultForCopy() string {
1404− if t.result == nil {
1405− return ""
1406− }
1407−
1408− var params tools.TouchParams
1409− if json.Unmarshal([]byte(t.toolCall.Input), &params) != nil {
1410− return t.result.Content
1411− }
1412−
1413− return fmt.Sprintf("File: %s\n```\n```", fsext.PrettyPath(params.FilePath))
1414−}
1415−
1416// formatWriteResultForCopy formats write tool results for clipboard.
1393// formatWriteResultForCopy formats write tool results for clipboard.
1417func (t *baseToolMessageItem) formatWriteResultForCopy() string {
1394func (t *baseToolMessageItem) formatWriteResultForCopy() string {
1418 if t.result == nil {
1395 if t.result == nil {
@@ -1598,8 +1575,6 @@ func prettifyToolName(name string) string {
1598 return "Sourcegraph"
1575 return "Sourcegraph"
1599 case tools.TodosToolName:
1576 case tools.TodosToolName:
1600 return "To-Do"
1577 return "To-Do"
1601− case tools.TouchToolName:
1602− return "Touch"
1603 case tools.ViewToolName:
1578 case tools.ViewToolName:
1604 return "View"
1579 return "View"
1605 case tools.WriteToolName:
1580 case tools.WriteToolName:
internal/ui/dialog/permissions.go+2−14
@@ -316,7 +316,7 @@ func (p *Permissions) respond(action PermissionAction) tea.Msg {
316
316
317func (p *Permissions) hasDiffView() bool {
317func (p *Permissions) hasDiffView() bool {
318 switch p.permission.ToolName {
318 switch p.permission.ToolName {
319− case tools.EditToolName, tools.TouchToolName, tools.WriteToolName, tools.MultiEditToolName:
319+ case tools.EditToolName, tools.WriteToolName, tools.MultiEditToolName:
320 return true
320 return true
321 }
321 }
322 return false
322 return false
@@ -463,13 +463,11 @@ func (p *Permissions) renderHeader(contentWidth int) string {
463 lines = append(lines, p.renderKeyValue("URL", params.URL, contentWidth))
463 lines = append(lines, p.renderKeyValue("URL", params.URL, contentWidth))
464 lines = append(lines, p.renderKeyValue("File", fsext.PrettyPath(params.FilePath), contentWidth))
464 lines = append(lines, p.renderKeyValue("File", fsext.PrettyPath(params.FilePath), contentWidth))
465 }
465 }
466− case tools.EditToolName, tools.TouchToolName, tools.WriteToolName, tools.MultiEditToolName, tools.ViewToolName:
466+ case tools.EditToolName, tools.WriteToolName, tools.MultiEditToolName, tools.ViewToolName:
467 var filePath string
467 var filePath string
468 switch params := p.permission.Params.(type) {
468 switch params := p.permission.Params.(type) {
469 case tools.EditPermissionsParams:
469 case tools.EditPermissionsParams:
470 filePath = params.FilePath
470 filePath = params.FilePath
471− case tools.TouchPermissionsParams:
472− filePath = params.FilePath
473 case tools.WritePermissionsParams:
471 case tools.WritePermissionsParams:
474 filePath = params.FilePath
472 filePath = params.FilePath
475 case tools.MultiEditPermissionsParams:
473 case tools.MultiEditPermissionsParams:
@@ -529,8 +527,6 @@ func (p *Permissions) renderContent(width int) string {
529 return p.renderBashContent(width)
527 return p.renderBashContent(width)
530 case tools.EditToolName:
528 case tools.EditToolName:
531 return p.renderEditContent(width)
529 return p.renderEditContent(width)
532− case tools.TouchToolName:
533− return p.renderTouchContent(width)
534 case tools.WriteToolName:
530 case tools.WriteToolName:
535 return p.renderWriteContent(width)
531 return p.renderWriteContent(width)
536 case tools.MultiEditToolName:
532 case tools.MultiEditToolName:
@@ -567,14 +563,6 @@ func (p *Permissions) renderEditContent(contentWidth int) string {
567 return p.renderDiff(params.FilePath, params.OldContent, params.NewContent, contentWidth)
563 return p.renderDiff(params.FilePath, params.OldContent, params.NewContent, contentWidth)
568}
564}
569
565
570−func (p *Permissions) renderTouchContent(contentWidth int) string {
571− params, ok := p.permission.Params.(tools.TouchPermissionsParams)
572− if !ok {
573− return ""
574− }
575− return p.renderDiff(params.FilePath, params.OldContent, params.NewContent, contentWidth)
576−}
577−
578func (p *Permissions) renderWriteContent(contentWidth int) string {
566func (p *Permissions) renderWriteContent(contentWidth int) string {
579 params, ok := p.permission.Params.(tools.WritePermissionsParams)
567 params, ok := p.permission.Params.(tools.WritePermissionsParams)
580 if !ok {
568 if !ok {