From b6c792bce3ab0f847f8b4a69a9ab3715ffa12160 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Fri, 20 Jun 2025 15:20:00 +0200 Subject: [PATCH] chore: implement agent tool --- .../tui/components/chat/messages/messages.go | 2 +- .../tui/components/chat/messages/renderer.go | 66 +++++++++++++++---- internal/tui/components/chat/messages/tool.go | 11 +++- internal/tui/components/core/list/keys.go | 6 +- internal/tui/styles/crush.go | 3 +- internal/tui/styles/theme.go | 3 +- 6 files changed, 69 insertions(+), 22 deletions(-) diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go index 97e391bc6b041fb60a73a8b5abdd29ec6b576cf8..52ca288b9aa5a140f2abaa9ee64ae8775e78bfa6 100644 --- a/internal/tui/components/chat/messages/messages.go +++ b/internal/tui/components/chat/messages/messages.go @@ -164,7 +164,7 @@ func (m *messageCmp) renderUserMessage() string { parts = append(parts, "", strings.Join(attachments, "")) } joined := lipgloss.JoinVertical(lipgloss.Left, parts...) - return m.style().MarginBottom(1).Render(joined) + return m.style().Render(joined) } // toMarkdown converts text content to rendered markdown using the configured renderer diff --git a/internal/tui/components/chat/messages/renderer.go b/internal/tui/components/chat/messages/renderer.go index 1c6a96b445fe6053807309b0c4aefeab931387c6..c32a7a124f160b2efe6a832ac8fea6ae8357692c 100644 --- a/internal/tui/components/chat/messages/renderer.go +++ b/internal/tui/components/chat/messages/renderer.go @@ -111,8 +111,18 @@ func (br baseRenderer) unmarshalParams(input string, target any) error { return json.Unmarshal([]byte(input), target) } +// makeHeader builds the tool call header with status icon and parameters for a nested tool call. +func (br baseRenderer) makeNestedHeader(v *toolCallCmp, tool string, width int, params ...string) string { + t := styles.CurrentTheme() + tool = t.S().Base.Foreground(t.FgHalfMuted).Render(tool) + " " + return tool + renderParamList(true, width-lipgloss.Width(tool), params...) +} + // makeHeader builds ": param (key=value)" and truncates as needed. func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params ...string) string { + if v.isNested { + return br.makeNestedHeader(v, tool, width, params...) + } t := styles.CurrentTheme() icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending) if v.result.ToolCallID != "" { @@ -126,7 +136,7 @@ func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params } tool = t.S().Base.Foreground(t.Blue).Render(tool) prefix := fmt.Sprintf("%s %s ", icon, tool) - return prefix + renderParamList(width-lipgloss.Width(prefix), params...) + return prefix + renderParamList(false, width-lipgloss.Width(prefix), params...) } // renderError provides consistent error rendering @@ -477,25 +487,45 @@ type agentRenderer struct { baseRenderer } +func RoundedEnumerator(children tree.Children, index int) string { + if children.Length()-1 == index { + return " ╰──" + } + return " ├──" +} + // Render displays agent task parameters and result content func (tr agentRenderer) Render(v *toolCallCmp) string { + t := styles.CurrentTheme() var params agent.AgentParams if err := tr.unmarshalParams(v.call.Input, ¶ms); err != nil { return tr.renderError(v, "Invalid task parameters") } prompt := params.Prompt prompt = strings.ReplaceAll(prompt, "\n", " ") - args := newParamBuilder().addMain(prompt).build() - header := tr.makeHeader(v, "Task", v.textWidth(), args...) - t := tree.Root(header) + header := tr.makeHeader(v, "Agent", v.textWidth()) + taskTag := t.S().Base.Padding(0, 1).MarginLeft(1).Background(t.BlueLight).Foreground(t.White).Render("Task") + remainingWidth := v.textWidth() - lipgloss.Width(header) - lipgloss.Width(taskTag) - 2 // -2 for padding + prompt = t.S().Muted.Width(remainingWidth).Render(prompt) + header = lipgloss.JoinVertical( + lipgloss.Left, + header, + "", + lipgloss.JoinHorizontal( + lipgloss.Left, + taskTag, + " ", + prompt, + ), + ) + childTools := tree.Root(header) for _, call := range v.nestedToolCalls { - t.Child(call.View()) + childTools.Child(call.View()) } - parts := []string{ - t.Enumerator(tree.RoundedEnumerator).String(), + childTools.Enumerator(RoundedEnumerator).String(), } if v.result.ToolCallID == "" { v.spinning = true @@ -518,7 +548,7 @@ func (tr agentRenderer) Render(v *toolCallCmp) string { } // renderParamList renders params, params[0] (params[1]=params[2] ....) -func renderParamList(paramsWidth int, params ...string) string { +func renderParamList(nested bool, paramsWidth int, params ...string) string { t := styles.CurrentTheme() if len(params) == 0 { return "" @@ -529,6 +559,9 @@ func renderParamList(paramsWidth int, params ...string) string { } if len(params) == 1 { + if nested { + return t.S().Muted.Render(mainParam) + } return t.S().Subtle.Render(mainParam) } otherParams := params[1:] @@ -550,6 +583,9 @@ func renderParamList(paramsWidth int, params ...string) string { partsRendered := strings.Join(parts, ", ") remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()" if remainingWidth < 30 { + if nested { + return t.S().Muted.Render(mainParam) + } // No space for the params, just show the main return t.S().Subtle.Render(mainParam) } @@ -558,6 +594,9 @@ func renderParamList(paramsWidth int, params ...string) string { mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", ")) } + if nested { + return t.S().Muted.Render(ansi.Truncate(mainParam, paramsWidth, "...")) + } return t.S().Subtle.Render(ansi.Truncate(mainParam, paramsWidth, "...")) } @@ -635,7 +674,7 @@ func renderCodeContent(v *toolCallCmp, path, content string, offset int) string if len(strings.Split(content, "\n")) > responseContextHeight { lines = append(lines, t.S().Muted. Background(t.BgBase). - Render(fmt.Sprintf(" ... (%d lines)", len(strings.Split(content, "\n"))-responseContextHeight))) + Render(fmt.Sprintf(" …(%d lines)", len(strings.Split(content, "\n"))-responseContextHeight))) } maxLineNumber := len(lines) + offset @@ -647,13 +686,12 @@ func renderCodeContent(v *toolCallCmp, path, content string, offset int) string PaddingRight(1). PaddingLeft(1). Render(pad(i+1+offset, padding)) - w := v.textWidth() - 2 - lipgloss.Width(num) // -2 for left padding + w := v.textWidth() - 10 - lipgloss.Width(num) // -4 for left padding lines[i] = lipgloss.JoinHorizontal(lipgloss.Left, num, t.S().Base. PaddingLeft(1). - Width(w). - Render(v.fit(ln, w))) + Render(v.fit(ln, w-1))) } return lipgloss.JoinVertical(lipgloss.Left, lines...) } @@ -662,7 +700,7 @@ func (v *toolCallCmp) renderToolError() string { t := styles.CurrentTheme() err := strings.ReplaceAll(v.result.Content, "\n", " ") err = fmt.Sprintf("Error: %s", err) - return t.S().Base.Foreground(t.Error).Render(v.fit(err, v.textWidth())) + return t.S().Base.Foreground(t.Error).Render(v.fit(err, v.textWidth()-2)) } func truncateHeight(s string, h int) string { @@ -676,7 +714,7 @@ func truncateHeight(s string, h int) string { func prettifyToolName(name string) string { switch name { case agent.AgentToolName: - return "Task" + return "Agent" case tools.BashToolName: return "Bash" case tools.EditToolName: diff --git a/internal/tui/components/chat/messages/tool.go b/internal/tui/components/chat/messages/tool.go index 89d1f407eb12e157307f95f8c81735f7cdd26260..458e5ed320c2ce6c33fc35afef8a076a6e594e56 100644 --- a/internal/tui/components/chat/messages/tool.go +++ b/internal/tui/components/chat/messages/tool.go @@ -207,6 +207,10 @@ func (m *toolCallCmp) SetIsNested(isNested bool) { // renderPending displays the tool name with a loading animation for pending tool calls func (m *toolCallCmp) renderPending() string { t := styles.CurrentTheme() + if m.isNested { + tool := t.S().Base.Foreground(t.FgHalfMuted).Render(prettifyToolName(m.call.Name)) + return fmt.Sprintf("%s %s", tool, m.anim.View()) + } icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending) tool := t.S().Base.Foreground(t.Blue).Render(prettifyToolName(m.call.Name)) return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View()) @@ -226,14 +230,17 @@ func (m *toolCallCmp) style() lipgloss.Style { // textWidth calculates the available width for text content, // accounting for borders and padding func (m *toolCallCmp) textWidth() int { + if m.isNested { + return m.width - 6 + } return m.width - 5 // take into account the border and PaddingLeft } // fit truncates content to fit within the specified width with ellipsis func (m *toolCallCmp) fit(content string, width int) string { t := styles.CurrentTheme() - lineStyle := t.S().Muted.Background(t.BgSubtle) - dots := lineStyle.Render("...") + lineStyle := t.S().Muted + dots := lineStyle.Render("…") return ansi.Truncate(content, width, dots) } diff --git a/internal/tui/components/core/list/keys.go b/internal/tui/components/core/list/keys.go index 4ad2a9e27807063609215f1f6c834872ceff2aac..0e33b62d1b615ea49866881b770d292486b688de 100644 --- a/internal/tui/components/core/list/keys.go +++ b/internal/tui/components/core/list/keys.go @@ -32,10 +32,10 @@ func DefaultKeyMap() KeyMap { key.WithKeys("k"), ), UpOneItem: key.NewBinding( - key.WithKeys("shift+up", "shift+k"), + key.WithKeys("shift+up", "K"), ), DownOneItem: key.NewBinding( - key.WithKeys("shift+down", "shift+j"), + key.WithKeys("shift+down", "J"), ), HalfPageDown: key.NewBinding( key.WithKeys("d"), @@ -47,7 +47,7 @@ func DefaultKeyMap() KeyMap { key.WithKeys("g", "home"), ), End: key.NewBinding( - key.WithKeys("shift+g", "end"), + key.WithKeys("G", "end"), ), } } diff --git a/internal/tui/styles/crush.go b/internal/tui/styles/crush.go index 7ee690b99037770bdd2204db7f6270c20d473514..975c7f6080e654bad0a7d760543535bc6eea5827 100644 --- a/internal/tui/styles/crush.go +++ b/internal/tui/styles/crush.go @@ -39,7 +39,8 @@ func NewCrushTheme() *Theme { // Colors White: charmtone.Butter, - Blue: charmtone.Malibu, + BlueLight: charmtone.Sardine, + Blue: charmtone.Malibu, Yellow: charmtone.Mustard, diff --git a/internal/tui/styles/theme.go b/internal/tui/styles/theme.go index 4da3bd520f14da5d8e2fbfd561ed31d7a5a56fa2..b6a5b4d1e2b41b7bb1190d5a802bd48f4aeceec3 100644 --- a/internal/tui/styles/theme.go +++ b/internal/tui/styles/theme.go @@ -56,7 +56,8 @@ type Theme struct { White color.Color // Blues - Blue color.Color + BlueLight color.Color + Blue color.Color // Yellows Yellow color.Color