on main help sign in
chore: implement agent tool
commitb6c792bce3ab0f847f8b4a69a9ab3715ffa12160 authorKujtim Hoxha <kujtimii.h@gmail.com> dateFri 20 Jun 2025, 15:20 (1 year ago) parentc992b57
6 files changed+69−22
internal/tui/components/chat/messages/messages.go+1−1 internal/tui/components/chat/messages/renderer.go+52−14 internal/tui/components/chat/messages/tool.go+9−2 internal/tui/components/core/list/keys.go+3−3 internal/tui/styles/crush.go+2−1 internal/tui/styles/theme.go+2−1
internal/tui/components/chat/messages/messages.go+1−1
@@ -164,7 +164,7 @@ func (m *messageCmp) renderUserMessage() string {
164164 parts = append(parts, "", strings.Join(attachments, ""))
165165 }
166166 joined := lipgloss.JoinVertical(lipgloss.Left, parts...)
167− return m.style().MarginBottom(1).Render(joined)
167+ return m.style().Render(joined)
168168}
169169
170170// toMarkdown converts text content to rendered markdown using the configured renderer
internal/tui/components/chat/messages/renderer.go+52−14
@@ -111,8 +111,18 @@ func (br baseRenderer) unmarshalParams(input string, target any) error {
111111 return json.Unmarshal([]byte(input), target)
112112}
113113
114+// makeHeader builds the tool call header with status icon and parameters for a nested tool call.
115+func (br baseRenderer) makeNestedHeader(v *toolCallCmp, tool string, width int, params ...string) string {
116+ t := styles.CurrentTheme()
117+ tool = t.S().Base.Foreground(t.FgHalfMuted).Render(tool) + " "
118+ return tool + renderParamList(true, width-lipgloss.Width(tool), params...)
119+}
120+
114121// makeHeader builds "<Tool>: param (key=value)" and truncates as needed.
115122func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params ...string) string {
123+ if v.isNested {
124+ return br.makeNestedHeader(v, tool, width, params...)
125+ }
116126 t := styles.CurrentTheme()
117127 icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
118128 if v.result.ToolCallID != "" {
@@ -126,7 +136,7 @@ func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params
126136 }
127137 tool = t.S().Base.Foreground(t.Blue).Render(tool)
128138 prefix := fmt.Sprintf("%s %s ", icon, tool)
129− return prefix + renderParamList(width-lipgloss.Width(prefix), params...)
139+ return prefix + renderParamList(false, width-lipgloss.Width(prefix), params...)
130140}
131141
132142// renderError provides consistent error rendering
@@ -477,25 +487,45 @@ type agentRenderer struct {
477487 baseRenderer
478488}
479489
490+func RoundedEnumerator(children tree.Children, index int) string {
491+ if children.Length()-1 == index {
492+ return " ╰──"
493+ }
494+ return " ├──"
495+}
496+
480497// Render displays agent task parameters and result content
481498func (tr agentRenderer) Render(v *toolCallCmp) string {
499+ t := styles.CurrentTheme()
482500 var params agent.AgentParams
483501 if err := tr.unmarshalParams(v.call.Input, &params); err != nil {
484502 return tr.renderError(v, "Invalid task parameters")
485503 }
486504 prompt := params.Prompt
487505 prompt = strings.ReplaceAll(prompt, "\n", " ")
488− args := newParamBuilder().addMain(prompt).build()
489506
490− header := tr.makeHeader(v, "Task", v.textWidth(), args...)
491− t := tree.Root(header)
507+ header := tr.makeHeader(v, "Agent", v.textWidth())
508+ taskTag := t.S().Base.Padding(0, 1).MarginLeft(1).Background(t.BlueLight).Foreground(t.White).Render("Task")
509+ remainingWidth := v.textWidth() - lipgloss.Width(header) - lipgloss.Width(taskTag) - 2 // -2 for padding
510+ prompt = t.S().Muted.Width(remainingWidth).Render(prompt)
511+ header = lipgloss.JoinVertical(
512+ lipgloss.Left,
513+ header,
514+ "",
515+ lipgloss.JoinHorizontal(
516+ lipgloss.Left,
517+ taskTag,
518+ " ",
519+ prompt,
520+ ),
521+ )
522+ childTools := tree.Root(header)
492523
493524 for _, call := range v.nestedToolCalls {
494− t.Child(call.View())
525+ childTools.Child(call.View())
495526 }
496−
497527 parts := []string{
498− t.Enumerator(tree.RoundedEnumerator).String(),
528+ childTools.Enumerator(RoundedEnumerator).String(),
499529 }
500530 if v.result.ToolCallID == "" {
501531 v.spinning = true
@@ -518,7 +548,7 @@ func (tr agentRenderer) Render(v *toolCallCmp) string {
518548}
519549
520550// renderParamList renders params, params[0] (params[1]=params[2] ....)
521−func renderParamList(paramsWidth int, params ...string) string {
551+func renderParamList(nested bool, paramsWidth int, params ...string) string {
522552 t := styles.CurrentTheme()
523553 if len(params) == 0 {
524554 return ""
@@ -529,6 +559,9 @@ func renderParamList(paramsWidth int, params ...string) string {
529559 }
530560
531561 if len(params) == 1 {
562+ if nested {
563+ return t.S().Muted.Render(mainParam)
564+ }
532565 return t.S().Subtle.Render(mainParam)
533566 }
534567 otherParams := params[1:]
@@ -550,6 +583,9 @@ func renderParamList(paramsWidth int, params ...string) string {
550583 partsRendered := strings.Join(parts, ", ")
551584 remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()"
552585 if remainingWidth < 30 {
586+ if nested {
587+ return t.S().Muted.Render(mainParam)
588+ }
553589 // No space for the params, just show the main
554590 return t.S().Subtle.Render(mainParam)
555591 }
@@ -558,6 +594,9 @@ func renderParamList(paramsWidth int, params ...string) string {
558594 mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
559595 }
560596
597+ if nested {
598+ return t.S().Muted.Render(ansi.Truncate(mainParam, paramsWidth, "..."))
599+ }
561600 return t.S().Subtle.Render(ansi.Truncate(mainParam, paramsWidth, "..."))
562601}
563602
@@ -635,7 +674,7 @@ func renderCodeContent(v *toolCallCmp, path, content string, offset int) string
635674 if len(strings.Split(content, "\n")) > responseContextHeight {
636675 lines = append(lines, t.S().Muted.
637676 Background(t.BgBase).
638− Render(fmt.Sprintf(" ... (%d lines)", len(strings.Split(content, "\n"))-responseContextHeight)))
677+ Render(fmt.Sprintf(" …(%d lines)", len(strings.Split(content, "\n"))-responseContextHeight)))
639678 }
640679
641680 maxLineNumber := len(lines) + offset
@@ -647,13 +686,12 @@ func renderCodeContent(v *toolCallCmp, path, content string, offset int) string
647686 PaddingRight(1).
648687 PaddingLeft(1).
649688 Render(pad(i+1+offset, padding))
650− w := v.textWidth() - 2 - lipgloss.Width(num) // -2 for left padding
689+ w := v.textWidth() - 10 - lipgloss.Width(num) // -4 for left padding
651690 lines[i] = lipgloss.JoinHorizontal(lipgloss.Left,
652691 num,
653692 t.S().Base.
654693 PaddingLeft(1).
655− Width(w).
656− Render(v.fit(ln, w)))
694+ Render(v.fit(ln, w-1)))
657695 }
658696 return lipgloss.JoinVertical(lipgloss.Left, lines...)
659697}
@@ -662,7 +700,7 @@ func (v *toolCallCmp) renderToolError() string {
662700 t := styles.CurrentTheme()
663701 err := strings.ReplaceAll(v.result.Content, "\n", " ")
664702 err = fmt.Sprintf("Error: %s", err)
665− return t.S().Base.Foreground(t.Error).Render(v.fit(err, v.textWidth()))
703+ return t.S().Base.Foreground(t.Error).Render(v.fit(err, v.textWidth()-2))
666704}
667705
668706func truncateHeight(s string, h int) string {
@@ -676,7 +714,7 @@ func truncateHeight(s string, h int) string {
676714func prettifyToolName(name string) string {
677715 switch name {
678716 case agent.AgentToolName:
679− return "Task"
717+ return "Agent"
680718 case tools.BashToolName:
681719 return "Bash"
682720 case tools.EditToolName:
internal/tui/components/chat/messages/tool.go+9−2
@@ -207,6 +207,10 @@ func (m *toolCallCmp) SetIsNested(isNested bool) {
207207// renderPending displays the tool name with a loading animation for pending tool calls
208208func (m *toolCallCmp) renderPending() string {
209209 t := styles.CurrentTheme()
210+ if m.isNested {
211+ tool := t.S().Base.Foreground(t.FgHalfMuted).Render(prettifyToolName(m.call.Name))
212+ return fmt.Sprintf("%s %s", tool, m.anim.View())
213+ }
210214 icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
211215 tool := t.S().Base.Foreground(t.Blue).Render(prettifyToolName(m.call.Name))
212216 return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
@@ -226,14 +230,17 @@ func (m *toolCallCmp) style() lipgloss.Style {
226230// textWidth calculates the available width for text content,
227231// accounting for borders and padding
228232func (m *toolCallCmp) textWidth() int {
233+ if m.isNested {
234+ return m.width - 6
235+ }
229236 return m.width - 5 // take into account the border and PaddingLeft
230237}
231238
232239// fit truncates content to fit within the specified width with ellipsis
233240func (m *toolCallCmp) fit(content string, width int) string {
234241 t := styles.CurrentTheme()
235− lineStyle := t.S().Muted.Background(t.BgSubtle)
236− dots := lineStyle.Render("...")
242+ lineStyle := t.S().Muted
243+ dots := lineStyle.Render("…")
237244 return ansi.Truncate(content, width, dots)
238245}
239246
internal/tui/components/core/list/keys.go+3−3
@@ -32,10 +32,10 @@ func DefaultKeyMap() KeyMap {
3232 key.WithKeys("k"),
3333 ),
3434 UpOneItem: key.NewBinding(
35− key.WithKeys("shift+up", "shift+k"),
35+ key.WithKeys("shift+up", "K"),
3636 ),
3737 DownOneItem: key.NewBinding(
38− key.WithKeys("shift+down", "shift+j"),
38+ key.WithKeys("shift+down", "J"),
3939 ),
4040 HalfPageDown: key.NewBinding(
4141 key.WithKeys("d"),
@@ -47,7 +47,7 @@ func DefaultKeyMap() KeyMap {
4747 key.WithKeys("g", "home"),
4848 ),
4949 End: key.NewBinding(
50− key.WithKeys("shift+g", "end"),
50+ key.WithKeys("G", "end"),
5151 ),
5252 }
5353}
internal/tui/styles/crush.go+2−1
@@ -39,7 +39,8 @@ func NewCrushTheme() *Theme {
3939 // Colors
4040 White: charmtone.Butter,
4141
42− Blue: charmtone.Malibu,
42+ BlueLight: charmtone.Sardine,
43+ Blue: charmtone.Malibu,
4344
4445 Yellow: charmtone.Mustard,
4546
internal/tui/styles/theme.go+2−1
@@ -56,7 +56,8 @@ type Theme struct {
5656 White color.Color
5757
5858 // Blues
59− Blue color.Color
59+ BlueLight color.Color
60+ Blue color.Color
6061
6162 // Yellows
6263 Yellow color.Color