86f69f6dd3
- retry logic for transient LLM errors (openai.go) - file_search context_lines + max_results (file.go) - FormatResult now preserves head+tail with omitted bytes (tools.go) - /status /diff /test slash commands wired (workflow.go, app.go) - CodeSymbolsTool for Go package/type/func listing (code.go) - README updated with v0.0.4 features
144 lines
3.4 KiB
Go
144 lines
3.4 KiB
Go
package tui
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"agentu/internal/tools"
|
|
)
|
|
|
|
const workflowCommandTimeout = 2 * time.Minute
|
|
|
|
func (m model) handleStatusCommand(args []string) (string, error) {
|
|
if len(args) != 0 {
|
|
return "", fmt.Errorf("usage: /status")
|
|
}
|
|
out, err := m.runGitCommand("status", "--short")
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
if strings.TrimSpace(out) == "" {
|
|
return "Working tree clean.", nil
|
|
}
|
|
return "Changed files:\n" + out, nil
|
|
}
|
|
|
|
func (m model) handleDiffCommand(args []string) (string, error) {
|
|
if len(args) > 0 && args[0] == "--stat" {
|
|
out, err := m.runGitCommand(append([]string{"diff", "--stat", "--"}, args[1:]...)...)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
if strings.TrimSpace(out) == "" {
|
|
return "No unstaged diff.", nil
|
|
}
|
|
return out, nil
|
|
}
|
|
out, err := m.runGitCommand(append([]string{"diff", "--"}, args...)...)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
if strings.TrimSpace(out) == "" {
|
|
return "No unstaged diff.", nil
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m model) handleTestCommand(args []string) (string, error) {
|
|
command := strings.TrimSpace(strings.Join(args, " "))
|
|
if command == "" {
|
|
var err error
|
|
command, err = defaultTestCommand(m.workflowDir())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
out, err := m.runShellCommand(command)
|
|
if err != nil {
|
|
if strings.TrimSpace(out) != "" {
|
|
return out, err
|
|
}
|
|
return "", err
|
|
}
|
|
if strings.TrimSpace(out) == "" {
|
|
return fmt.Sprintf("$ %s\n(no output)", command), nil
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m model) runGitCommand(args ...string) (string, error) {
|
|
ctx, cancel := context.WithTimeout(m.ctx, workflowCommandTimeout)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(ctx, "git", append([]string{"-C", m.workflowDir()}, args...)...)
|
|
output, err := cmd.CombinedOutput()
|
|
text := tools.FormatResult(string(output))
|
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
|
return text, fmt.Errorf("git command timed out: %w", ctx.Err())
|
|
}
|
|
if err != nil {
|
|
if strings.TrimSpace(text) != "" {
|
|
return text, fmt.Errorf("git command failed: %w", err)
|
|
}
|
|
return "", fmt.Errorf("git command failed: %w", err)
|
|
}
|
|
return text, nil
|
|
}
|
|
|
|
func (m model) runShellCommand(command string) (string, error) {
|
|
ctx, cancel := context.WithTimeout(m.ctx, workflowCommandTimeout)
|
|
defer cancel()
|
|
|
|
shell := os.Getenv("SHELL")
|
|
if shell == "" {
|
|
shell = "/bin/sh"
|
|
}
|
|
cmd := exec.CommandContext(ctx, shell, "-lc", command)
|
|
cmd.Dir = m.workflowDir()
|
|
|
|
var combined bytes.Buffer
|
|
cmd.Stdout = &combined
|
|
cmd.Stderr = &combined
|
|
err := cmd.Run()
|
|
text := strings.TrimRight(tools.FormatResult(combined.String()), "\n")
|
|
if text != "" {
|
|
text = fmt.Sprintf("$ %s\n%s", command, text)
|
|
}
|
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
|
return text, fmt.Errorf("command timed out: %w", ctx.Err())
|
|
}
|
|
if err != nil {
|
|
return text, fmt.Errorf("command failed: %w", err)
|
|
}
|
|
return text, nil
|
|
}
|
|
|
|
func (m model) workflowDir() string {
|
|
if strings.TrimSpace(m.workingDir) == "" {
|
|
return "."
|
|
}
|
|
return m.workingDir
|
|
}
|
|
|
|
func defaultTestCommand(dir string) (string, error) {
|
|
if fileExists(filepath.Join(dir, "go.mod")) {
|
|
return "go test ./...", nil
|
|
}
|
|
if fileExists(filepath.Join(dir, "package.json")) {
|
|
return "npm test", nil
|
|
}
|
|
return "", fmt.Errorf("no default test command found; use /test <command>")
|
|
}
|
|
|
|
func fileExists(path string) bool {
|
|
info, err := os.Stat(path)
|
|
return err == nil && !info.IsDir()
|
|
}
|