Detailed changes
@@ -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 {
@@ -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 {
@@ -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)
@@ -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)
}
// -----------------------------------------------------------------------------
@@ -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, "…")
@@ -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)
@@ -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
@@ -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,