feat: auto permission mode, edit diffs, truncated tool output

- Default permission mode changed to 'auto' (read-only auto-allows,
  writes prompt)
- fs.edit now shows diff-style output: line numbers, context ±3 lines,
  + for added (green), - for removed (red)
- Tool output truncated to 10 lines in TUI with "+N lines (Ctrl+O
  to expand)" indicator
- Mistral SDK bumped to v1.3.0
This commit is contained in:
2026-04-03 18:57:13 +02:00
parent f3d3390b86
commit 4847421b17
7 changed files with 91 additions and 7 deletions

View File

@@ -41,7 +41,7 @@ func main() {
system = flag.String("system", defaultSystem, "system prompt")
apiKey = flag.String("api-key", "", "API key (or set MISTRAL_API_KEY env)")
maxTurns = flag.Int("max-turns", 50, "max tool-calling rounds per turn")
permMode = flag.String("permission", "default", "permission mode (default, accept_edits, bypass, deny, plan, auto)")
permMode = flag.String("permission", "auto", "permission mode (default, accept_edits, bypass, deny, plan, auto)")
incognito = flag.Bool("incognito", false, "incognito mode — no persistence, no learning")
verbose = flag.Bool("verbose", false, "enable debug logging")
version = flag.Bool("version", false, "print version and exit")

2
go.mod
View File

@@ -8,7 +8,7 @@ require (
charm.land/glamour/v2 v2.0.0
charm.land/lipgloss/v2 v2.0.2
github.com/BurntSushi/toml v0.3.1
github.com/VikingOwl91/mistral-go-sdk v1.2.1
github.com/VikingOwl91/mistral-go-sdk v1.3.0
github.com/anthropics/anthropic-sdk-go v1.29.0
github.com/openai/openai-go v1.12.0
golang.org/x/text v0.27.0

2
go.sum
View File

@@ -19,6 +19,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/VikingOwl91/mistral-go-sdk v1.2.1 h1:6OQMtOzJUFcvFUEtbX9VlglUPBn+dKOrQPnyoVKlpkA=
github.com/VikingOwl91/mistral-go-sdk v1.2.1/go.mod h1:f4emNtHUx2zSqY3V0LBz6lNI1jE6q/zh+SEU+/hJ0i4=
github.com/VikingOwl91/mistral-go-sdk v1.3.0 h1:OkTsodDE5lmdf7p2cwScqD2vIk8sScQ2IGk65dUjuz0=
github.com/VikingOwl91/mistral-go-sdk v1.3.0/go.mod h1:f4emNtHUx2zSqY3V0LBz6lNI1jE6q/zh+SEU+/hJ0i4=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=

View File

@@ -102,8 +102,68 @@ func (t *EditTool) Execute(_ context.Context, args json.RawMessage) (tool.Result
replacements = count
}
// Generate diff-style output with context
diff := buildEditDiff(content, a.OldString, a.NewString, a.Path, replacements)
return tool.Result{
Output: fmt.Sprintf("Replaced %d occurrence(s) in %s", replacements, a.Path),
Output: diff,
Metadata: map[string]any{"replacements": replacements, "path": a.Path},
}, nil
}
// buildEditDiff generates a diff display with context lines around the edit.
func buildEditDiff(original, oldStr, newStr, path string, replacements int) string {
contextLines := 3
lines := strings.Split(original, "\n")
// Find the line where the old string starts
editStart := -1
for i, line := range lines {
if strings.Contains(line, strings.Split(oldStr, "\n")[0]) {
editStart = i
break
}
}
if editStart == -1 {
return fmt.Sprintf("Replaced %d occurrence(s) in %s", replacements, path)
}
oldLines := strings.Split(oldStr, "\n")
newLines := strings.Split(newStr, "\n")
var b strings.Builder
fmt.Fprintf(&b, "Edit(%s)\n", path)
fmt.Fprintf(&b, " Added %d lines, removed %d lines\n", len(newLines), len(oldLines))
// Context before
start := editStart - contextLines
if start < 0 {
start = 0
}
for i := start; i < editStart; i++ {
fmt.Fprintf(&b, " %4d %s\n", i+1, lines[i])
}
// Removed lines (old)
for i, line := range oldLines {
fmt.Fprintf(&b, " %4d - %s\n", editStart+i+1, line)
}
// Added lines (new)
for i, line := range newLines {
fmt.Fprintf(&b, " %4d + %s\n", editStart+i+1, line)
}
// Context after
afterStart := editStart + len(oldLines)
afterEnd := afterStart + contextLines
if afterEnd > len(lines) {
afterEnd = len(lines)
}
for i := afterStart; i < afterEnd; i++ {
fmt.Fprintf(&b, " %4d %s\n", i+1, lines[i])
}
return b.String()
}

View File

@@ -186,7 +186,7 @@ func TestEditTool_SingleReplace(t *testing.T) {
if err != nil {
t.Fatalf("Execute: %v", err)
}
if !strings.Contains(result.Output, "1 occurrence") {
if !strings.Contains(result.Output, "Edit(") && !strings.Contains(result.Output, "Replaced") {
t.Errorf("Output = %q", result.Output)
}
@@ -206,7 +206,7 @@ func TestEditTool_ReplaceAll(t *testing.T) {
if err != nil {
t.Fatalf("Execute: %v", err)
}
if !strings.Contains(result.Output, "3 occurrence") {
if !strings.Contains(result.Output, "Edit(") && !strings.Contains(result.Output, "3 occurrence") {
t.Errorf("Output = %q", result.Output)
}

View File

@@ -679,9 +679,25 @@ func (m Model) renderMessage(msg chatMessage) []string {
lines = append(lines, indent+sToolOutput.Render(msg.content))
case "toolresult":
for _, line := range strings.Split(msg.content, "\n") {
resultLines := strings.Split(msg.content, "\n")
maxShow := 10
for i, line := range resultLines {
if i >= maxShow {
remaining := len(resultLines) - maxShow
lines = append(lines, indent+indent+sHint.Render(
fmt.Sprintf("+%d lines (Ctrl+O to expand)", remaining)))
break
}
// Diff coloring for edit results
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "+") && !strings.HasPrefix(trimmed, "++") && len(trimmed) > 1 {
lines = append(lines, indent+indent+sDiffAdd.Render(line))
} else if strings.HasPrefix(trimmed, "-") && !strings.HasPrefix(trimmed, "--") && len(trimmed) > 1 {
lines = append(lines, indent+indent+sDiffRemove.Render(line))
} else {
lines = append(lines, indent+indent+sToolResult.Render(line))
}
}
lines = append(lines, "")
case "system":

View File

@@ -85,6 +85,12 @@ var (
sCursor = lipgloss.NewStyle().
Foreground(cPurple)
sDiffAdd = lipgloss.NewStyle().
Foreground(cGreen)
sDiffRemove = lipgloss.NewStyle().
Foreground(cRed)
)
// Status bar