diff --git a/internal/agent/tools/edit.go b/internal/agent/tools/edit.go index c445a3f11cf0078f2ad5a823eacfe4fd88072d5d..b79de6d49d30d60421c810247904d4830886bc3d 100644 --- a/internal/agent/tools/edit.go +++ b/internal/agent/tools/edit.go @@ -154,7 +154,14 @@ func createNewFile(edit editContext, filePath, content string, call fantasy.Tool return fantasy.ToolResponse{}, err } if !p { - return NewPermissionDeniedResponse(), nil + resp := NewPermissionDeniedResponse() + resp = fantasy.WithResponseMetadata(resp, EditResponseMetadata{ + OldContent: "", + NewContent: content, + Additions: additions, + Removals: removals, + }) + return resp, nil } err = os.WriteFile(filePath, []byte(content), 0o644) @@ -276,7 +283,14 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool return fantasy.ToolResponse{}, err } if !p { - return NewPermissionDeniedResponse(), nil + resp := NewPermissionDeniedResponse() + resp = fantasy.WithResponseMetadata(resp, EditResponseMetadata{ + OldContent: oldContent, + NewContent: newContent, + Additions: additions, + Removals: removals, + }) + return resp, nil } if isCrlf { @@ -410,7 +424,14 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep return fantasy.ToolResponse{}, err } if !p { - return NewPermissionDeniedResponse(), nil + resp := NewPermissionDeniedResponse() + resp = fantasy.WithResponseMetadata(resp, EditResponseMetadata{ + OldContent: oldContent, + NewContent: newContent, + Additions: additions, + Removals: removals, + }) + return resp, nil } if isCrlf { diff --git a/internal/agent/tools/multiedit.go b/internal/agent/tools/multiedit.go index 270acc3d3cfee2b33cf0e3b7264f71958738004c..0f694211b372dc5c5d01d9da8b97ee213078c79e 100644 --- a/internal/agent/tools/multiedit.go +++ b/internal/agent/tools/multiedit.go @@ -196,7 +196,16 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call return fantasy.ToolResponse{}, err } if !p { - return NewPermissionDeniedResponse(), nil + resp := NewPermissionDeniedResponse() + resp = fantasy.WithResponseMetadata(resp, MultiEditResponseMetadata{ + OldContent: "", + NewContent: currentContent, + Additions: additions, + Removals: removals, + EditsApplied: editsApplied, + EditsFailed: failedEdits, + }) + return resp, nil } // Write the file @@ -340,7 +349,16 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call return fantasy.ToolResponse{}, err } if !p { - return NewPermissionDeniedResponse(), nil + resp := NewPermissionDeniedResponse() + resp = fantasy.WithResponseMetadata(resp, MultiEditResponseMetadata{ + OldContent: oldContent, + NewContent: currentContent, + Additions: additions, + Removals: removals, + EditsApplied: editsApplied, + EditsFailed: failedEdits, + }) + return resp, nil } if isCrlf { diff --git a/internal/agent/tools/write.go b/internal/agent/tools/write.go index c76e244b1926698bd5c61001f5eaf3209949ab84..2868826dcb7f986e61cf48abdc698c7ae4c89c37 100644 --- a/internal/agent/tools/write.go +++ b/internal/agent/tools/write.go @@ -125,7 +125,13 @@ func NewWriteTool( return fantasy.ToolResponse{}, err } if !p { - return NewPermissionDeniedResponse(), nil + resp := NewPermissionDeniedResponse() + resp = fantasy.WithResponseMetadata(resp, WriteResponseMetadata{ + Diff: diff, + Additions: additions, + Removals: removals, + }) + return resp, nil } err = os.WriteFile(filePath, []byte(params.Content), 0o644) diff --git a/internal/ui/chat/file.go b/internal/ui/chat/file.go index 14fc5169aec1b0a238cad177a0be5bf4d6db27b0..3b4b147056f362e9949cbdac4588dd020caddab6 100644 --- a/internal/ui/chat/file.go +++ b/internal/ui/chat/file.go @@ -139,17 +139,31 @@ func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { - return joinToolParts(header, earlyState) + if !opts.HasResult() { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { + return joinToolParts(header, earlyState) + } + return header } - if params.Content == "" { - return header + // On error with diff metadata (e.g. denied permission), show error + diff. + if opts.Result.IsError { + var meta tools.WriteResponseMetadata + if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err == nil && meta.Diff != "" { + errLine := toolErrorContent(sty, opts.Result, cappedWidth) + diff := toolOutputDiffContentFromUnified(sty, meta.Diff, cappedWidth, opts.ExpandedContent) + return strings.Join([]string{header, "", errLine, "", diff}, "\n") + } + return joinToolParts(header, toolErrorContent(sty, opts.Result, cappedWidth)) } // Render code content with syntax highlighting. - body := toolOutputCodeContent(sty, params.FilePath, params.Content, 0, cappedWidth, opts.ExpandedContent) - return joinToolParts(header, body) + if params.Content != "" { + body := toolOutputCodeContent(sty, params.FilePath, params.Content, 0, cappedWidth, opts.ExpandedContent) + return joinToolParts(header, body) + } + + return header } // ----------------------------------------------------------------------------- @@ -194,11 +208,10 @@ func (e *EditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts * return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { - return joinToolParts(header, earlyState) - } - if !opts.HasResult() { + if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + return joinToolParts(header, earlyState) + } return header } @@ -210,9 +223,15 @@ func (e *EditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts * return joinToolParts(header, body) } - // Render diff. - body := toolOutputDiffContent(sty, file, meta.OldContent, meta.NewContent, width, opts.ExpandedContent) - return joinToolParts(header, body) + diff := toolOutputDiffContent(sty, file, meta.OldContent, meta.NewContent, width, opts.ExpandedContent) + + // On error (e.g. denied permission), show error above the diff. + if opts.Result.IsError { + errLine := toolErrorContent(sty, opts.Result, width) + return strings.Join([]string{header, "", errLine, "", diff}, "\n") + } + + return joinToolParts(header, diff) } // ----------------------------------------------------------------------------- @@ -262,11 +281,10 @@ func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, o return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { - return joinToolParts(header, earlyState) - } - if !opts.HasResult() { + if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + return joinToolParts(header, earlyState) + } return header } @@ -279,8 +297,15 @@ func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, o } // Render diff with optional failed edits note. - body := toolOutputMultiEditDiffContent(sty, file, meta, len(params.Edits), width, opts.ExpandedContent) - return joinToolParts(header, body) + diff := toolOutputMultiEditDiffContent(sty, file, meta, len(params.Edits), width, opts.ExpandedContent) + + // On error (e.g. denied permission), show error above the diff. + if opts.Result.IsError { + errLine := toolErrorContent(sty, opts.Result, width) + return strings.Join([]string{header, "", errLine, "", diff}, "\n") + } + + return joinToolParts(header, diff) } // ----------------------------------------------------------------------------- diff --git a/internal/ui/chat/tools.go b/internal/ui/chat/tools.go index 9a4cf33af149af6d12b6e15b341bd98301734f94..961173f30bfcf8a32333372ea26952cb932fd444 100644 --- a/internal/ui/chat/tools.go +++ b/internal/ui/chat/tools.go @@ -536,12 +536,18 @@ func toolEarlyStateContent(sty *styles.Styles, opts *ToolRenderOpts, width int) return msg, true } -// toolErrorContent formats an error message with ERROR tag. +// toolErrorContent formats an error message with an ERROR or WARN tag. func toolErrorContent(sty *styles.Styles, result *message.ToolResult, width int) string { if result == nil { return "" } errContent := strings.ReplaceAll(result.Content, "\n", " ") + if strings.Contains(errContent, "User denied permission") { + deniedTag := sty.Tool.WarnTag.Render("WARN") + deniedTagWidth := lipgloss.Width(deniedTag) + errContent = ansi.Truncate(errContent, width-deniedTagWidth-3, "…") + return fmt.Sprintf("%s %s", deniedTag, sty.Tool.WarnMessage.Render(errContent)) + } errTag := sty.Tool.ErrorTag.Render("ERROR") tagWidth := lipgloss.Width(errTag) errContent = ansi.Truncate(errContent, width-tagWidth-3, "…") diff --git a/internal/ui/styles/quickstyle.go b/internal/ui/styles/quickstyle.go index 4600ea9fe8692d8c7175336379e8080270c46a42..465916db8f95718e97ad45a6a68a187c59eb47a7 100644 --- a/internal/ui/styles/quickstyle.go +++ b/internal/ui/styles/quickstyle.go @@ -47,6 +47,7 @@ type quickStyleOpts struct { error color.Color warning color.Color warningSubtle color.Color + denied color.Color busy color.Color info color.Color infoMoreSubtle color.Color @@ -622,6 +623,9 @@ func quickStyle(o quickStyleOpts) Styles { s.Tool.ErrorTag = base.Padding(0, 1).Background(o.destructive).Foreground(o.onPrimary) s.Tool.ErrorMessage = base.Foreground(o.fgSubtle) + s.Tool.WarnTag = base.Padding(0, 1).Background(o.denied).Foreground(o.bgBase).Bold(true) + s.Tool.WarnMessage = base.Foreground(o.fgSubtle) + // Diff and multi-edit styles s.Tool.DiffTruncation = muted.Background(o.bgLeastVisible).PaddingLeft(2) s.Tool.NoteTag = base.Padding(0, 1).Background(o.info).Foreground(o.onPrimary) diff --git a/internal/ui/styles/styles.go b/internal/ui/styles/styles.go index f0a08537352eac2ee821265db5b76457bc4402d2..c2ca9824ba746f5b04530b210c6f5ca9caaa370f 100644 --- a/internal/ui/styles/styles.go +++ b/internal/ui/styles/styles.go @@ -292,6 +292,10 @@ type Styles struct { ErrorTag lipgloss.Style // ERROR tag ErrorMessage lipgloss.Style // Error message text + // Warning styles (used for permission denied) + WarnTag lipgloss.Style // WARN tag + WarnMessage lipgloss.Style // Warning message text + // Diff styles DiffTruncation lipgloss.Style // Diff truncation message with padding diff --git a/internal/ui/styles/themes.go b/internal/ui/styles/themes.go index 754abee7b4329f7237fced66340f4f129cbd59f3..a6ed551c877b1e1dd2e77b8861a30faab6e409d4 100644 --- a/internal/ui/styles/themes.go +++ b/internal/ui/styles/themes.go @@ -41,6 +41,7 @@ func CharmtonePantera() Styles { error: charmtone.Sriracha, warningSubtle: charmtone.Zest, warning: charmtone.Mustard, + denied: charmtone.Tang, busy: charmtone.Citron, info: charmtone.Malibu, infoMoreSubtle: charmtone.Sardine, @@ -76,6 +77,7 @@ func HypercrushObsidiana() Styles { error: charmtone.Sriracha, warningSubtle: charmtone.Zest, warning: charmtone.Mustard, + denied: charmtone.Tang, busy: charmtone.Citron, info: charmtone.Malibu, infoMoreSubtle: charmtone.Sardine,