9 "charm.land/lipgloss/v2"
10 "charm.land/lipgloss/v2/tree"
11 "github.com/charmbracelet/crush/internal/agent"
12 "github.com/charmbracelet/crush/internal/agent/tools"
13 "github.com/charmbracelet/crush/internal/ansiext"
14 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/message"
16 "github.com/charmbracelet/crush/internal/ui/common"
17 "github.com/charmbracelet/crush/internal/ui/styles"
18 "github.com/charmbracelet/x/ansi"
21// responseContextHeight limits the number of lines displayed in tool output.
22const responseContextHeight = 10
24// RenderContext provides the context needed for rendering a tool call.
25type RenderContext struct {
27 Result message.ToolResult
34// TextWidth returns the available width for content accounting for borders.
35func (rc *RenderContext) TextWidth() int {
42// Fit truncates content to fit within the specified width with ellipsis.
43func (rc *RenderContext) Fit(content string, width int) string {
44 lineStyle := rc.Styles.Muted
45 dots := lineStyle.Render("…")
46 return ansi.Truncate(content, width, dots)
49// Render renders a tool call using the appropriate renderer based on tool name.
50func Render(ctx *RenderContext) string {
51 switch ctx.Call.Name {
52 case tools.ViewToolName:
53 return renderView(ctx)
54 case tools.EditToolName:
55 return renderEdit(ctx)
56 case tools.MultiEditToolName:
57 return renderMultiEdit(ctx)
58 case tools.WriteToolName:
59 return renderWrite(ctx)
60 case tools.BashToolName:
61 return renderBash(ctx)
62 case tools.JobOutputToolName:
63 return renderJobOutput(ctx)
64 case tools.JobKillToolName:
65 return renderJobKill(ctx)
66 case tools.FetchToolName:
67 return renderSimpleFetch(ctx)
68 case tools.AgenticFetchToolName:
69 return renderAgenticFetch(ctx)
70 case tools.WebFetchToolName:
71 return renderWebFetch(ctx)
72 case tools.DownloadToolName:
73 return renderDownload(ctx)
74 case tools.GlobToolName:
75 return renderGlob(ctx)
76 case tools.GrepToolName:
77 return renderGrep(ctx)
78 case tools.LSToolName:
80 case tools.SourcegraphToolName:
81 return renderSourcegraph(ctx)
82 case tools.DiagnosticsToolName:
83 return renderDiagnostics(ctx)
84 case agent.AgentToolName:
85 return renderAgent(ctx)
87 return renderGeneric(ctx)
93func unmarshalParams(input string, target any) error {
94 return json.Unmarshal([]byte(input), target)
97type paramBuilder struct {
101func newParamBuilder() *paramBuilder {
102 return ¶mBuilder{args: make([]string, 0)}
105func (pb *paramBuilder) addMain(value string) *paramBuilder {
107 pb.args = append(pb.args, value)
112func (pb *paramBuilder) addKeyValue(key, value string) *paramBuilder {
114 pb.args = append(pb.args, key, value)
119func (pb *paramBuilder) addFlag(key string, value bool) *paramBuilder {
121 pb.args = append(pb.args, key, "true")
126func (pb *paramBuilder) build() []string {
130func formatNonZero[T comparable](value T) string {
135 return fmt.Sprintf("%v", value)
138func makeHeader(ctx *RenderContext, toolName string, args []string) string {
140 return makeNestedHeader(ctx, toolName, args)
144 if ctx.Result.ToolCallID != "" {
145 if ctx.Result.IsError {
146 icon = s.Tool.IconError.Render()
148 icon = s.Tool.IconSuccess.Render()
150 } else if ctx.Cancelled {
151 icon = s.Tool.IconCancelled.Render()
153 icon = s.Tool.IconPending.Render()
155 tool := s.Tool.NameNormal.Render(toolName)
156 prefix := fmt.Sprintf("%s %s ", icon, tool)
157 return prefix + renderParamList(ctx, false, ctx.TextWidth()-lipgloss.Width(prefix), args...)
160func makeNestedHeader(ctx *RenderContext, toolName string, args []string) string {
163 if ctx.Result.ToolCallID != "" {
164 if ctx.Result.IsError {
165 icon = s.Tool.IconError.Render()
167 icon = s.Tool.IconSuccess.Render()
169 } else if ctx.Cancelled {
170 icon = s.Tool.IconCancelled.Render()
172 icon = s.Tool.IconPending.Render()
174 tool := s.Tool.NameNested.Render(toolName)
175 prefix := fmt.Sprintf("%s %s ", icon, tool)
176 return prefix + renderParamList(ctx, true, ctx.TextWidth()-lipgloss.Width(prefix), args...)
179func renderParamList(ctx *RenderContext, nested bool, paramsWidth int, params ...string) string {
181 if len(params) == 0 {
184 mainParam := params[0]
185 if paramsWidth >= 0 && lipgloss.Width(mainParam) > paramsWidth {
186 mainParam = ansi.Truncate(mainParam, paramsWidth, "…")
189 if len(params) == 1 {
190 return s.Tool.ParamMain.Render(mainParam)
192 otherParams := params[1:]
193 if len(otherParams)%2 != 0 {
194 otherParams = append(otherParams, "")
196 parts := make([]string, 0, len(otherParams)/2)
197 for i := 0; i < len(otherParams); i += 2 {
198 key := otherParams[i]
199 value := otherParams[i+1]
203 parts = append(parts, fmt.Sprintf("%s=%s", key, value))
206 partsRendered := strings.Join(parts, ", ")
207 remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3
208 if remainingWidth < 30 {
209 return s.Tool.ParamMain.Render(mainParam)
213 mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
216 return s.Tool.ParamMain.Render(ansi.Truncate(mainParam, paramsWidth, "…"))
219func earlyState(ctx *RenderContext, header string) (string, bool) {
223 case ctx.Result.IsError:
224 message = renderToolError(ctx)
226 message = s.Tool.StateCancelled.Render("Canceled.")
227 case ctx.Result.ToolCallID == "":
228 message = s.Tool.StateWaiting.Render("Waiting for tool response...")
233 message = s.Tool.BodyPadding.Render(message)
234 return lipgloss.JoinVertical(lipgloss.Left, header, "", message), true
237func renderToolError(ctx *RenderContext) string {
239 errTag := s.Tool.ErrorTag.Render("ERROR")
240 msg := ctx.Result.Content
242 msg = "An error occurred"
244 truncated := ansi.Truncate(msg, ctx.TextWidth()-3-lipgloss.Width(errTag), "…")
245 return errTag + " " + s.Tool.ErrorMessage.Render(truncated)
248func joinHeaderBody(ctx *RenderContext, header, body string) string {
253 body = s.Tool.BodyPadding.Render(body)
254 return lipgloss.JoinVertical(lipgloss.Left, header, "", body)
257func renderWithParams(ctx *RenderContext, toolName string, args []string, contentRenderer func() string) string {
258 header := makeHeader(ctx, toolName, args)
262 if res, done := earlyState(ctx, header); done {
265 body := contentRenderer()
266 return joinHeaderBody(ctx, header, body)
269func renderError(ctx *RenderContext, message string) string {
271 header := makeHeader(ctx, prettifyToolName(ctx.Call.Name), []string{})
272 errorTag := s.Tool.ErrorTag.Render("ERROR")
273 message = s.Tool.ErrorMessage.Render(ctx.Fit(message, ctx.TextWidth()-3-lipgloss.Width(errorTag)))
274 return joinHeaderBody(ctx, header, errorTag+" "+message)
277func renderPlainContent(ctx *RenderContext, content string) string {
279 content = strings.ReplaceAll(content, "\r\n", "\n")
280 content = strings.ReplaceAll(content, "\t", " ")
281 content = strings.TrimSpace(content)
282 lines := strings.Split(content, "\n")
284 width := ctx.TextWidth() - 2
286 for i, ln := range lines {
287 if i >= responseContextHeight {
290 ln = ansiext.Escape(ln)
293 ln = ctx.Fit(ln, width)
295 out = append(out, s.Tool.ContentLine.Width(width).Render(ln))
298 if len(lines) > responseContextHeight {
299 out = append(out, s.Tool.ContentTruncation.Width(width).Render(fmt.Sprintf("… (%d lines)", len(lines)-responseContextHeight)))
302 return strings.Join(out, "\n")
305func renderMarkdownContent(ctx *RenderContext, content string) string {
307 content = strings.ReplaceAll(content, "\r\n", "\n")
308 content = strings.ReplaceAll(content, "\t", " ")
309 content = strings.TrimSpace(content)
311 width := ctx.TextWidth() - 2
312 width = min(width, 120)
314 renderer := common.PlainMarkdownRenderer(width)
315 rendered, err := renderer.Render(content)
317 return renderPlainContent(ctx, content)
320 lines := strings.Split(rendered, "\n")
323 for i, ln := range lines {
324 if i >= responseContextHeight {
327 out = append(out, ln)
330 style := s.Tool.ContentLine
331 if len(lines) > responseContextHeight {
332 out = append(out, s.Tool.ContentTruncation.
334 Render(fmt.Sprintf("… (%d lines)", len(lines)-responseContextHeight)))
337 return style.Render(strings.Join(out, "\n"))
340func renderCodeContent(ctx *RenderContext, path, content string, offset int) string {
342 content = strings.ReplaceAll(content, "\r\n", "\n")
343 content = strings.ReplaceAll(content, "\t", " ")
344 truncated := truncateHeight(content, responseContextHeight)
346 lines := strings.Split(truncated, "\n")
347 for i, ln := range lines {
348 lines[i] = ansiext.Escape(ln)
351 bg := s.Tool.ContentCodeBg
352 highlighted, _ := common.SyntaxHighlight(ctx.Styles, strings.Join(lines, "\n"), path, bg)
353 lines = strings.Split(highlighted, "\n")
355 width := ctx.TextWidth() - 2
356 gutterWidth := getDigits(offset+len(lines)) + 1
359 for i, ln := range lines {
360 lineNum := fmt.Sprintf("%*d", gutterWidth, offset+i+1)
361 gutter := s.Subtle.Render(lineNum + " ")
363 if lipgloss.Width(gutter+ln) > width {
364 ln = ctx.Fit(ln, width-lipgloss.Width(gutter))
366 out = append(out, s.Tool.ContentCodeLine.Width(width).Render(gutter+ln))
369 contentLines := strings.Split(content, "\n")
370 if len(contentLines) > responseContextHeight {
371 out = append(out, s.Tool.ContentTruncation.Width(width).Render(fmt.Sprintf("… (%d lines)", len(contentLines)-responseContextHeight)))
374 return strings.Join(out, "\n")
377func getDigits(n int) int {
394func truncateHeight(content string, maxLines int) string {
395 lines := strings.Split(content, "\n")
396 if len(lines) <= maxLines {
399 return strings.Join(lines[:maxLines], "\n")
402func prettifyToolName(name string) string {
420 case "agentic_fetch":
421 return "Agentic Fetch"
436 case "lsp_references":
437 return "Find References"
438 case "lsp_diagnostics":
441 parts := strings.Split(name, "_")
442 for i := range parts {
443 if len(parts[i]) > 0 {
444 parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
447 return strings.Join(parts, " ")
451// Tool-specific renderers
453func renderGeneric(ctx *RenderContext) string {
454 return renderWithParams(ctx, prettifyToolName(ctx.Call.Name), []string{ctx.Call.Input}, func() string {
455 return renderPlainContent(ctx, ctx.Result.Content)
459func renderView(ctx *RenderContext) string {
460 var params tools.ViewParams
461 if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
462 return renderError(ctx, "Invalid view parameters")
465 file := fsext.PrettyPath(params.FilePath)
466 args := newParamBuilder().
468 addKeyValue("limit", formatNonZero(params.Limit)).
469 addKeyValue("offset", formatNonZero(params.Offset)).
472 return renderWithParams(ctx, "View", args, func() string {
473 var meta tools.ViewResponseMetadata
474 if err := unmarshalParams(ctx.Result.Metadata, &meta); err != nil {
475 return renderPlainContent(ctx, ctx.Result.Content)
477 return renderCodeContent(ctx, meta.FilePath, meta.Content, params.Offset)
481func renderEdit(ctx *RenderContext) string {
483 var params tools.EditParams
485 if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil {
486 file := fsext.PrettyPath(params.FilePath)
487 args = newParamBuilder().addMain(file).build()
490 return renderWithParams(ctx, "Edit", args, func() string {
491 var meta tools.EditResponseMetadata
492 if err := unmarshalParams(ctx.Result.Metadata, &meta); err != nil {
493 return renderPlainContent(ctx, ctx.Result.Content)
496 formatter := common.DiffFormatter(ctx.Styles).
497 Before(fsext.PrettyPath(params.FilePath), meta.OldContent).
498 After(fsext.PrettyPath(params.FilePath), meta.NewContent).
499 Width(ctx.TextWidth() - 2)
500 if ctx.TextWidth() > 120 {
501 formatter = formatter.Split()
503 formatted := formatter.String()
504 if lipgloss.Height(formatted) > responseContextHeight {
505 contentLines := strings.Split(formatted, "\n")
506 truncateMessage := s.Tool.DiffTruncation.
507 Width(ctx.TextWidth() - 2).
508 Render(fmt.Sprintf("… (%d lines)", len(contentLines)-responseContextHeight))
509 formatted = strings.Join(contentLines[:responseContextHeight], "\n") + "\n" + truncateMessage
515func renderMultiEdit(ctx *RenderContext) string {
517 var params tools.MultiEditParams
519 if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil {
520 file := fsext.PrettyPath(params.FilePath)
521 args = newParamBuilder().
523 addKeyValue("edits", fmt.Sprintf("%d", len(params.Edits))).
527 return renderWithParams(ctx, "Multi-Edit", args, func() string {
528 var meta tools.MultiEditResponseMetadata
529 if err := unmarshalParams(ctx.Result.Metadata, &meta); err != nil {
530 return renderPlainContent(ctx, ctx.Result.Content)
533 formatter := common.DiffFormatter(ctx.Styles).
534 Before(fsext.PrettyPath(params.FilePath), meta.OldContent).
535 After(fsext.PrettyPath(params.FilePath), meta.NewContent).
536 Width(ctx.TextWidth() - 2)
537 if ctx.TextWidth() > 120 {
538 formatter = formatter.Split()
540 formatted := formatter.String()
541 if lipgloss.Height(formatted) > responseContextHeight {
542 contentLines := strings.Split(formatted, "\n")
543 truncateMessage := s.Tool.DiffTruncation.
544 Width(ctx.TextWidth() - 2).
545 Render(fmt.Sprintf("… (%d lines)", len(contentLines)-responseContextHeight))
546 formatted = strings.Join(contentLines[:responseContextHeight], "\n") + "\n" + truncateMessage
549 // Add note about failed edits if any.
550 if len(meta.EditsFailed) > 0 {
551 noteTag := s.Tool.NoteTag.Render("NOTE")
552 noteMsg := s.Tool.NoteMessage.Render(
553 fmt.Sprintf("%d of %d edits failed", len(meta.EditsFailed), len(params.Edits)))
554 formatted = formatted + "\n\n" + noteTag + " " + noteMsg
561func renderWrite(ctx *RenderContext) string {
562 var params tools.WriteParams
563 if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
564 return renderError(ctx, "Invalid write parameters")
567 file := fsext.PrettyPath(params.FilePath)
568 args := newParamBuilder().addMain(file).build()
570 return renderWithParams(ctx, "Write", args, func() string {
571 return renderCodeContent(ctx, params.FilePath, params.Content, 0)
575func renderBash(ctx *RenderContext) string {
576 var params tools.BashParams
577 if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
578 return renderError(ctx, "Invalid bash parameters")
581 cmd := strings.ReplaceAll(params.Command, "\n", " ")
582 cmd = strings.ReplaceAll(cmd, "\t", " ")
583 args := newParamBuilder().
585 addFlag("background", params.RunInBackground).
588 if ctx.Call.Finished {
589 var meta tools.BashResponseMetadata
590 _ = unmarshalParams(ctx.Result.Metadata, &meta)
592 description := cmp.Or(meta.Description, params.Command)
593 width := ctx.TextWidth()
597 header := makeJobHeader(ctx, "Start", fmt.Sprintf("PID %s", meta.ShellID), description, width)
601 if res, done := earlyState(ctx, header); done {
604 content := "Command: " + params.Command + "\n" + ctx.Result.Content
605 body := renderPlainContent(ctx, content)
606 return joinHeaderBody(ctx, header, body)
610 return renderWithParams(ctx, "Bash", args, func() string {
611 var meta tools.BashResponseMetadata
612 if err := unmarshalParams(ctx.Result.Metadata, &meta); err != nil {
613 return renderPlainContent(ctx, ctx.Result.Content)
615 if meta.Output == "" && ctx.Result.Content != tools.BashNoOutput {
616 meta.Output = ctx.Result.Content
619 if meta.Output == "" {
622 return renderPlainContent(ctx, meta.Output)
626func makeJobHeader(ctx *RenderContext, action, pid, description string, width int) string {
628 icon := s.Tool.JobIconPending.Render(styles.ToolPending)
629 if ctx.Result.ToolCallID != "" {
630 if ctx.Result.IsError {
631 icon = s.Tool.JobIconError.Render(styles.ToolError)
633 icon = s.Tool.JobIconSuccess.Render(styles.ToolSuccess)
635 } else if ctx.Cancelled {
636 icon = s.Muted.Render(styles.ToolPending)
639 toolName := s.Tool.JobToolName.Render("Bash")
640 actionPart := s.Tool.JobAction.Render(action)
641 pidPart := s.Tool.JobPID.Render(pid)
643 prefix := fmt.Sprintf("%s %s %s %s ", icon, toolName, actionPart, pidPart)
644 remainingWidth := width - lipgloss.Width(prefix)
646 descDisplay := ansi.Truncate(description, remainingWidth, "…")
647 descDisplay = s.Tool.JobDescription.Render(descDisplay)
649 return prefix + descDisplay
652func renderJobOutput(ctx *RenderContext) string {
653 var params tools.JobOutputParams
654 if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
655 return renderError(ctx, "Invalid job output parameters")
658 width := ctx.TextWidth()
663 var meta tools.JobOutputResponseMetadata
664 _ = unmarshalParams(ctx.Result.Metadata, &meta)
665 description := cmp.Or(meta.Description, meta.Command)
667 header := makeJobHeader(ctx, "Output", fmt.Sprintf("PID %s", params.ShellID), description, width)
671 if res, done := earlyState(ctx, header); done {
674 body := renderPlainContent(ctx, ctx.Result.Content)
675 return joinHeaderBody(ctx, header, body)
678func renderJobKill(ctx *RenderContext) string {
679 var params tools.JobKillParams
680 if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
681 return renderError(ctx, "Invalid job kill parameters")
684 width := ctx.TextWidth()
689 var meta tools.JobKillResponseMetadata
690 _ = unmarshalParams(ctx.Result.Metadata, &meta)
691 description := cmp.Or(meta.Description, meta.Command)
693 header := makeJobHeader(ctx, "Kill", fmt.Sprintf("PID %s", params.ShellID), description, width)
697 if res, done := earlyState(ctx, header); done {
700 body := renderPlainContent(ctx, ctx.Result.Content)
701 return joinHeaderBody(ctx, header, body)
704func renderSimpleFetch(ctx *RenderContext) string {
705 var params tools.FetchParams
706 if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
707 return renderError(ctx, "Invalid fetch parameters")
710 args := newParamBuilder().
712 addKeyValue("format", params.Format).
713 addKeyValue("timeout", formatNonZero(params.Timeout)).
716 return renderWithParams(ctx, "Fetch", args, func() string {
717 path := "file." + params.Format
718 return renderCodeContent(ctx, path, ctx.Result.Content, 0)
722func renderAgenticFetch(ctx *RenderContext) string {
723 // TODO: Implement nested tool call rendering with tree.
724 return renderGeneric(ctx)
727func renderWebFetch(ctx *RenderContext) string {
728 var params tools.WebFetchParams
729 if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
730 return renderError(ctx, "Invalid web fetch parameters")
733 args := newParamBuilder().addMain(params.URL).build()
735 return renderWithParams(ctx, "Fetching", args, func() string {
736 return renderMarkdownContent(ctx, ctx.Result.Content)
740func renderDownload(ctx *RenderContext) string {
741 var params tools.DownloadParams
742 if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
743 return renderError(ctx, "Invalid download parameters")
746 args := newParamBuilder().
748 addKeyValue("file", fsext.PrettyPath(params.FilePath)).
749 addKeyValue("timeout", formatNonZero(params.Timeout)).
752 return renderWithParams(ctx, "Download", args, func() string {
753 return renderPlainContent(ctx, ctx.Result.Content)
757func renderGlob(ctx *RenderContext) string {
758 var params tools.GlobParams
759 if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
760 return renderError(ctx, "Invalid glob parameters")
763 args := newParamBuilder().
764 addMain(params.Pattern).
765 addKeyValue("path", params.Path).
768 return renderWithParams(ctx, "Glob", args, func() string {
769 return renderPlainContent(ctx, ctx.Result.Content)
773func renderGrep(ctx *RenderContext) string {
774 var params tools.GrepParams
776 if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil {
777 args = newParamBuilder().
778 addMain(params.Pattern).
779 addKeyValue("path", params.Path).
780 addKeyValue("include", params.Include).
781 addFlag("literal", params.LiteralText).
785 return renderWithParams(ctx, "Grep", args, func() string {
786 return renderPlainContent(ctx, ctx.Result.Content)
790func renderLS(ctx *RenderContext) string {
791 var params tools.LSParams
792 path := cmp.Or(params.Path, ".")
793 args := newParamBuilder().addMain(path).build()
795 if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil && params.Path != "" {
796 args = newParamBuilder().addMain(params.Path).build()
799 return renderWithParams(ctx, "List", args, func() string {
800 return renderPlainContent(ctx, ctx.Result.Content)
804func renderSourcegraph(ctx *RenderContext) string {
805 var params tools.SourcegraphParams
806 if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
807 return renderError(ctx, "Invalid sourcegraph parameters")
810 args := newParamBuilder().
811 addMain(params.Query).
812 addKeyValue("count", formatNonZero(params.Count)).
813 addKeyValue("context", formatNonZero(params.ContextWindow)).
816 return renderWithParams(ctx, "Sourcegraph", args, func() string {
817 return renderPlainContent(ctx, ctx.Result.Content)
821func renderDiagnostics(ctx *RenderContext) string {
822 args := newParamBuilder().addMain("project").build()
824 return renderWithParams(ctx, "Diagnostics", args, func() string {
825 return renderPlainContent(ctx, ctx.Result.Content)
829func renderAgent(ctx *RenderContext) string {
831 var params agent.AgentParams
832 unmarshalParams(ctx.Call.Input, ¶ms)
834 prompt := params.Prompt
835 prompt = strings.ReplaceAll(prompt, "\n", " ")
837 header := makeHeader(ctx, "Agent", []string{})
838 if res, done := earlyState(ctx, header); ctx.Cancelled && done {
841 taskTag := s.Tool.AgentTaskTag.Render("Task")
842 remainingWidth := ctx.TextWidth() - lipgloss.Width(header) - lipgloss.Width(taskTag) - 2
843 remainingWidth = min(remainingWidth, 120-lipgloss.Width(taskTag)-2)
844 prompt = s.Tool.AgentPrompt.Width(remainingWidth).Render(prompt)
845 header = lipgloss.JoinVertical(
849 lipgloss.JoinHorizontal(
856 childTools := tree.Root(header)
858 // TODO: Render nested tool calls when available.
861 childTools.Enumerator(roundedEnumeratorWithWidth(2, lipgloss.Width(taskTag)-5)).String(),
864 if ctx.Result.ToolCallID == "" {
865 // Pending state - would show animation in TUI.
866 parts = append(parts, "", s.Subtle.Render("Working..."))
869 header = lipgloss.JoinVertical(
874 if ctx.Result.ToolCallID == "" {
878 body := renderMarkdownContent(ctx, ctx.Result.Content)
879 return joinHeaderBody(ctx, header, body)
882func roundedEnumeratorWithWidth(width int, offset int) func(tree.Children, int) string {
883 return func(children tree.Children, i int) string {
884 if children.Length()-1 == i {
885 return strings.Repeat(" ", offset) + "└" + strings.Repeat("─", width-1) + " "
887 return strings.Repeat(" ", offset) + "├" + strings.Repeat("─", width-1) + " "
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list