on main help sign in
Merge pull request #328 from charmbracelet/cwd-permissions
d931f57Kujtim Hoxha
internal/fsext/expand.gonew file+29
@@ -0,0 +1,29 @@
1+package fsext
2+
3+import (
4+ "os"
5+ "strings"
6+
7+ "mvdan.cc/sh/v3/expand"
8+ "mvdan.cc/sh/v3/syntax"
9+)
10+
11+// Expand is a wrapper around [expand.Literal]. It will escape the input
12+// string, expand any shell symbols (such as '~') and resolve any environment
13+// variables.
14+func Expand(s string) (string, error) {
15+ if s == "" {
16+ return "", nil
17+ }
18+ p := syntax.NewParser()
19+ word, err := p.Document(strings.NewReader(s))
20+ if err != nil {
21+ return "", err
22+ }
23+ cfg := &expand.Config{
24+ Env: expand.FuncEnviron(os.Getenv),
25+ ReadDir2: os.ReadDir,
26+ GlobStar: true,
27+ }
28+ return expand.Literal(cfg, word)
29+}
internal/fsext/ls.go+1−1
@@ -156,7 +156,7 @@ func ListDirectory(initialPath string, ignorePatterns []string, limit int) ([]st
156
156
157 return nil
157 return nil
158 })
158 })
159− if err != nil {
159+ if err != nil && len(results) == 0 {
160 return nil, truncated, err
160 return nil, truncated, err
161 }
161 }
162
162
internal/llm/agent/agent.go+2−2
@@ -181,9 +181,9 @@ func NewAgent(
181 tools.NewFetchTool(permissions, cwd),
181 tools.NewFetchTool(permissions, cwd),
182 tools.NewGlobTool(cwd),
182 tools.NewGlobTool(cwd),
183 tools.NewGrepTool(cwd),
183 tools.NewGrepTool(cwd),
184− tools.NewLsTool(cwd),
184+ tools.NewLsTool(permissions, cwd),
185 tools.NewSourcegraphTool(),
185 tools.NewSourcegraphTool(),
186− tools.NewViewTool(lspClients, cwd),
186+ tools.NewViewTool(lspClients, permissions, cwd),
187 tools.NewWriteTool(lspClients, permissions, history, cwd),
187 tools.NewWriteTool(lspClients, permissions, history, cwd),
188 }
188 }
189
189
internal/llm/prompt/coder.go+2−6
@@ -1,7 +1,6 @@
1package prompt
1package prompt
2
2
3import (
3import (
4− "context"
5 _ "embed"
4 _ "embed"
6 "fmt"
5 "fmt"
7 "log/slog"
6 "log/slog"
@@ -38,10 +37,7 @@ func getEnvironmentInfo() string {
38 isGit := isGitRepo(cwd)
37 isGit := isGitRepo(cwd)
39 platform := runtime.GOOS
38 platform := runtime.GOOS
40 date := time.Now().Format("1/2/2006")
39 date := time.Now().Format("1/2/2006")
41− ls := tools.NewLsTool(cwd)
40+ output, _ := tools.ListDirectoryTree(cwd, nil)
42− r, _ := ls.Run(context.Background(), tools.ToolCall{
43− Input: `{"path":"."}`,
44− })
45 return fmt.Sprintf(`Here is useful information about the environment you are running in:
41 return fmt.Sprintf(`Here is useful information about the environment you are running in:
46<env>
42<env>
47Working directory: %s
43Working directory: %s
@@ -52,7 +48,7 @@ Today's date: %s
52<project>
48<project>
53%s
49%s
54</project>
50</project>
55− `, cwd, boolToYesNo(isGit), platform, date, r.Content)
51+ `, cwd, boolToYesNo(isGit), platform, date, output)
56}
52}
57
53
58func isGitRepo(dir string) bool {
54func isGitRepo(dir string) bool {
internal/llm/tools/edit.go+3−18
@@ -215,15 +215,10 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string,
215 content,
215 content,
216 strings.TrimPrefix(filePath, e.workingDir),
216 strings.TrimPrefix(filePath, e.workingDir),
217 )
217 )
218− rootDir := e.workingDir
219− permissionPath := filepath.Dir(filePath)
220− if strings.HasPrefix(filePath, rootDir) {
221− permissionPath = rootDir
222− }
223 p := e.permissions.Request(
218 p := e.permissions.Request(
224 permission.CreatePermissionRequest{
219 permission.CreatePermissionRequest{
225 SessionID: sessionID,
220 SessionID: sessionID,
226− Path: permissionPath,
221+ Path: filePath,
227 ToolCallID: call.ID,
222 ToolCallID: call.ID,
228 ToolName: EditToolName,
223 ToolName: EditToolName,
229 Action: "write",
224 Action: "write",
@@ -341,15 +336,10 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
341 strings.TrimPrefix(filePath, e.workingDir),
336 strings.TrimPrefix(filePath, e.workingDir),
342 )
337 )
343
338
344− rootDir := e.workingDir
345− permissionPath := filepath.Dir(filePath)
346− if strings.HasPrefix(filePath, rootDir) {
347− permissionPath = rootDir
348− }
349 p := e.permissions.Request(
339 p := e.permissions.Request(
350 permission.CreatePermissionRequest{
340 permission.CreatePermissionRequest{
351 SessionID: sessionID,
341 SessionID: sessionID,
352− Path: permissionPath,
342+ Path: filePath,
353 ToolCallID: call.ID,
343 ToolCallID: call.ID,
354 ToolName: EditToolName,
344 ToolName: EditToolName,
355 Action: "write",
345 Action: "write",
@@ -476,15 +466,10 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
476 newContent,
466 newContent,
477 strings.TrimPrefix(filePath, e.workingDir),
467 strings.TrimPrefix(filePath, e.workingDir),
478 )
468 )
479− rootDir := e.workingDir
480− permissionPath := filepath.Dir(filePath)
481− if strings.HasPrefix(filePath, rootDir) {
482− permissionPath = rootDir
483− }
484 p := e.permissions.Request(
469 p := e.permissions.Request(
485 permission.CreatePermissionRequest{
470 permission.CreatePermissionRequest{
486 SessionID: sessionID,
471 SessionID: sessionID,
487− Path: permissionPath,
472+ Path: filePath,
488 ToolCallID: call.ID,
473 ToolCallID: call.ID,
489 ToolName: EditToolName,
474 ToolName: EditToolName,
490 Action: "write",
475 Action: "write",
internal/llm/tools/ls.go+76−11
@@ -9,6 +9,7 @@ import (
9 "strings"
9 "strings"
10
10
11 "github.com/charmbracelet/crush/internal/fsext"
11 "github.com/charmbracelet/crush/internal/fsext"
12+ "github.com/charmbracelet/crush/internal/permission"
12)
13)
13
14
14type LSParams struct {
15type LSParams struct {
@@ -16,6 +17,11 @@ type LSParams struct {
16 Ignore []string `json:"ignore"`
17 Ignore []string `json:"ignore"`
17}
18}
18
19
20+type LSPermissionsParams struct {
21+ Path string `json:"path"`
22+ Ignore []string `json:"ignore"`
23+}
24+
19type TreeNode struct {
25type TreeNode struct {
20 Name string `json:"name"`
26 Name string `json:"name"`
21 Path string `json:"path"`
27 Path string `json:"path"`
@@ -29,7 +35,8 @@ type LSResponseMetadata struct {
29}
35}
30
36
31type lsTool struct {
37type lsTool struct {
32− workingDir string
38+ workingDir string
39+ permissions permission.Service
33}
40}
34
41
35const (
42const (
@@ -71,9 +78,10 @@ TIPS:
71- Combine with other tools for more effective exploration`
78- Combine with other tools for more effective exploration`
72)
79)
73
80
74−func NewLsTool(workingDir string) BaseTool {
81+func NewLsTool(permissions permission.Service, workingDir string) BaseTool {
75 return &lsTool{
82 return &lsTool{
76− workingDir: workingDir,
83+ workingDir: workingDir,
84+ permissions: permissions,
77 }
85 }
78}
86}
79
87
@@ -113,24 +121,61 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
113 searchPath = l.workingDir
121 searchPath = l.workingDir
114 }
122 }
115
123
124+ var err error
125+ searchPath, err = fsext.Expand(searchPath)
126+ if err != nil {
127+ return ToolResponse{}, fmt.Errorf("error expanding path: %w", err)
128+ }
129+
116 if !filepath.IsAbs(searchPath) {
130 if !filepath.IsAbs(searchPath) {
117 searchPath = filepath.Join(l.workingDir, searchPath)
131 searchPath = filepath.Join(l.workingDir, searchPath)
118 }
132 }
119
133
120− if _, err := os.Stat(searchPath); os.IsNotExist(err) {
134+ // Check if directory is outside working directory and request permission if needed
121− return NewTextErrorResponse(fmt.Sprintf("path does not exist: %s", searchPath)), nil
135+ absWorkingDir, err := filepath.Abs(l.workingDir)
136+ if err != nil {
137+ return ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
122 }
138 }
123
139
124− files, truncated, err := fsext.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
140+ absSearchPath, err := filepath.Abs(searchPath)
125 if err != nil {
141 if err != nil {
126− return ToolResponse{}, fmt.Errorf("error listing directory: %w", err)
142+ return ToolResponse{}, fmt.Errorf("error resolving search path: %w", err)
127 }
143 }
128
144
129− tree := createFileTree(files)
145+ relPath, err := filepath.Rel(absWorkingDir, absSearchPath)
130− output := printTree(tree, searchPath)
146+ if err != nil || strings.HasPrefix(relPath, "..") {
147+ // Directory is outside working directory, request permission
148+ sessionID, messageID := GetContextValues(ctx)
149+ if sessionID == "" || messageID == "" {
150+ return ToolResponse{}, fmt.Errorf("session ID and message ID are required for accessing directories outside working directory")
151+ }
131
152
132− if truncated {
153+ granted := l.permissions.Request(
133− output = fmt.Sprintf("There are more than %d files in the directory. Use a more specific path or use the Glob tool to find specific files. The first %d files and directories are included below:\n\n%s", MaxLSFiles, MaxLSFiles, output)
154+ permission.CreatePermissionRequest{
155+ SessionID: sessionID,
156+ Path: absSearchPath,
157+ ToolCallID: call.ID,
158+ ToolName: LSToolName,
159+ Action: "list",
160+ Description: fmt.Sprintf("List directory outside working directory: %s", absSearchPath),
161+ Params: LSPermissionsParams(params),
162+ },
163+ )
164+
165+ if !granted {
166+ return ToolResponse{}, permission.ErrorPermissionDenied
167+ }
168+ }
169+
170+ output, err := ListDirectoryTree(searchPath, params.Ignore)
171+ if err != nil {
172+ return ToolResponse{}, err
173+ }
174+
175+ // Get file count for metadata
176+ files, truncated, err := fsext.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
177+ if err != nil {
178+ return ToolResponse{}, fmt.Errorf("error listing directory for metadata: %w", err)
134 }
179 }
135
180
136 return WithResponseMetadata(
181 return WithResponseMetadata(
@@ -142,6 +187,26 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
142 ), nil
187 ), nil
143}
188}
144
189
190+func ListDirectoryTree(searchPath string, ignore []string) (string, error) {
191+ if _, err := os.Stat(searchPath); os.IsNotExist(err) {
192+ return "", fmt.Errorf("path does not exist: %s", searchPath)
193+ }
194+
195+ files, truncated, err := fsext.ListDirectory(searchPath, ignore, MaxLSFiles)
196+ if err != nil {
197+ return "", fmt.Errorf("error listing directory: %w", err)
198+ }
199+
200+ tree := createFileTree(files)
201+ output := printTree(tree, searchPath)
202+
203+ if truncated {
204+ output = fmt.Sprintf("There are more than %d files in the directory. Use a more specific path or use the Glob tool to find specific files. The first %d files and directories are included below:\n\n%s", MaxLSFiles, MaxLSFiles, output)
205+ }
206+
207+ return output, nil
208+}
209+
145func createFileTree(sortedPaths []string) []*TreeNode {
210func createFileTree(sortedPaths []string) []*TreeNode {
146 root := []*TreeNode{}
211 root := []*TreeNode{}
147 pathMap := make(map[string]*TreeNode)
212 pathMap := make(map[string]*TreeNode)
internal/llm/tools/multiedit.go+2−12
@@ -249,15 +249,10 @@ func (m *multiEditTool) processMultiEditWithCreation(ctx context.Context, params
249
249
250 // Check permissions
250 // Check permissions
251 _, additions, removals := diff.GenerateDiff("", currentContent, strings.TrimPrefix(params.FilePath, m.workingDir))
251 _, additions, removals := diff.GenerateDiff("", currentContent, strings.TrimPrefix(params.FilePath, m.workingDir))
252− rootDir := m.workingDir
253− permissionPath := filepath.Dir(params.FilePath)
254− if strings.HasPrefix(params.FilePath, rootDir) {
255− permissionPath = rootDir
256− }
257
252
258 p := m.permissions.Request(permission.CreatePermissionRequest{
253 p := m.permissions.Request(permission.CreatePermissionRequest{
259 SessionID: sessionID,
254 SessionID: sessionID,
260− Path: permissionPath,
255+ Path: params.FilePath,
261 ToolCallID: call.ID,
256 ToolCallID: call.ID,
262 ToolName: MultiEditToolName,
257 ToolName: MultiEditToolName,
263 Action: "write",
258 Action: "write",
@@ -364,15 +359,10 @@ func (m *multiEditTool) processMultiEditExistingFile(ctx context.Context, params
364
359
365 // Generate diff and check permissions
360 // Generate diff and check permissions
366 _, additions, removals := diff.GenerateDiff(oldContent, currentContent, strings.TrimPrefix(params.FilePath, m.workingDir))
361 _, additions, removals := diff.GenerateDiff(oldContent, currentContent, strings.TrimPrefix(params.FilePath, m.workingDir))
367− rootDir := m.workingDir
368− permissionPath := filepath.Dir(params.FilePath)
369− if strings.HasPrefix(params.FilePath, rootDir) {
370− permissionPath = rootDir
371− }
372
362
373 p := m.permissions.Request(permission.CreatePermissionRequest{
363 p := m.permissions.Request(permission.CreatePermissionRequest{
374 SessionID: sessionID,
364 SessionID: sessionID,
375− Path: permissionPath,
365+ Path: params.FilePath,
376 ToolCallID: call.ID,
366 ToolCallID: call.ID,
377 ToolName: MultiEditToolName,
367 ToolName: MultiEditToolName,
378 Action: "write",
368 Action: "write",
internal/llm/tools/view.go+51−5
@@ -12,6 +12,7 @@ import (
12 "unicode/utf8"
12 "unicode/utf8"
13
13
14 "github.com/charmbracelet/crush/internal/lsp"
14 "github.com/charmbracelet/crush/internal/lsp"
15+ "github.com/charmbracelet/crush/internal/permission"
15)
16)
16
17
17type ViewParams struct {
18type ViewParams struct {
@@ -20,9 +21,16 @@ type ViewParams struct {
20 Limit int `json:"limit"`
21 Limit int `json:"limit"`
21}
22}
22
23
24+type ViewPermissionsParams struct {
25+ FilePath string `json:"file_path"`
26+ Offset int `json:"offset"`
27+ Limit int `json:"limit"`
28+}
29+
23type viewTool struct {
30type viewTool struct {
24− lspClients map[string]*lsp.Client
31+ lspClients map[string]*lsp.Client
25− workingDir string
32+ workingDir string
33+ permissions permission.Service
26}
34}
27
35
28type ViewResponseMetadata struct {
36type ViewResponseMetadata struct {
@@ -46,6 +54,7 @@ HOW TO USE:
46- Provide the path to the file you want to view
54- Provide the path to the file you want to view
47- Optionally specify an offset to start reading from a specific line
55- Optionally specify an offset to start reading from a specific line
48- Optionally specify a limit to control how many lines are read
56- Optionally specify a limit to control how many lines are read
57+- Do not use this for directories use the ls tool instead
49
58
50FEATURES:
59FEATURES:
51- Displays file contents with line numbers for easy reference
60- Displays file contents with line numbers for easy reference
@@ -72,10 +81,11 @@ TIPS:
72- When viewing large files, use the offset parameter to read specific sections`
81- When viewing large files, use the offset parameter to read specific sections`
73)
82)
74
83
75−func NewViewTool(lspClients map[string]*lsp.Client, workingDir string) BaseTool {
84+func NewViewTool(lspClients map[string]*lsp.Client, permissions permission.Service, workingDir string) BaseTool {
76 return &viewTool{
85 return &viewTool{
77− lspClients: lspClients,
86+ lspClients: lspClients,
78− workingDir: workingDir,
87+ workingDir: workingDir,
88+ permissions: permissions,
79 }
89 }
80}
90}
81
91
@@ -122,6 +132,42 @@ func (v *viewTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
122 filePath = filepath.Join(v.workingDir, filePath)
132 filePath = filepath.Join(v.workingDir, filePath)
123 }
133 }
124
134
135+ // Check if file is outside working directory and request permission if needed
136+ absWorkingDir, err := filepath.Abs(v.workingDir)
137+ if err != nil {
138+ return ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
139+ }
140+
141+ absFilePath, err := filepath.Abs(filePath)
142+ if err != nil {
143+ return ToolResponse{}, fmt.Errorf("error resolving file path: %w", err)
144+ }
145+
146+ relPath, err := filepath.Rel(absWorkingDir, absFilePath)
147+ if err != nil || strings.HasPrefix(relPath, "..") {
148+ // File is outside working directory, request permission
149+ sessionID, messageID := GetContextValues(ctx)
150+ if sessionID == "" || messageID == "" {
151+ return ToolResponse{}, fmt.Errorf("session ID and message ID are required for accessing files outside working directory")
152+ }
153+
154+ granted := v.permissions.Request(
155+ permission.CreatePermissionRequest{
156+ SessionID: sessionID,
157+ Path: absFilePath,
158+ ToolCallID: call.ID,
159+ ToolName: ViewToolName,
160+ Action: "read",
161+ Description: fmt.Sprintf("Read file outside working directory: %s", absFilePath),
162+ Params: ViewPermissionsParams(params),
163+ },
164+ )
165+
166+ if !granted {
167+ return ToolResponse{}, permission.ErrorPermissionDenied
168+ }
169+ }
170+
125 // Check if file exists
171 // Check if file exists
126 fileInfo, err := os.Stat(filePath)
172 fileInfo, err := os.Stat(filePath)
127 if err != nil {
173 if err != nil {
internal/llm/tools/write.go+1−6
@@ -172,15 +172,10 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
172 strings.TrimPrefix(filePath, w.workingDir),
172 strings.TrimPrefix(filePath, w.workingDir),
173 )
173 )
174
174
175− rootDir := w.workingDir
176− permissionPath := filepath.Dir(filePath)
177− if strings.HasPrefix(filePath, rootDir) {
178− permissionPath = rootDir
179− }
180 p := w.permissions.Request(
175 p := w.permissions.Request(
181 permission.CreatePermissionRequest{
176 permission.CreatePermissionRequest{
182 SessionID: sessionID,
177 SessionID: sessionID,
183− Path: permissionPath,
178+ Path: filePath,
184 ToolCallID: call.ID,
179 ToolCallID: call.ID,
185 ToolName: WriteToolName,
180 ToolName: WriteToolName,
186 Action: "write",
181 Action: "write",
internal/permission/permission.go+11−1
@@ -3,6 +3,7 @@ package permission
3import (
3import (
4 "context"
4 "context"
5 "errors"
5 "errors"
6+ "os"
6 "path/filepath"
7 "path/filepath"
7 "slices"
8 "slices"
8 "sync"
9 "sync"
@@ -145,7 +146,16 @@ func (s *permissionService) Request(opts CreatePermissionRequest) bool {
145 return true
146 return true
146 }
147 }
147
148
149+ fileInfo, err := os.Stat(opts.Path)
150+ dir := opts.Path
151+ if err == nil {
152+ if fileInfo.IsDir() {
153+ dir = opts.Path
154+ } else {
148− dir := filepath.Dir(opts.Path)
155+ dir = filepath.Dir(opts.Path)
156+ }
157+ }
158+
149 if dir == "." {
159 if dir == "." {
150 dir = s.workingDir
160 dir = s.workingDir
151 }
161 }
internal/tui/components/dialogs/permissions/permissions.go+77
@@ -321,6 +321,34 @@ func (p *permissionDialogCmp) renderHeader() string {
321 )
321 )
322 case tools.FetchToolName:
322 case tools.FetchToolName:
323 headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("URL"))
323 headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("URL"))
324+ case tools.ViewToolName:
325+ params := p.permission.Params.(tools.ViewPermissionsParams)
326+ fileKey := t.S().Muted.Render("File")
327+ filePath := t.S().Text.
328+ Width(p.width - lipgloss.Width(fileKey)).
329+ Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
330+ headerParts = append(headerParts,
331+ lipgloss.JoinHorizontal(
332+ lipgloss.Left,
333+ fileKey,
334+ filePath,
335+ ),
336+ baseStyle.Render(strings.Repeat(" ", p.width)),
337+ )
338+ case tools.LSToolName:
339+ params := p.permission.Params.(tools.LSPermissionsParams)
340+ pathKey := t.S().Muted.Render("Directory")
341+ pathValue := t.S().Text.
342+ Width(p.width - lipgloss.Width(pathKey)).
343+ Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.Path)))
344+ headerParts = append(headerParts,
345+ lipgloss.JoinHorizontal(
346+ lipgloss.Left,
347+ pathKey,
348+ pathValue,
349+ ),
350+ baseStyle.Render(strings.Repeat(" ", p.width)),
351+ )
324 }
352 }
325
353
326 return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
354 return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
@@ -347,6 +375,10 @@ func (p *permissionDialogCmp) getOrGenerateContent() string {
347 content = p.generateMultiEditContent()
375 content = p.generateMultiEditContent()
348 case tools.FetchToolName:
376 case tools.FetchToolName:
349 content = p.generateFetchContent()
377 content = p.generateFetchContent()
378+ case tools.ViewToolName:
379+ content = p.generateViewContent()
380+ case tools.LSToolName:
381+ content = p.generateLSContent()
350 default:
382 default:
351 content = p.generateDefaultContent()
383 content = p.generateDefaultContent()
352 }
384 }
@@ -486,6 +518,45 @@ func (p *permissionDialogCmp) generateFetchContent() string {
486 return ""
518 return ""
487}
519}
488
520
521+func (p *permissionDialogCmp) generateViewContent() string {
522+ t := styles.CurrentTheme()
523+ baseStyle := t.S().Base.Background(t.BgSubtle)
524+ if pr, ok := p.permission.Params.(tools.ViewPermissionsParams); ok {
525+ content := fmt.Sprintf("File: %s", fsext.PrettyPath(pr.FilePath))
526+ if pr.Offset > 0 {
527+ content += fmt.Sprintf("\nStarting from line: %d", pr.Offset+1)
528+ }
529+ if pr.Limit > 0 && pr.Limit != 2000 { // 2000 is the default limit
530+ content += fmt.Sprintf("\nLines to read: %d", pr.Limit)
531+ }
532+
533+ finalContent := baseStyle.
534+ Padding(1, 2).
535+ Width(p.contentViewPort.Width()).
536+ Render(content)
537+ return finalContent
538+ }
539+ return ""
540+}
541+
542+func (p *permissionDialogCmp) generateLSContent() string {
543+ t := styles.CurrentTheme()
544+ baseStyle := t.S().Base.Background(t.BgSubtle)
545+ if pr, ok := p.permission.Params.(tools.LSPermissionsParams); ok {
546+ content := fmt.Sprintf("Directory: %s", fsext.PrettyPath(pr.Path))
547+ if len(pr.Ignore) > 0 {
548+ content += fmt.Sprintf("\nIgnore patterns: %s", strings.Join(pr.Ignore, ", "))
549+ }
550+
551+ finalContent := baseStyle.
552+ Padding(1, 2).
553+ Width(p.contentViewPort.Width()).
554+ Render(content)
555+ return finalContent
556+ }
557+ return ""
558+}
559+
489func (p *permissionDialogCmp) generateDefaultContent() string {
560func (p *permissionDialogCmp) generateDefaultContent() string {
490 t := styles.CurrentTheme()
561 t := styles.CurrentTheme()
491 baseStyle := t.S().Base.Background(t.BgSubtle)
562 baseStyle := t.S().Base.Background(t.BgSubtle)
@@ -623,6 +694,12 @@ func (p *permissionDialogCmp) SetSize() tea.Cmd {
623 case tools.FetchToolName:
694 case tools.FetchToolName:
624 p.width = int(float64(p.wWidth) * 0.8)
695 p.width = int(float64(p.wWidth) * 0.8)
625 p.height = int(float64(p.wHeight) * 0.3)
696 p.height = int(float64(p.wHeight) * 0.3)
697+ case tools.ViewToolName:
698+ p.width = int(float64(p.wWidth) * 0.8)
699+ p.height = int(float64(p.wHeight) * 0.4)
700+ case tools.LSToolName:
701+ p.width = int(float64(p.wWidth) * 0.8)
702+ p.height = int(float64(p.wHeight) * 0.4)
626 default:
703 default:
627 p.width = int(float64(p.wWidth) * 0.7)
704 p.width = int(float64(p.wWidth) * 0.7)
628 p.height = int(float64(p.wHeight) * 0.5)
705 p.height = int(float64(p.wHeight) * 0.5)