|
|
@@ -1,889 +0,0 @@
|
|
|
-package toolrender
|
|
|
-
|
|
|
-import (
|
|
|
- "cmp"
|
|
|
- "encoding/json"
|
|
|
- "fmt"
|
|
|
- "strings"
|
|
|
-
|
|
|
- "charm.land/lipgloss/v2"
|
|
|
- "charm.land/lipgloss/v2/tree"
|
|
|
- "github.com/charmbracelet/crush/internal/agent"
|
|
|
- "github.com/charmbracelet/crush/internal/agent/tools"
|
|
|
- "github.com/charmbracelet/crush/internal/ansiext"
|
|
|
- "github.com/charmbracelet/crush/internal/fsext"
|
|
|
- "github.com/charmbracelet/crush/internal/message"
|
|
|
- "github.com/charmbracelet/crush/internal/ui/common"
|
|
|
- "github.com/charmbracelet/crush/internal/ui/styles"
|
|
|
- "github.com/charmbracelet/x/ansi"
|
|
|
-)
|
|
|
-
|
|
|
-// responseContextHeight limits the number of lines displayed in tool output.
|
|
|
-const responseContextHeight = 10
|
|
|
-
|
|
|
-// RenderContext provides the context needed for rendering a tool call.
|
|
|
-type RenderContext struct {
|
|
|
- Call message.ToolCall
|
|
|
- Result message.ToolResult
|
|
|
- Cancelled bool
|
|
|
- IsNested bool
|
|
|
- Width int
|
|
|
- Styles *styles.Styles
|
|
|
-}
|
|
|
-
|
|
|
-// TextWidth returns the available width for content accounting for borders.
|
|
|
-func (rc *RenderContext) TextWidth() int {
|
|
|
- if rc.IsNested {
|
|
|
- return rc.Width - 6
|
|
|
- }
|
|
|
- return rc.Width - 5
|
|
|
-}
|
|
|
-
|
|
|
-// Fit truncates content to fit within the specified width with ellipsis.
|
|
|
-func (rc *RenderContext) Fit(content string, width int) string {
|
|
|
- lineStyle := rc.Styles.Muted
|
|
|
- dots := lineStyle.Render("…")
|
|
|
- return ansi.Truncate(content, width, dots)
|
|
|
-}
|
|
|
-
|
|
|
-// Render renders a tool call using the appropriate renderer based on tool name.
|
|
|
-func Render(ctx *RenderContext) string {
|
|
|
- switch ctx.Call.Name {
|
|
|
- case tools.ViewToolName:
|
|
|
- return renderView(ctx)
|
|
|
- case tools.EditToolName:
|
|
|
- return renderEdit(ctx)
|
|
|
- case tools.MultiEditToolName:
|
|
|
- return renderMultiEdit(ctx)
|
|
|
- case tools.WriteToolName:
|
|
|
- return renderWrite(ctx)
|
|
|
- case tools.BashToolName:
|
|
|
- return renderBash(ctx)
|
|
|
- case tools.JobOutputToolName:
|
|
|
- return renderJobOutput(ctx)
|
|
|
- case tools.JobKillToolName:
|
|
|
- return renderJobKill(ctx)
|
|
|
- case tools.FetchToolName:
|
|
|
- return renderSimpleFetch(ctx)
|
|
|
- case tools.AgenticFetchToolName:
|
|
|
- return renderAgenticFetch(ctx)
|
|
|
- case tools.WebFetchToolName:
|
|
|
- return renderWebFetch(ctx)
|
|
|
- case tools.DownloadToolName:
|
|
|
- return renderDownload(ctx)
|
|
|
- case tools.GlobToolName:
|
|
|
- return renderGlob(ctx)
|
|
|
- case tools.GrepToolName:
|
|
|
- return renderGrep(ctx)
|
|
|
- case tools.LSToolName:
|
|
|
- return renderLS(ctx)
|
|
|
- case tools.SourcegraphToolName:
|
|
|
- return renderSourcegraph(ctx)
|
|
|
- case tools.DiagnosticsToolName:
|
|
|
- return renderDiagnostics(ctx)
|
|
|
- case agent.AgentToolName:
|
|
|
- return renderAgent(ctx)
|
|
|
- default:
|
|
|
- return renderGeneric(ctx)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// Helper functions
|
|
|
-
|
|
|
-func unmarshalParams(input string, target any) error {
|
|
|
- return json.Unmarshal([]byte(input), target)
|
|
|
-}
|
|
|
-
|
|
|
-type paramBuilder struct {
|
|
|
- args []string
|
|
|
-}
|
|
|
-
|
|
|
-func newParamBuilder() *paramBuilder {
|
|
|
- return ¶mBuilder{args: make([]string, 0)}
|
|
|
-}
|
|
|
-
|
|
|
-func (pb *paramBuilder) addMain(value string) *paramBuilder {
|
|
|
- if value != "" {
|
|
|
- pb.args = append(pb.args, value)
|
|
|
- }
|
|
|
- return pb
|
|
|
-}
|
|
|
-
|
|
|
-func (pb *paramBuilder) addKeyValue(key, value string) *paramBuilder {
|
|
|
- if value != "" {
|
|
|
- pb.args = append(pb.args, key, value)
|
|
|
- }
|
|
|
- return pb
|
|
|
-}
|
|
|
-
|
|
|
-func (pb *paramBuilder) addFlag(key string, value bool) *paramBuilder {
|
|
|
- if value {
|
|
|
- pb.args = append(pb.args, key, "true")
|
|
|
- }
|
|
|
- return pb
|
|
|
-}
|
|
|
-
|
|
|
-func (pb *paramBuilder) build() []string {
|
|
|
- return pb.args
|
|
|
-}
|
|
|
-
|
|
|
-func formatNonZero[T comparable](value T) string {
|
|
|
- var zero T
|
|
|
- if value == zero {
|
|
|
- return ""
|
|
|
- }
|
|
|
- return fmt.Sprintf("%v", value)
|
|
|
-}
|
|
|
-
|
|
|
-func makeHeader(ctx *RenderContext, toolName string, args []string) string {
|
|
|
- if ctx.IsNested {
|
|
|
- return makeNestedHeader(ctx, toolName, args)
|
|
|
- }
|
|
|
- s := ctx.Styles
|
|
|
- var icon string
|
|
|
- if ctx.Result.ToolCallID != "" {
|
|
|
- if ctx.Result.IsError {
|
|
|
- icon = s.Tool.IconError.Render()
|
|
|
- } else {
|
|
|
- icon = s.Tool.IconSuccess.Render()
|
|
|
- }
|
|
|
- } else if ctx.Cancelled {
|
|
|
- icon = s.Tool.IconCancelled.Render()
|
|
|
- } else {
|
|
|
- icon = s.Tool.IconPending.Render()
|
|
|
- }
|
|
|
- tool := s.Tool.NameNormal.Render(toolName)
|
|
|
- prefix := fmt.Sprintf("%s %s ", icon, tool)
|
|
|
- return prefix + renderParamList(ctx, false, ctx.TextWidth()-lipgloss.Width(prefix), args...)
|
|
|
-}
|
|
|
-
|
|
|
-func makeNestedHeader(ctx *RenderContext, toolName string, args []string) string {
|
|
|
- s := ctx.Styles
|
|
|
- var icon string
|
|
|
- if ctx.Result.ToolCallID != "" {
|
|
|
- if ctx.Result.IsError {
|
|
|
- icon = s.Tool.IconError.Render()
|
|
|
- } else {
|
|
|
- icon = s.Tool.IconSuccess.Render()
|
|
|
- }
|
|
|
- } else if ctx.Cancelled {
|
|
|
- icon = s.Tool.IconCancelled.Render()
|
|
|
- } else {
|
|
|
- icon = s.Tool.IconPending.Render()
|
|
|
- }
|
|
|
- tool := s.Tool.NameNested.Render(toolName)
|
|
|
- prefix := fmt.Sprintf("%s %s ", icon, tool)
|
|
|
- return prefix + renderParamList(ctx, true, ctx.TextWidth()-lipgloss.Width(prefix), args...)
|
|
|
-}
|
|
|
-
|
|
|
-func renderParamList(ctx *RenderContext, nested bool, paramsWidth int, params ...string) string {
|
|
|
- s := ctx.Styles
|
|
|
- if len(params) == 0 {
|
|
|
- return ""
|
|
|
- }
|
|
|
- mainParam := params[0]
|
|
|
- if paramsWidth >= 0 && lipgloss.Width(mainParam) > paramsWidth {
|
|
|
- mainParam = ansi.Truncate(mainParam, paramsWidth, "…")
|
|
|
- }
|
|
|
-
|
|
|
- if len(params) == 1 {
|
|
|
- return s.Tool.ParamMain.Render(mainParam)
|
|
|
- }
|
|
|
- otherParams := params[1:]
|
|
|
- if len(otherParams)%2 != 0 {
|
|
|
- otherParams = append(otherParams, "")
|
|
|
- }
|
|
|
- parts := make([]string, 0, len(otherParams)/2)
|
|
|
- for i := 0; i < len(otherParams); i += 2 {
|
|
|
- key := otherParams[i]
|
|
|
- value := otherParams[i+1]
|
|
|
- if value == "" {
|
|
|
- continue
|
|
|
- }
|
|
|
- parts = append(parts, fmt.Sprintf("%s=%s", key, value))
|
|
|
- }
|
|
|
-
|
|
|
- partsRendered := strings.Join(parts, ", ")
|
|
|
- remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3
|
|
|
- if remainingWidth < 30 {
|
|
|
- return s.Tool.ParamMain.Render(mainParam)
|
|
|
- }
|
|
|
-
|
|
|
- if len(parts) > 0 {
|
|
|
- mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
|
|
|
- }
|
|
|
-
|
|
|
- return s.Tool.ParamMain.Render(ansi.Truncate(mainParam, paramsWidth, "…"))
|
|
|
-}
|
|
|
-
|
|
|
-func earlyState(ctx *RenderContext, header string) (string, bool) {
|
|
|
- s := ctx.Styles
|
|
|
- message := ""
|
|
|
- switch {
|
|
|
- case ctx.Result.IsError:
|
|
|
- message = renderToolError(ctx)
|
|
|
- case ctx.Cancelled:
|
|
|
- message = s.Tool.StateCancelled.Render("Canceled.")
|
|
|
- case ctx.Result.ToolCallID == "":
|
|
|
- message = s.Tool.StateWaiting.Render("Waiting for tool response...")
|
|
|
- default:
|
|
|
- return "", false
|
|
|
- }
|
|
|
-
|
|
|
- message = s.Tool.BodyPadding.Render(message)
|
|
|
- return lipgloss.JoinVertical(lipgloss.Left, header, "", message), true
|
|
|
-}
|
|
|
-
|
|
|
-func renderToolError(ctx *RenderContext) string {
|
|
|
- s := ctx.Styles
|
|
|
- errTag := s.Tool.ErrorTag.Render("ERROR")
|
|
|
- msg := ctx.Result.Content
|
|
|
- if msg == "" {
|
|
|
- msg = "An error occurred"
|
|
|
- }
|
|
|
- truncated := ansi.Truncate(msg, ctx.TextWidth()-3-lipgloss.Width(errTag), "…")
|
|
|
- return errTag + " " + s.Tool.ErrorMessage.Render(truncated)
|
|
|
-}
|
|
|
-
|
|
|
-func joinHeaderBody(ctx *RenderContext, header, body string) string {
|
|
|
- s := ctx.Styles
|
|
|
- if body == "" {
|
|
|
- return header
|
|
|
- }
|
|
|
- body = s.Tool.BodyPadding.Render(body)
|
|
|
- return lipgloss.JoinVertical(lipgloss.Left, header, "", body)
|
|
|
-}
|
|
|
-
|
|
|
-func renderWithParams(ctx *RenderContext, toolName string, args []string, contentRenderer func() string) string {
|
|
|
- header := makeHeader(ctx, toolName, args)
|
|
|
- if ctx.IsNested {
|
|
|
- return header
|
|
|
- }
|
|
|
- if res, done := earlyState(ctx, header); done {
|
|
|
- return res
|
|
|
- }
|
|
|
- body := contentRenderer()
|
|
|
- return joinHeaderBody(ctx, header, body)
|
|
|
-}
|
|
|
-
|
|
|
-func renderError(ctx *RenderContext, message string) string {
|
|
|
- s := ctx.Styles
|
|
|
- header := makeHeader(ctx, prettifyToolName(ctx.Call.Name), []string{})
|
|
|
- errorTag := s.Tool.ErrorTag.Render("ERROR")
|
|
|
- message = s.Tool.ErrorMessage.Render(ctx.Fit(message, ctx.TextWidth()-3-lipgloss.Width(errorTag)))
|
|
|
- return joinHeaderBody(ctx, header, errorTag+" "+message)
|
|
|
-}
|
|
|
-
|
|
|
-func renderPlainContent(ctx *RenderContext, content string) string {
|
|
|
- s := ctx.Styles
|
|
|
- content = strings.ReplaceAll(content, "\r\n", "\n")
|
|
|
- content = strings.ReplaceAll(content, "\t", " ")
|
|
|
- content = strings.TrimSpace(content)
|
|
|
- lines := strings.Split(content, "\n")
|
|
|
-
|
|
|
- width := ctx.TextWidth() - 2
|
|
|
- var out []string
|
|
|
- for i, ln := range lines {
|
|
|
- if i >= responseContextHeight {
|
|
|
- break
|
|
|
- }
|
|
|
- ln = ansiext.Escape(ln)
|
|
|
- ln = " " + ln
|
|
|
- if len(ln) > width {
|
|
|
- ln = ctx.Fit(ln, width)
|
|
|
- }
|
|
|
- out = append(out, s.Tool.ContentLine.Width(width).Render(ln))
|
|
|
- }
|
|
|
-
|
|
|
- if len(lines) > responseContextHeight {
|
|
|
- out = append(out, s.Tool.ContentTruncation.Width(width).Render(fmt.Sprintf("… (%d lines)", len(lines)-responseContextHeight)))
|
|
|
- }
|
|
|
-
|
|
|
- return strings.Join(out, "\n")
|
|
|
-}
|
|
|
-
|
|
|
-func renderMarkdownContent(ctx *RenderContext, content string) string {
|
|
|
- s := ctx.Styles
|
|
|
- content = strings.ReplaceAll(content, "\r\n", "\n")
|
|
|
- content = strings.ReplaceAll(content, "\t", " ")
|
|
|
- content = strings.TrimSpace(content)
|
|
|
-
|
|
|
- width := ctx.TextWidth() - 2
|
|
|
- width = min(width, 120)
|
|
|
-
|
|
|
- renderer := common.PlainMarkdownRenderer(width)
|
|
|
- rendered, err := renderer.Render(content)
|
|
|
- if err != nil {
|
|
|
- return renderPlainContent(ctx, content)
|
|
|
- }
|
|
|
-
|
|
|
- lines := strings.Split(rendered, "\n")
|
|
|
-
|
|
|
- var out []string
|
|
|
- for i, ln := range lines {
|
|
|
- if i >= responseContextHeight {
|
|
|
- break
|
|
|
- }
|
|
|
- out = append(out, ln)
|
|
|
- }
|
|
|
-
|
|
|
- style := s.Tool.ContentLine
|
|
|
- if len(lines) > responseContextHeight {
|
|
|
- out = append(out, s.Tool.ContentTruncation.
|
|
|
- Width(width-2).
|
|
|
- Render(fmt.Sprintf("… (%d lines)", len(lines)-responseContextHeight)))
|
|
|
- }
|
|
|
-
|
|
|
- return style.Render(strings.Join(out, "\n"))
|
|
|
-}
|
|
|
-
|
|
|
-func renderCodeContent(ctx *RenderContext, path, content string, offset int) string {
|
|
|
- s := ctx.Styles
|
|
|
- content = strings.ReplaceAll(content, "\r\n", "\n")
|
|
|
- content = strings.ReplaceAll(content, "\t", " ")
|
|
|
- truncated := truncateHeight(content, responseContextHeight)
|
|
|
-
|
|
|
- lines := strings.Split(truncated, "\n")
|
|
|
- for i, ln := range lines {
|
|
|
- lines[i] = ansiext.Escape(ln)
|
|
|
- }
|
|
|
-
|
|
|
- bg := s.Tool.ContentCodeBg
|
|
|
- highlighted, _ := common.SyntaxHighlight(ctx.Styles, strings.Join(lines, "\n"), path, bg)
|
|
|
- lines = strings.Split(highlighted, "\n")
|
|
|
-
|
|
|
- width := ctx.TextWidth() - 2
|
|
|
- gutterWidth := getDigits(offset+len(lines)) + 1
|
|
|
-
|
|
|
- var out []string
|
|
|
- for i, ln := range lines {
|
|
|
- lineNum := fmt.Sprintf("%*d", gutterWidth, offset+i+1)
|
|
|
- gutter := s.Subtle.Render(lineNum + " ")
|
|
|
- ln = " " + ln
|
|
|
- if lipgloss.Width(gutter+ln) > width {
|
|
|
- ln = ctx.Fit(ln, width-lipgloss.Width(gutter))
|
|
|
- }
|
|
|
- out = append(out, s.Tool.ContentCodeLine.Width(width).Render(gutter+ln))
|
|
|
- }
|
|
|
-
|
|
|
- contentLines := strings.Split(content, "\n")
|
|
|
- if len(contentLines) > responseContextHeight {
|
|
|
- out = append(out, s.Tool.ContentTruncation.Width(width).Render(fmt.Sprintf("… (%d lines)", len(contentLines)-responseContextHeight)))
|
|
|
- }
|
|
|
-
|
|
|
- return strings.Join(out, "\n")
|
|
|
-}
|
|
|
-
|
|
|
-func getDigits(n int) int {
|
|
|
- if n == 0 {
|
|
|
- return 1
|
|
|
- }
|
|
|
- if n < 0 {
|
|
|
- n = -n
|
|
|
- }
|
|
|
-
|
|
|
- digits := 0
|
|
|
- for n > 0 {
|
|
|
- n /= 10
|
|
|
- digits++
|
|
|
- }
|
|
|
-
|
|
|
- return digits
|
|
|
-}
|
|
|
-
|
|
|
-func truncateHeight(content string, maxLines int) string {
|
|
|
- lines := strings.Split(content, "\n")
|
|
|
- if len(lines) <= maxLines {
|
|
|
- return content
|
|
|
- }
|
|
|
- return strings.Join(lines[:maxLines], "\n")
|
|
|
-}
|
|
|
-
|
|
|
-func prettifyToolName(name string) string {
|
|
|
- switch name {
|
|
|
- case "agent":
|
|
|
- return "Agent"
|
|
|
- case "bash":
|
|
|
- return "Bash"
|
|
|
- case "job_output":
|
|
|
- return "Job: Output"
|
|
|
- case "job_kill":
|
|
|
- return "Job: Kill"
|
|
|
- case "download":
|
|
|
- return "Download"
|
|
|
- case "edit":
|
|
|
- return "Edit"
|
|
|
- case "multiedit":
|
|
|
- return "Multi-Edit"
|
|
|
- case "fetch":
|
|
|
- return "Fetch"
|
|
|
- case "agentic_fetch":
|
|
|
- return "Agentic Fetch"
|
|
|
- case "web_fetch":
|
|
|
- return "Fetching"
|
|
|
- case "glob":
|
|
|
- return "Glob"
|
|
|
- case "grep":
|
|
|
- return "Grep"
|
|
|
- case "ls":
|
|
|
- return "List"
|
|
|
- case "sourcegraph":
|
|
|
- return "Sourcegraph"
|
|
|
- case "view":
|
|
|
- return "View"
|
|
|
- case "write":
|
|
|
- return "Write"
|
|
|
- case "lsp_references":
|
|
|
- return "Find References"
|
|
|
- case "lsp_diagnostics":
|
|
|
- return "Diagnostics"
|
|
|
- default:
|
|
|
- parts := strings.Split(name, "_")
|
|
|
- for i := range parts {
|
|
|
- if len(parts[i]) > 0 {
|
|
|
- parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
|
|
|
- }
|
|
|
- }
|
|
|
- return strings.Join(parts, " ")
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// Tool-specific renderers
|
|
|
-
|
|
|
-func renderGeneric(ctx *RenderContext) string {
|
|
|
- return renderWithParams(ctx, prettifyToolName(ctx.Call.Name), []string{ctx.Call.Input}, func() string {
|
|
|
- return renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderView(ctx *RenderContext) string {
|
|
|
- var params tools.ViewParams
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
|
|
|
- return renderError(ctx, "Invalid view parameters")
|
|
|
- }
|
|
|
-
|
|
|
- file := fsext.PrettyPath(params.FilePath)
|
|
|
- args := newParamBuilder().
|
|
|
- addMain(file).
|
|
|
- addKeyValue("limit", formatNonZero(params.Limit)).
|
|
|
- addKeyValue("offset", formatNonZero(params.Offset)).
|
|
|
- build()
|
|
|
-
|
|
|
- return renderWithParams(ctx, "View", args, func() string {
|
|
|
- var meta tools.ViewResponseMetadata
|
|
|
- if err := unmarshalParams(ctx.Result.Metadata, &meta); err != nil {
|
|
|
- return renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- }
|
|
|
- return renderCodeContent(ctx, meta.FilePath, meta.Content, params.Offset)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderEdit(ctx *RenderContext) string {
|
|
|
- s := ctx.Styles
|
|
|
- var params tools.EditParams
|
|
|
- var args []string
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil {
|
|
|
- file := fsext.PrettyPath(params.FilePath)
|
|
|
- args = newParamBuilder().addMain(file).build()
|
|
|
- }
|
|
|
-
|
|
|
- return renderWithParams(ctx, "Edit", args, func() string {
|
|
|
- var meta tools.EditResponseMetadata
|
|
|
- if err := unmarshalParams(ctx.Result.Metadata, &meta); err != nil {
|
|
|
- return renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- }
|
|
|
-
|
|
|
- formatter := common.DiffFormatter(ctx.Styles).
|
|
|
- Before(fsext.PrettyPath(params.FilePath), meta.OldContent).
|
|
|
- After(fsext.PrettyPath(params.FilePath), meta.NewContent).
|
|
|
- Width(ctx.TextWidth() - 2)
|
|
|
- if ctx.TextWidth() > 120 {
|
|
|
- formatter = formatter.Split()
|
|
|
- }
|
|
|
- formatted := formatter.String()
|
|
|
- if lipgloss.Height(formatted) > responseContextHeight {
|
|
|
- contentLines := strings.Split(formatted, "\n")
|
|
|
- truncateMessage := s.Tool.DiffTruncation.
|
|
|
- Width(ctx.TextWidth() - 2).
|
|
|
- Render(fmt.Sprintf("… (%d lines)", len(contentLines)-responseContextHeight))
|
|
|
- formatted = strings.Join(contentLines[:responseContextHeight], "\n") + "\n" + truncateMessage
|
|
|
- }
|
|
|
- return formatted
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderMultiEdit(ctx *RenderContext) string {
|
|
|
- s := ctx.Styles
|
|
|
- var params tools.MultiEditParams
|
|
|
- var args []string
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil {
|
|
|
- file := fsext.PrettyPath(params.FilePath)
|
|
|
- args = newParamBuilder().
|
|
|
- addMain(file).
|
|
|
- addKeyValue("edits", fmt.Sprintf("%d", len(params.Edits))).
|
|
|
- build()
|
|
|
- }
|
|
|
-
|
|
|
- return renderWithParams(ctx, "Multi-Edit", args, func() string {
|
|
|
- var meta tools.MultiEditResponseMetadata
|
|
|
- if err := unmarshalParams(ctx.Result.Metadata, &meta); err != nil {
|
|
|
- return renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- }
|
|
|
-
|
|
|
- formatter := common.DiffFormatter(ctx.Styles).
|
|
|
- Before(fsext.PrettyPath(params.FilePath), meta.OldContent).
|
|
|
- After(fsext.PrettyPath(params.FilePath), meta.NewContent).
|
|
|
- Width(ctx.TextWidth() - 2)
|
|
|
- if ctx.TextWidth() > 120 {
|
|
|
- formatter = formatter.Split()
|
|
|
- }
|
|
|
- formatted := formatter.String()
|
|
|
- if lipgloss.Height(formatted) > responseContextHeight {
|
|
|
- contentLines := strings.Split(formatted, "\n")
|
|
|
- truncateMessage := s.Tool.DiffTruncation.
|
|
|
- Width(ctx.TextWidth() - 2).
|
|
|
- Render(fmt.Sprintf("… (%d lines)", len(contentLines)-responseContextHeight))
|
|
|
- formatted = strings.Join(contentLines[:responseContextHeight], "\n") + "\n" + truncateMessage
|
|
|
- }
|
|
|
-
|
|
|
- // Add note about failed edits if any.
|
|
|
- if len(meta.EditsFailed) > 0 {
|
|
|
- noteTag := s.Tool.NoteTag.Render("NOTE")
|
|
|
- noteMsg := s.Tool.NoteMessage.Render(
|
|
|
- fmt.Sprintf("%d of %d edits failed", len(meta.EditsFailed), len(params.Edits)))
|
|
|
- formatted = formatted + "\n\n" + noteTag + " " + noteMsg
|
|
|
- }
|
|
|
-
|
|
|
- return formatted
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderWrite(ctx *RenderContext) string {
|
|
|
- var params tools.WriteParams
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
|
|
|
- return renderError(ctx, "Invalid write parameters")
|
|
|
- }
|
|
|
-
|
|
|
- file := fsext.PrettyPath(params.FilePath)
|
|
|
- args := newParamBuilder().addMain(file).build()
|
|
|
-
|
|
|
- return renderWithParams(ctx, "Write", args, func() string {
|
|
|
- return renderCodeContent(ctx, params.FilePath, params.Content, 0)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderBash(ctx *RenderContext) string {
|
|
|
- var params tools.BashParams
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
|
|
|
- return renderError(ctx, "Invalid bash parameters")
|
|
|
- }
|
|
|
-
|
|
|
- cmd := strings.ReplaceAll(params.Command, "\n", " ")
|
|
|
- cmd = strings.ReplaceAll(cmd, "\t", " ")
|
|
|
- args := newParamBuilder().
|
|
|
- addMain(cmd).
|
|
|
- addFlag("background", params.RunInBackground).
|
|
|
- build()
|
|
|
-
|
|
|
- if ctx.Call.Finished {
|
|
|
- var meta tools.BashResponseMetadata
|
|
|
- _ = unmarshalParams(ctx.Result.Metadata, &meta)
|
|
|
- if meta.Background {
|
|
|
- description := cmp.Or(meta.Description, params.Command)
|
|
|
- width := ctx.TextWidth()
|
|
|
- if ctx.IsNested {
|
|
|
- width -= 4
|
|
|
- }
|
|
|
- header := makeJobHeader(ctx, "Start", fmt.Sprintf("PID %s", meta.ShellID), description, width)
|
|
|
- if ctx.IsNested {
|
|
|
- return header
|
|
|
- }
|
|
|
- if res, done := earlyState(ctx, header); done {
|
|
|
- return res
|
|
|
- }
|
|
|
- content := "Command: " + params.Command + "\n" + ctx.Result.Content
|
|
|
- body := renderPlainContent(ctx, content)
|
|
|
- return joinHeaderBody(ctx, header, body)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return renderWithParams(ctx, "Bash", args, func() string {
|
|
|
- var meta tools.BashResponseMetadata
|
|
|
- if err := unmarshalParams(ctx.Result.Metadata, &meta); err != nil {
|
|
|
- return renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- }
|
|
|
- if meta.Output == "" && ctx.Result.Content != tools.BashNoOutput {
|
|
|
- meta.Output = ctx.Result.Content
|
|
|
- }
|
|
|
-
|
|
|
- if meta.Output == "" {
|
|
|
- return ""
|
|
|
- }
|
|
|
- return renderPlainContent(ctx, meta.Output)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func makeJobHeader(ctx *RenderContext, action, pid, description string, width int) string {
|
|
|
- s := ctx.Styles
|
|
|
- icon := s.Tool.JobIconPending.Render(styles.ToolPending)
|
|
|
- if ctx.Result.ToolCallID != "" {
|
|
|
- if ctx.Result.IsError {
|
|
|
- icon = s.Tool.JobIconError.Render(styles.ToolError)
|
|
|
- } else {
|
|
|
- icon = s.Tool.JobIconSuccess.Render(styles.ToolSuccess)
|
|
|
- }
|
|
|
- } else if ctx.Cancelled {
|
|
|
- icon = s.Muted.Render(styles.ToolPending)
|
|
|
- }
|
|
|
-
|
|
|
- toolName := s.Tool.JobToolName.Render("Bash")
|
|
|
- actionPart := s.Tool.JobAction.Render(action)
|
|
|
- pidPart := s.Tool.JobPID.Render(pid)
|
|
|
-
|
|
|
- prefix := fmt.Sprintf("%s %s %s %s ", icon, toolName, actionPart, pidPart)
|
|
|
- remainingWidth := width - lipgloss.Width(prefix)
|
|
|
-
|
|
|
- descDisplay := ansi.Truncate(description, remainingWidth, "…")
|
|
|
- descDisplay = s.Tool.JobDescription.Render(descDisplay)
|
|
|
-
|
|
|
- return prefix + descDisplay
|
|
|
-}
|
|
|
-
|
|
|
-func renderJobOutput(ctx *RenderContext) string {
|
|
|
- var params tools.JobOutputParams
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
|
|
|
- return renderError(ctx, "Invalid job output parameters")
|
|
|
- }
|
|
|
-
|
|
|
- width := ctx.TextWidth()
|
|
|
- if ctx.IsNested {
|
|
|
- width -= 4
|
|
|
- }
|
|
|
-
|
|
|
- var meta tools.JobOutputResponseMetadata
|
|
|
- _ = unmarshalParams(ctx.Result.Metadata, &meta)
|
|
|
- description := cmp.Or(meta.Description, meta.Command)
|
|
|
-
|
|
|
- header := makeJobHeader(ctx, "Output", fmt.Sprintf("PID %s", params.ShellID), description, width)
|
|
|
- if ctx.IsNested {
|
|
|
- return header
|
|
|
- }
|
|
|
- if res, done := earlyState(ctx, header); done {
|
|
|
- return res
|
|
|
- }
|
|
|
- body := renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- return joinHeaderBody(ctx, header, body)
|
|
|
-}
|
|
|
-
|
|
|
-func renderJobKill(ctx *RenderContext) string {
|
|
|
- var params tools.JobKillParams
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
|
|
|
- return renderError(ctx, "Invalid job kill parameters")
|
|
|
- }
|
|
|
-
|
|
|
- width := ctx.TextWidth()
|
|
|
- if ctx.IsNested {
|
|
|
- width -= 4
|
|
|
- }
|
|
|
-
|
|
|
- var meta tools.JobKillResponseMetadata
|
|
|
- _ = unmarshalParams(ctx.Result.Metadata, &meta)
|
|
|
- description := cmp.Or(meta.Description, meta.Command)
|
|
|
-
|
|
|
- header := makeJobHeader(ctx, "Kill", fmt.Sprintf("PID %s", params.ShellID), description, width)
|
|
|
- if ctx.IsNested {
|
|
|
- return header
|
|
|
- }
|
|
|
- if res, done := earlyState(ctx, header); done {
|
|
|
- return res
|
|
|
- }
|
|
|
- body := renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- return joinHeaderBody(ctx, header, body)
|
|
|
-}
|
|
|
-
|
|
|
-func renderSimpleFetch(ctx *RenderContext) string {
|
|
|
- var params tools.FetchParams
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
|
|
|
- return renderError(ctx, "Invalid fetch parameters")
|
|
|
- }
|
|
|
-
|
|
|
- args := newParamBuilder().
|
|
|
- addMain(params.URL).
|
|
|
- addKeyValue("format", params.Format).
|
|
|
- addKeyValue("timeout", formatNonZero(params.Timeout)).
|
|
|
- build()
|
|
|
-
|
|
|
- return renderWithParams(ctx, "Fetch", args, func() string {
|
|
|
- path := "file." + params.Format
|
|
|
- return renderCodeContent(ctx, path, ctx.Result.Content, 0)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderAgenticFetch(ctx *RenderContext) string {
|
|
|
- // TODO: Implement nested tool call rendering with tree.
|
|
|
- return renderGeneric(ctx)
|
|
|
-}
|
|
|
-
|
|
|
-func renderWebFetch(ctx *RenderContext) string {
|
|
|
- var params tools.WebFetchParams
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
|
|
|
- return renderError(ctx, "Invalid web fetch parameters")
|
|
|
- }
|
|
|
-
|
|
|
- args := newParamBuilder().addMain(params.URL).build()
|
|
|
-
|
|
|
- return renderWithParams(ctx, "Fetching", args, func() string {
|
|
|
- return renderMarkdownContent(ctx, ctx.Result.Content)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderDownload(ctx *RenderContext) string {
|
|
|
- var params tools.DownloadParams
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
|
|
|
- return renderError(ctx, "Invalid download parameters")
|
|
|
- }
|
|
|
-
|
|
|
- args := newParamBuilder().
|
|
|
- addMain(params.URL).
|
|
|
- addKeyValue("file", fsext.PrettyPath(params.FilePath)).
|
|
|
- addKeyValue("timeout", formatNonZero(params.Timeout)).
|
|
|
- build()
|
|
|
-
|
|
|
- return renderWithParams(ctx, "Download", args, func() string {
|
|
|
- return renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderGlob(ctx *RenderContext) string {
|
|
|
- var params tools.GlobParams
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
|
|
|
- return renderError(ctx, "Invalid glob parameters")
|
|
|
- }
|
|
|
-
|
|
|
- args := newParamBuilder().
|
|
|
- addMain(params.Pattern).
|
|
|
- addKeyValue("path", params.Path).
|
|
|
- build()
|
|
|
-
|
|
|
- return renderWithParams(ctx, "Glob", args, func() string {
|
|
|
- return renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderGrep(ctx *RenderContext) string {
|
|
|
- var params tools.GrepParams
|
|
|
- var args []string
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil {
|
|
|
- args = newParamBuilder().
|
|
|
- addMain(params.Pattern).
|
|
|
- addKeyValue("path", params.Path).
|
|
|
- addKeyValue("include", params.Include).
|
|
|
- addFlag("literal", params.LiteralText).
|
|
|
- build()
|
|
|
- }
|
|
|
-
|
|
|
- return renderWithParams(ctx, "Grep", args, func() string {
|
|
|
- return renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderLS(ctx *RenderContext) string {
|
|
|
- var params tools.LSParams
|
|
|
- path := cmp.Or(params.Path, ".")
|
|
|
- args := newParamBuilder().addMain(path).build()
|
|
|
-
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil && params.Path != "" {
|
|
|
- args = newParamBuilder().addMain(params.Path).build()
|
|
|
- }
|
|
|
-
|
|
|
- return renderWithParams(ctx, "List", args, func() string {
|
|
|
- return renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderSourcegraph(ctx *RenderContext) string {
|
|
|
- var params tools.SourcegraphParams
|
|
|
- if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
|
|
|
- return renderError(ctx, "Invalid sourcegraph parameters")
|
|
|
- }
|
|
|
-
|
|
|
- args := newParamBuilder().
|
|
|
- addMain(params.Query).
|
|
|
- addKeyValue("count", formatNonZero(params.Count)).
|
|
|
- addKeyValue("context", formatNonZero(params.ContextWindow)).
|
|
|
- build()
|
|
|
-
|
|
|
- return renderWithParams(ctx, "Sourcegraph", args, func() string {
|
|
|
- return renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderDiagnostics(ctx *RenderContext) string {
|
|
|
- args := newParamBuilder().addMain("project").build()
|
|
|
-
|
|
|
- return renderWithParams(ctx, "Diagnostics", args, func() string {
|
|
|
- return renderPlainContent(ctx, ctx.Result.Content)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func renderAgent(ctx *RenderContext) string {
|
|
|
- s := ctx.Styles
|
|
|
- var params agent.AgentParams
|
|
|
- unmarshalParams(ctx.Call.Input, ¶ms)
|
|
|
-
|
|
|
- prompt := params.Prompt
|
|
|
- prompt = strings.ReplaceAll(prompt, "\n", " ")
|
|
|
-
|
|
|
- header := makeHeader(ctx, "Agent", []string{})
|
|
|
- if res, done := earlyState(ctx, header); ctx.Cancelled && done {
|
|
|
- return res
|
|
|
- }
|
|
|
- taskTag := s.Tool.AgentTaskTag.Render("Task")
|
|
|
- remainingWidth := ctx.TextWidth() - lipgloss.Width(header) - lipgloss.Width(taskTag) - 2
|
|
|
- remainingWidth = min(remainingWidth, 120-lipgloss.Width(taskTag)-2)
|
|
|
- prompt = s.Tool.AgentPrompt.Width(remainingWidth).Render(prompt)
|
|
|
- header = lipgloss.JoinVertical(
|
|
|
- lipgloss.Left,
|
|
|
- header,
|
|
|
- "",
|
|
|
- lipgloss.JoinHorizontal(
|
|
|
- lipgloss.Left,
|
|
|
- taskTag,
|
|
|
- " ",
|
|
|
- prompt,
|
|
|
- ),
|
|
|
- )
|
|
|
- childTools := tree.Root(header)
|
|
|
-
|
|
|
- // TODO: Render nested tool calls when available.
|
|
|
-
|
|
|
- parts := []string{
|
|
|
- childTools.Enumerator(roundedEnumeratorWithWidth(2, lipgloss.Width(taskTag)-5)).String(),
|
|
|
- }
|
|
|
-
|
|
|
- if ctx.Result.ToolCallID == "" {
|
|
|
- // Pending state - would show animation in TUI.
|
|
|
- parts = append(parts, "", s.Subtle.Render("Working..."))
|
|
|
- }
|
|
|
-
|
|
|
- header = lipgloss.JoinVertical(
|
|
|
- lipgloss.Left,
|
|
|
- parts...,
|
|
|
- )
|
|
|
-
|
|
|
- if ctx.Result.ToolCallID == "" {
|
|
|
- return header
|
|
|
- }
|
|
|
-
|
|
|
- body := renderMarkdownContent(ctx, ctx.Result.Content)
|
|
|
- return joinHeaderBody(ctx, header, body)
|
|
|
-}
|
|
|
-
|
|
|
-func roundedEnumeratorWithWidth(width int, offset int) func(tree.Children, int) string {
|
|
|
- return func(children tree.Children, i int) string {
|
|
|
- if children.Length()-1 == i {
|
|
|
- return strings.Repeat(" ", offset) + "└" + strings.Repeat("─", width-1) + " "
|
|
|
- }
|
|
|
- return strings.Repeat(" ", offset) + "├" + strings.Repeat("─", width-1) + " "
|
|
|
- }
|
|
|
-}
|