on main help sign in
feat: add touch tool for empty files
010ca2fvorticalbox
internal/agent/coordinator.go+1
@@ -488,6 +488,7 @@ 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()),
491 tools.NewViewTool(c.lspManager, c.permissions, c.filetracker, c.skillTracker, c.cfg.WorkingDir(), c.cfg.Config().Options.SkillsPaths...),
492 tools.NewViewTool(c.lspManager, c.permissions, c.filetracker, c.skillTracker, c.cfg.WorkingDir(), c.cfg.Config().Options.SkillsPaths...),
492 tools.NewWriteTool(c.lspManager, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()),
493 tools.NewWriteTool(c.lspManager, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()),
493 )
494 )
internal/agent/tools/touch.gonew file+126
@@ -0,0 +1,126 @@
1+package tools
2+
3+import (
4+ "context"
5+ _ "embed"
6+ "fmt"
7+ "os"
8+ "path/filepath"
9+
10+ "charm.land/fantasy"
11+ "github.com/charmbracelet/crush/internal/filepathext"
12+ "github.com/charmbracelet/crush/internal/filetracker"
13+ "github.com/charmbracelet/crush/internal/fsext"
14+ "github.com/charmbracelet/crush/internal/history"
15+ "github.com/charmbracelet/crush/internal/lsp"
16+ "github.com/charmbracelet/crush/internal/permission"
17+)
18+
19+//go:embed touch.md
20+var touchDescription []byte
21+
22+type TouchParams struct {
23+ FilePath string `json:"file_path" description:"The path to the empty file to create"`
24+}
25+
26+type TouchPermissionsParams struct {
27+ FilePath string `json:"file_path"`
28+ OldContent string `json:"old_content,omitempty"`
29+ NewContent string `json:"new_content,omitempty"`
30+}
31+
32+type TouchResponseMetadata struct {
33+ FilePath string `json:"file_path"`
34+}
35+
36+const TouchToolName = "touch"
37+
38+func NewTouchTool(
39+ lspManager *lsp.Manager,
40+ permissions permission.Service,
41+ files history.Service,
42+ filetracker filetracker.Service,
43+ workingDir string,
44+) fantasy.AgentTool {
45+ return fantasy.NewAgentTool(
46+ TouchToolName,
47+ FirstLineDescription(touchDescription),
48+ func(ctx context.Context, params TouchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
49+ if params.FilePath == "" {
50+ return fantasy.NewTextErrorResponse("file_path is required"), nil
51+ }
52+
53+ sessionID := GetSessionFromContext(ctx)
54+ if sessionID == "" {
55+ return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")
56+ }
57+
58+ filePath := filepathext.SmartJoin(workingDir, params.FilePath)
59+
60+ fileInfo, err := os.Stat(filePath)
61+ if err == nil {
62+ if fileInfo.IsDir() {
63+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
64+ }
65+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", filePath)), nil
66+ } else if !os.IsNotExist(err) {
67+ return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
68+ }
69+
70+ dir := filepath.Dir(filePath)
71+ if err = os.MkdirAll(dir, 0o755); err != nil {
72+ return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
73+ }
74+
75+ p, err := permissions.Request(ctx,
76+ permission.CreatePermissionRequest{
77+ SessionID: sessionID,
78+ Path: fsext.PathOrPrefix(filePath, workingDir),
79+ ToolCallID: call.ID,
80+ ToolName: TouchToolName,
81+ Action: "write",
82+ Description: fmt.Sprintf("Create empty file %s", filePath),
83+ Params: TouchPermissionsParams{
84+ FilePath: filePath,
85+ OldContent: "",
86+ NewContent: "",
87+ },
88+ },
89+ )
90+ if err != nil {
91+ return fantasy.ToolResponse{}, err
92+ }
93+ if !p {
94+ return NewPermissionDeniedResponse(), nil
95+ }
96+
97+ file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644)
98+ if err != nil {
99+ if os.IsExist(err) {
100+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", filePath)), nil
101+ }
102+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file: %w", err)
103+ }
104+ if err = file.Close(); err != nil {
105+ return fantasy.ToolResponse{}, fmt.Errorf("error closing file: %w", err)
106+ }
107+
108+ _, err = files.Create(ctx, sessionID, filePath, "")
109+ if err != nil {
110+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
111+ }
112+
113+ filetracker.RecordRead(ctx, sessionID, filePath)
114+
115+ notifyLSPs(ctx, lspManager, filePath)
116+
117+ result := fmt.Sprintf("Empty file successfully created: %s", filePath)
118+ result = fmt.Sprintf("<result>\n%s\n</result>", result)
119+ result += getDiagnostics(filePath, lspManager)
120+ return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
121+ TouchResponseMetadata{
122+ FilePath: filePath,
123+ },
124+ ), nil
125+ })
126+}
internal/agent/tools/touch.mdnew file+27
@@ -0,0 +1,27 @@
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.gonew file+94
@@ -0,0 +1,94 @@
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 TestTouchToolCreatesEmptyFile(t *testing.T) {
28+ t.Parallel()
29+
30+ workingDir := t.TempDir()
31+ tool := NewTouchTool(nil, &mockPermissionService{}, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
32+ ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
33+
34+ resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "nested/empty.txt"})
35+ require.False(t, resp.IsError)
36+
37+ filePath := filepath.Join(workingDir, "nested", "empty.txt")
38+ info, err := os.Stat(filePath)
39+ require.NoError(t, err)
40+ require.False(t, info.IsDir())
41+ require.Zero(t, info.Size())
42+}
43+
44+func TestTouchToolRefusesExistingFile(t *testing.T) {
45+ t.Parallel()
46+
47+ workingDir := t.TempDir()
48+ filePath := filepath.Join(workingDir, "existing.txt")
49+ require.NoError(t, os.WriteFile(filePath, []byte("content"), 0o644))
50+
51+ tool := NewTouchTool(nil, &mockPermissionService{}, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
52+ ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
53+
54+ resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "existing.txt"})
55+ require.True(t, resp.IsError)
56+ require.Contains(t, resp.Content, "File already exists")
57+
58+ content, err := os.ReadFile(filePath)
59+ require.NoError(t, err)
60+ require.Equal(t, "content", string(content))
61+}
62+
63+func TestWriteToolEmptyContentPointsToTouch(t *testing.T) {
64+ t.Parallel()
65+
66+ tool := NewWriteTool(nil, nil, nil, nil, t.TempDir())
67+
68+ input, err := json.Marshal(WriteParams{FilePath: "empty.txt"})
69+ require.NoError(t, err)
70+
71+ resp, err := tool.Run(context.Background(), fantasy.ToolCall{
72+ ID: "test-call",
73+ Name: WriteToolName,
74+ Input: string(input),
75+ })
76+ require.NoError(t, err)
77+ require.True(t, resp.IsError)
78+ require.Equal(t, `content is required. use the "touch" tool to create an empty file`, resp.Content)
79+}
80+
81+func runTouchTool(t *testing.T, tool fantasy.AgentTool, ctx context.Context, params TouchParams) fantasy.ToolResponse {
82+ t.Helper()
83+
84+ input, err := json.Marshal(params)
85+ require.NoError(t, err)
86+
87+ resp, err := tool.Run(ctx, fantasy.ToolCall{
88+ ID: "test-call",
89+ Name: TouchToolName,
90+ Input: string(input),
91+ })
92+ require.NoError(t, err)
93+ return resp
94+}
internal/agent/tools/write.go+1−1
@@ -59,7 +59,7 @@ func NewWriteTool(
59 }
59 }
60
60
61 if params.Content == "" {
61 if params.Content == "" {
62− return fantasy.NewTextErrorResponse("content is required"), nil
62+ return fantasy.NewTextErrorResponse(`content is required. use the "touch" tool to create an empty file`), nil
63 }
63 }
64
64
65 sessionID := GetSessionFromContext(ctx)
65 sessionID := GetSessionFromContext(ctx)
internal/config/config.go+1
@@ -674,6 +674,7 @@ func allToolNames() []string {
674 "ls",
674 "ls",
675 "sourcegraph",
675 "sourcegraph",
676 "todos",
676 "todos",
677+ "touch",
677 "view",
678 "view",
678 "write",
679 "write",
679 "list_mcp_resources",
680 "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", "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", "touch", "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", "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", "touch", "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,6 +100,12 @@ 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
103 case WriteToolName:
109 case WriteToolName:
104 var params WritePermissionsParams
110 var params WritePermissionsParams
105 if err := json.Unmarshal(raw, &params); err != nil {
111 if err := json.Unmarshal(raw, &params); err != nil {
internal/proto/tools.go+19
@@ -227,6 +227,25 @@ 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+
230const WriteToolName = "write"
249const WriteToolName = "write"
231
250
232// WriteParams represents the parameters for the write tool.
251// WriteParams represents the parameters for the write tool.
internal/ui/chat/file.go+49
@@ -97,6 +97,55 @@ 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+
100// -----------------------------------------------------------------------------
149// -----------------------------------------------------------------------------
101// Write Tool
150// Write Tool
102// -----------------------------------------------------------------------------
151// -----------------------------------------------------------------------------
internal/ui/chat/tools.go+25
@@ -221,6 +221,8 @@ 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)
224 case tools.WriteToolName:
226 case tools.WriteToolName:
225 item = NewWriteToolMessageItem(sty, toolCall, result, canceled)
227 item = NewWriteToolMessageItem(sty, toolCall, result, canceled)
226 case tools.EditToolName:
228 case tools.EditToolName:
@@ -1076,6 +1078,11 @@ func (t *baseToolMessageItem) formatParametersForCopy() string {
1076 parts = append(parts, fmt.Sprintf("**Edits:** %d", len(params.Edits)))
1078 parts = append(parts, fmt.Sprintf("**Edits:** %d", len(params.Edits)))
1077 return strings.Join(parts, "\n")
1079 return strings.Join(parts, "\n")
1078 }
1080 }
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+ }
1079 case tools.WriteToolName:
1086 case tools.WriteToolName:
1080 var params tools.WriteParams
1087 var params tools.WriteParams
1081 if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
1088 if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
@@ -1217,6 +1224,8 @@ func (t *baseToolMessageItem) formatResultForCopy() string {
1217 return t.formatEditResultForCopy()
1224 return t.formatEditResultForCopy()
1218 case tools.MultiEditToolName:
1225 case tools.MultiEditToolName:
1219 return t.formatMultiEditResultForCopy()
1226 return t.formatMultiEditResultForCopy()
1227+ case tools.TouchToolName:
1228+ return t.formatTouchResultForCopy()
1220 case tools.WriteToolName:
1229 case tools.WriteToolName:
1221 return t.formatWriteResultForCopy()
1230 return t.formatWriteResultForCopy()
1222 case tools.FetchToolName:
1231 case tools.FetchToolName:
@@ -1390,6 +1399,20 @@ func (t *baseToolMessageItem) formatMultiEditResultForCopy() string {
1390 return result.String()
1399 return result.String()
1391}
1400}
1392
1401
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+
1393// formatWriteResultForCopy formats write tool results for clipboard.
1416// formatWriteResultForCopy formats write tool results for clipboard.
1394func (t *baseToolMessageItem) formatWriteResultForCopy() string {
1417func (t *baseToolMessageItem) formatWriteResultForCopy() string {
1395 if t.result == nil {
1418 if t.result == nil {
@@ -1575,6 +1598,8 @@ func prettifyToolName(name string) string {
1575 return "Sourcegraph"
1598 return "Sourcegraph"
1576 case tools.TodosToolName:
1599 case tools.TodosToolName:
1577 return "To-Do"
1600 return "To-Do"
1601+ case tools.TouchToolName:
1602+ return "Touch"
1578 case tools.ViewToolName:
1603 case tools.ViewToolName:
1579 return "View"
1604 return "View"
1580 case tools.WriteToolName:
1605 case tools.WriteToolName:
internal/ui/dialog/permissions.go+14−2
@@ -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.WriteToolName, tools.MultiEditToolName:
319+ case tools.EditToolName, tools.TouchToolName, tools.WriteToolName, tools.MultiEditToolName:
320 return true
320 return true
321 }
321 }
322 return false
322 return false
@@ -463,11 +463,13 @@ 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.WriteToolName, tools.MultiEditToolName, tools.ViewToolName:
466+ case tools.EditToolName, tools.TouchToolName, 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
471 case tools.WritePermissionsParams:
473 case tools.WritePermissionsParams:
472 filePath = params.FilePath
474 filePath = params.FilePath
473 case tools.MultiEditPermissionsParams:
475 case tools.MultiEditPermissionsParams:
@@ -527,6 +529,8 @@ func (p *Permissions) renderContent(width int) string {
527 return p.renderBashContent(width)
529 return p.renderBashContent(width)
528 case tools.EditToolName:
530 case tools.EditToolName:
529 return p.renderEditContent(width)
531 return p.renderEditContent(width)
532+ case tools.TouchToolName:
533+ return p.renderTouchContent(width)
530 case tools.WriteToolName:
534 case tools.WriteToolName:
531 return p.renderWriteContent(width)
535 return p.renderWriteContent(width)
532 case tools.MultiEditToolName:
536 case tools.MultiEditToolName:
@@ -563,6 +567,14 @@ func (p *Permissions) renderEditContent(contentWidth int) string {
563 return p.renderDiff(params.FilePath, params.OldContent, params.NewContent, contentWidth)
567 return p.renderDiff(params.FilePath, params.OldContent, params.NewContent, contentWidth)
564}
568}
565
569
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+
566func (p *Permissions) renderWriteContent(contentWidth int) string {
578func (p *Permissions) renderWriteContent(contentWidth int) string {
567 params, ok := p.permission.Params.(tools.WritePermissionsParams)
579 params, ok := p.permission.Params.(tools.WritePermissionsParams)
568 if !ok {
580 if !ok {