feat: Phase 1 code-agent capabilities — file_edit, git tools, parallel execution, tool output preview

New tools:
- file_edit: surgical line-range or pattern edits (yolo only)
- git_status: show changed files (read-only)
- git_diff: show unstaged/staged diff (read-only)
- git_log: show recent commits (read-only)

Agent improvements:
- parallel execution for independent read-only tool calls
- mutating tools (file_write, file_edit, shell_run) run sequentially
- tool output preview in logs with [output] marker

TUI improvements:
- tool cards now show truncated output preview (up to 8 lines)
- mergeToolLog updates placeholder cards with results
- toolStatusText keeps activity bar single-line

Documentation:
- README updated with git tools and file_edit descriptions
This commit is contained in:
loveuer
2026-06-24 19:47:14 -07:00
parent 7b8bad5e62
commit 3d403bd685
13 changed files with 1135 additions and 39 deletions
+69 -10
View File
@@ -290,8 +290,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case toolLogMsg:
text := cleanToolLog(string(msg))
if text != "" {
m.messages = append(m.messages, message{role: roleTool, content: text})
m.status = text
if !m.mergeToolLog(text) {
m.messages = append(m.messages, message{role: roleTool, content: text})
}
m.status = toolStatusText(text)
m.refreshViewport(true)
}
return m, m.waitForEvent()
@@ -572,8 +574,9 @@ func roleLabel(r role) string {
}
type toolLogParts struct {
name string
args string
name string
args string
output string
}
func parseToolLog(content string) toolLogParts {
@@ -581,14 +584,21 @@ func parseToolLog(content string) toolLogParts {
if content == "" {
return toolLogParts{}
}
idx := strings.IndexFunc(content, unicode.IsSpace)
beforeOutput, output, hasOutput := strings.Cut(content, "\n[output]\n")
parts := toolLogParts{}
if hasOutput {
parts.output = strings.TrimSpace(output)
}
firstLine := strings.SplitN(beforeOutput, "\n", 2)[0]
firstLine = strings.TrimSpace(firstLine)
idx := strings.IndexFunc(firstLine, unicode.IsSpace)
if idx < 0 {
return toolLogParts{name: content}
}
return toolLogParts{
name: strings.TrimSpace(content[:idx]),
args: strings.TrimSpace(content[idx+1:]),
parts.name = firstLine
return parts
}
parts.name = strings.TrimSpace(firstLine[:idx])
parts.args = strings.TrimSpace(firstLine[idx+1:])
return parts
}
func (m model) toolMessageView(content string, width int) string {
@@ -599,11 +609,60 @@ func (m model) toolMessageView(content string, width int) string {
args := fitLine(parts.args, max(1, width-4))
lines = append(lines, m.styles.ToolValue.Render(args))
}
if parts.output != "" {
lines = append(lines, m.styles.ToolKey.Render("output"))
previewLines := toolOutputPreviewLines(parts.output, max(1, width-4))
for _, line := range previewLines {
lines = append(lines, m.styles.ToolValue.Render(line))
}
}
bodyContent := strings.Join(lines, "\n")
bodyWidth := messageBodyWidth(bodyContent, width, m.styles.ToolMessage)
return m.styles.ToolMessage.Width(bodyWidth).Render(bodyContent)
}
const maxToolOutputPreviewLines = 8
func toolStatusText(content string) string {
content = strings.TrimSpace(content)
if content == "" {
return ""
}
lines := strings.SplitN(content, "\n", 2)
return lines[0]
}
func (m *model) mergeToolLog(text string) bool {
incoming := parseToolLog(text)
if incoming.output == "" {
return false
}
for i := len(m.messages) - 1; i >= 0; i-- {
if m.messages[i].role != roleTool {
continue
}
existing := parseToolLog(m.messages[i].content)
if existing.name == incoming.name && existing.args == incoming.args && existing.output == "" {
m.messages[i].content = text
return true
}
}
return false
}
func toolOutputPreviewLines(output string, width int) []string {
lines := strings.Split(output, "\n")
if len(lines) > maxToolOutputPreviewLines {
lines = lines[:maxToolOutputPreviewLines]
lines = append(lines, fmt.Sprintf("[truncated: showing first %d lines]", maxToolOutputPreviewLines))
}
result := make([]string, 0, len(lines))
for _, line := range lines {
result = append(result, fitLine(line, width))
}
return result
}
type metaPill struct {
text string
accent bool