| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328 |
- package chat
- import (
- "cmp"
- "fmt"
- "strings"
- "time"
- tea "charm.land/bubbletea/v2"
- "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/fsext"
- "github.com/charmbracelet/crush/internal/ui/list"
- )
- // NewToolItem creates the appropriate tool item for the given context.
- func NewToolItem(ctx ToolCallContext) MessageItem {
- switch ctx.Call.Name {
- // Bash tools
- case tools.BashToolName:
- return NewBashToolItem(ctx)
- case tools.JobOutputToolName:
- return NewJobOutputToolItem(ctx)
- case tools.JobKillToolName:
- return NewJobKillToolItem(ctx)
- // File tools
- case tools.ViewToolName:
- return NewViewToolItem(ctx)
- case tools.EditToolName:
- return NewEditToolItem(ctx)
- case tools.MultiEditToolName:
- return NewMultiEditToolItem(ctx)
- case tools.WriteToolName:
- return NewWriteToolItem(ctx)
- // Search tools
- case tools.GlobToolName:
- return NewGlobToolItem(ctx)
- case tools.GrepToolName:
- return NewGrepToolItem(ctx)
- case tools.LSToolName:
- return NewLSToolItem(ctx)
- case tools.SourcegraphToolName:
- return NewSourcegraphToolItem(ctx)
- // Fetch tools
- case tools.FetchToolName:
- return NewFetchToolItem(ctx)
- case tools.AgenticFetchToolName:
- return NewAgenticFetchToolItem(ctx)
- case tools.WebFetchToolName:
- return NewWebFetchToolItem(ctx)
- case tools.WebSearchToolName:
- return NewWebSearchToolItem(ctx)
- case tools.DownloadToolName:
- return NewDownloadToolItem(ctx)
- // LSP tools
- case tools.DiagnosticsToolName:
- return NewDiagnosticsToolItem(ctx)
- case tools.ReferencesToolName:
- return NewReferencesToolItem(ctx)
- // Misc tools
- case tools.TodosToolName:
- return NewTodosToolItem(ctx)
- case agent.AgentToolName:
- return NewAgentToolItem(ctx)
- default:
- return NewGenericToolItem(ctx)
- }
- }
- // -----------------------------------------------------------------------------
- // Bash Tools
- // -----------------------------------------------------------------------------
- // BashToolItem renders bash command execution.
- type BashToolItem struct {
- toolItem
- }
- func NewBashToolItem(ctx ToolCallContext) *BashToolItem {
- return &BashToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- // Update implements list.Updatable.
- func (m *BashToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- func (m *BashToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.BashParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- cmd := strings.ReplaceAll(params.Command, "\n", " ")
- cmd = strings.ReplaceAll(cmd, "\t", " ")
- if m.ctx.Call.Finished && m.ctx.HasResult() {
- var meta tools.BashResponseMetadata
- unmarshalParams(m.ctx.Result.Metadata, &meta)
- if meta.Background {
- return m.renderBackgroundJob(params, meta, width)
- }
- }
- args := NewParamBuilder().
- Main(cmd).
- Flag("background", params.RunInBackground).
- Build()
- header := renderToolHeader(&m.ctx, "Bash", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- var meta tools.BashResponseMetadata
- unmarshalParams(m.ctx.Result.Metadata, &meta)
- output := meta.Output
- if output == "" && m.ctx.Result.Content != tools.BashNoOutput {
- output = m.ctx.Result.Content
- }
- if output == "" {
- return header
- }
- body := renderPlainContent(output, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- func (m *BashToolItem) renderBackgroundJob(params tools.BashParams, meta tools.BashResponseMetadata, width int) string {
- description := cmp.Or(meta.Description, params.Command)
- header := renderJobHeader(&m.ctx, "Start", meta.ShellID, description, width)
- if m.ctx.IsNested {
- return header
- }
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- content := "Command: " + params.Command + "\n" + m.ctx.Result.Content
- body := renderPlainContent(content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // JobOutputToolItem renders job output retrieval.
- type JobOutputToolItem struct {
- toolItem
- }
- func NewJobOutputToolItem(ctx ToolCallContext) *JobOutputToolItem {
- return &JobOutputToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *JobOutputToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.JobOutputParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- var meta tools.JobOutputResponseMetadata
- var description string
- if m.ctx.Result != nil && m.ctx.Result.Metadata != "" {
- unmarshalParams(m.ctx.Result.Metadata, &meta)
- description = cmp.Or(meta.Description, meta.Command)
- }
- header := renderJobHeader(&m.ctx, "Output", params.ShellID, description, width)
- if m.ctx.IsNested {
- return header
- }
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // JobKillToolItem renders job termination.
- type JobKillToolItem struct {
- toolItem
- }
- func NewJobKillToolItem(ctx ToolCallContext) *JobKillToolItem {
- return &JobKillToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *JobKillToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.JobKillParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- var meta tools.JobKillResponseMetadata
- var description string
- if m.ctx.Result != nil && m.ctx.Result.Metadata != "" {
- unmarshalParams(m.ctx.Result.Metadata, &meta)
- description = cmp.Or(meta.Description, meta.Command)
- }
- header := renderJobHeader(&m.ctx, "Kill", params.ShellID, description, width)
- if m.ctx.IsNested {
- return header
- }
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // renderJobHeader builds a job-specific header with action and PID.
- func renderJobHeader(ctx *ToolCallContext, action, pid, description string, width int) string {
- sty := ctx.Styles
- icon := renderToolIcon(ctx.Status(), sty)
- jobPart := sty.Tool.JobToolName.Render("Job")
- actionPart := sty.Tool.JobAction.Render("(" + action + ")")
- pidPart := sty.Tool.JobPID.Render("PID " + pid)
- prefix := fmt.Sprintf("%s %s %s %s", icon, jobPart, actionPart, pidPart)
- if description == "" {
- return prefix
- }
- descPart := " " + sty.Tool.JobDescription.Render(description)
- fullHeader := prefix + descPart
- if lipgloss.Width(fullHeader) > width {
- availableWidth := width - lipgloss.Width(prefix) - 1
- if availableWidth < 10 {
- return prefix
- }
- descPart = " " + sty.Tool.JobDescription.Render(truncateText(description, availableWidth))
- fullHeader = prefix + descPart
- }
- return fullHeader
- }
- // -----------------------------------------------------------------------------
- // File Tools
- // -----------------------------------------------------------------------------
- // ViewToolItem renders file viewing with syntax highlighting.
- type ViewToolItem struct {
- toolItem
- }
- func NewViewToolItem(ctx ToolCallContext) *ViewToolItem {
- return &ViewToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *ViewToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.ViewParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- file := fsext.PrettyPath(params.FilePath)
- args := NewParamBuilder().
- Main(file).
- KeyValue("limit", formatNonZero(params.Limit)).
- KeyValue("offset", formatNonZero(params.Offset)).
- Build()
- header := renderToolHeader(&m.ctx, "View", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- if m.ctx.Result.Data != "" && strings.HasPrefix(m.ctx.Result.MIMEType, "image/") {
- body := renderImageContent(m.ctx.Result.Data, m.ctx.Result.MIMEType, "", m.ctx.Styles)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- var meta tools.ViewResponseMetadata
- unmarshalParams(m.ctx.Result.Metadata, &meta)
- body := renderCodeContent(meta.FilePath, meta.Content, params.Offset, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // EditToolItem renders file editing with diff visualization.
- type EditToolItem struct {
- toolItem
- }
- func NewEditToolItem(ctx ToolCallContext) *EditToolItem {
- return &EditToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *EditToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.EditParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- file := fsext.PrettyPath(params.FilePath)
- args := NewParamBuilder().Main(file).Build()
- header := renderToolHeader(&m.ctx, "Edit", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- var meta tools.EditResponseMetadata
- if err := unmarshalParams(m.ctx.Result.Metadata, &meta); err != nil {
- body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, nil)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- body := renderDiffContent(file, meta.OldContent, meta.NewContent, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // MultiEditToolItem renders multiple file edits with diff visualization.
- type MultiEditToolItem struct {
- toolItem
- }
- func NewMultiEditToolItem(ctx ToolCallContext) *MultiEditToolItem {
- return &MultiEditToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *MultiEditToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.MultiEditParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- file := fsext.PrettyPath(params.FilePath)
- args := NewParamBuilder().
- Main(file).
- KeyValue("edits", fmt.Sprintf("%d", len(params.Edits))).
- Build()
- header := renderToolHeader(&m.ctx, "Multi-Edit", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- var meta tools.MultiEditResponseMetadata
- if err := unmarshalParams(m.ctx.Result.Metadata, &meta); err != nil {
- body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, nil)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- body := renderDiffContent(file, meta.OldContent, meta.NewContent, width-2, m.ctx.Styles, &m.toolItem)
- if len(meta.EditsFailed) > 0 {
- sty := m.ctx.Styles
- noteTag := sty.Tool.NoteTag.Render("Note")
- noteMsg := fmt.Sprintf("%d of %d edits succeeded", meta.EditsApplied, len(params.Edits))
- note := fmt.Sprintf("%s %s", noteTag, sty.Tool.NoteMessage.Render(noteMsg))
- body = lipgloss.JoinVertical(lipgloss.Left, body, "", note)
- }
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // WriteToolItem renders file writing with syntax-highlighted content preview.
- type WriteToolItem struct {
- toolItem
- }
- func NewWriteToolItem(ctx ToolCallContext) *WriteToolItem {
- return &WriteToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *WriteToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.WriteParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- file := fsext.PrettyPath(params.FilePath)
- args := NewParamBuilder().Main(file).Build()
- header := renderToolHeader(&m.ctx, "Write", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- body := renderCodeContent(file, params.Content, 0, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // -----------------------------------------------------------------------------
- // Search Tools
- // -----------------------------------------------------------------------------
- // GlobToolItem renders glob file pattern matching results.
- type GlobToolItem struct {
- toolItem
- }
- func NewGlobToolItem(ctx ToolCallContext) *GlobToolItem {
- return &GlobToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *GlobToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.GlobParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- args := NewParamBuilder().
- Main(params.Pattern).
- KeyValue("path", fsext.PrettyPath(params.Path)).
- Build()
- header := renderToolHeader(&m.ctx, "Glob", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // GrepToolItem renders grep content search results.
- type GrepToolItem struct {
- toolItem
- }
- func NewGrepToolItem(ctx ToolCallContext) *GrepToolItem {
- return &GrepToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *GrepToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.GrepParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- args := NewParamBuilder().
- Main(params.Pattern).
- KeyValue("path", fsext.PrettyPath(params.Path)).
- KeyValue("include", params.Include).
- Flag("literal", params.LiteralText).
- Build()
- header := renderToolHeader(&m.ctx, "Grep", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // LSToolItem renders directory listing results.
- type LSToolItem struct {
- toolItem
- }
- func NewLSToolItem(ctx ToolCallContext) *LSToolItem {
- return &LSToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *LSToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.LSParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- path := cmp.Or(params.Path, ".")
- path = fsext.PrettyPath(path)
- args := NewParamBuilder().Main(path).Build()
- header := renderToolHeader(&m.ctx, "List", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // SourcegraphToolItem renders code search results.
- type SourcegraphToolItem struct {
- toolItem
- }
- func NewSourcegraphToolItem(ctx ToolCallContext) *SourcegraphToolItem {
- return &SourcegraphToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *SourcegraphToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.SourcegraphParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- args := NewParamBuilder().
- Main(params.Query).
- KeyValue("count", formatNonZero(params.Count)).
- KeyValue("context", formatNonZero(params.ContextWindow)).
- Build()
- header := renderToolHeader(&m.ctx, "Sourcegraph", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // -----------------------------------------------------------------------------
- // Fetch Tools
- // -----------------------------------------------------------------------------
- // FetchToolItem renders URL fetching with format-specific content display.
- type FetchToolItem struct {
- toolItem
- }
- func NewFetchToolItem(ctx ToolCallContext) *FetchToolItem {
- return &FetchToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *FetchToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.FetchParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- args := NewParamBuilder().
- Main(params.URL).
- KeyValue("format", params.Format).
- KeyValue("timeout", formatTimeout(params.Timeout)).
- Build()
- header := renderToolHeader(&m.ctx, "Fetch", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- file := "fetch.md"
- switch params.Format {
- case "text":
- file = "fetch.txt"
- case "html":
- file = "fetch.html"
- }
- body := renderCodeContent(file, m.ctx.Result.Content, 0, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // AgenticFetchToolItem renders agentic URL fetching with nested tool calls.
- type AgenticFetchToolItem struct {
- toolItem
- }
- func NewAgenticFetchToolItem(ctx ToolCallContext) *AgenticFetchToolItem {
- return &AgenticFetchToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *AgenticFetchToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.AgenticFetchParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- var args []string
- if params.URL != "" {
- args = NewParamBuilder().Main(params.URL).Build()
- }
- header := renderToolHeader(&m.ctx, "Agentic Fetch", width, args...)
- // Render with nested tool calls tree
- body := renderAgentBody(&m.ctx, params.Prompt, "Prompt", header, width)
- return body
- }
- // WebFetchToolItem renders web page fetching.
- type WebFetchToolItem struct {
- toolItem
- }
- func NewWebFetchToolItem(ctx ToolCallContext) *WebFetchToolItem {
- return &WebFetchToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *WebFetchToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.WebFetchParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- args := NewParamBuilder().Main(params.URL).Build()
- header := renderToolHeader(&m.ctx, "Fetch", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- body := renderMarkdownContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // WebSearchToolItem renders web search results.
- type WebSearchToolItem struct {
- toolItem
- }
- func NewWebSearchToolItem(ctx ToolCallContext) *WebSearchToolItem {
- return &WebSearchToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *WebSearchToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.WebSearchParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- args := NewParamBuilder().Main(params.Query).Build()
- header := renderToolHeader(&m.ctx, "Search", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- body := renderMarkdownContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // DownloadToolItem renders file downloading.
- type DownloadToolItem struct {
- toolItem
- }
- func NewDownloadToolItem(ctx ToolCallContext) *DownloadToolItem {
- return &DownloadToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *DownloadToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.DownloadParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- args := NewParamBuilder().
- Main(params.URL).
- KeyValue("file_path", fsext.PrettyPath(params.FilePath)).
- KeyValue("timeout", formatTimeout(params.Timeout)).
- Build()
- header := renderToolHeader(&m.ctx, "Download", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // -----------------------------------------------------------------------------
- // LSP Tools
- // -----------------------------------------------------------------------------
- // DiagnosticsToolItem renders project-wide diagnostic information.
- type DiagnosticsToolItem struct {
- toolItem
- }
- func NewDiagnosticsToolItem(ctx ToolCallContext) *DiagnosticsToolItem {
- return &DiagnosticsToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *DiagnosticsToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- args := NewParamBuilder().Main("project").Build()
- header := renderToolHeader(&m.ctx, "Diagnostics", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // ReferencesToolItem renders LSP references search results.
- type ReferencesToolItem struct {
- toolItem
- }
- func NewReferencesToolItem(ctx ToolCallContext) *ReferencesToolItem {
- return &ReferencesToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *ReferencesToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params tools.ReferencesParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- args := NewParamBuilder().
- Main(params.Symbol).
- KeyValue("path", fsext.PrettyPath(params.Path)).
- Build()
- header := renderToolHeader(&m.ctx, "References", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // -----------------------------------------------------------------------------
- // Misc Tools
- // -----------------------------------------------------------------------------
- // TodosToolItem renders todo list management.
- type TodosToolItem struct {
- toolItem
- }
- func NewTodosToolItem(ctx ToolCallContext) *TodosToolItem {
- return &TodosToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *TodosToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- sty := m.ctx.Styles
- var params tools.TodosParams
- var meta tools.TodosResponseMetadata
- var headerText string
- var body string
- // Parse params for pending state
- if err := unmarshalParams(m.ctx.Call.Input, ¶ms); err == nil {
- completedCount := 0
- inProgressTask := ""
- for _, todo := range params.Todos {
- if todo.Status == "completed" {
- completedCount++
- }
- if todo.Status == "in_progress" {
- inProgressTask = cmp.Or(todo.ActiveForm, todo.Content)
- }
- }
- // Default display from params
- ratio := sty.Tool.JobAction.Render(fmt.Sprintf("%d/%d", completedCount, len(params.Todos)))
- headerText = ratio
- if inProgressTask != "" {
- headerText = fmt.Sprintf("%s · %s", ratio, inProgressTask)
- }
- // If we have metadata, use it for richer display
- if m.ctx.Result != nil && m.ctx.Result.Metadata != "" {
- if err := unmarshalParams(m.ctx.Result.Metadata, &meta); err == nil {
- headerText, body = m.formatTodosFromMeta(meta, width)
- }
- }
- }
- args := NewParamBuilder().Main(headerText).Build()
- header := renderToolHeader(&m.ctx, "To-Do", width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- if body == "" {
- return header
- }
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- func (m *TodosToolItem) formatTodosFromMeta(meta tools.TodosResponseMetadata, width int) (string, string) {
- sty := m.ctx.Styles
- var headerText, body string
- if meta.IsNew {
- if meta.JustStarted != "" {
- headerText = fmt.Sprintf("created %d todos, starting first", meta.Total)
- } else {
- headerText = fmt.Sprintf("created %d todos", meta.Total)
- }
- body = formatTodosList(meta.Todos, width, sty)
- } else {
- hasCompleted := len(meta.JustCompleted) > 0
- hasStarted := meta.JustStarted != ""
- allCompleted := meta.Completed == meta.Total
- ratio := sty.Tool.JobAction.Render(fmt.Sprintf("%d/%d", meta.Completed, meta.Total))
- if hasCompleted && hasStarted {
- text := sty.Tool.JobDescription.Render(fmt.Sprintf(" · completed %d, starting next", len(meta.JustCompleted)))
- headerText = ratio + text
- } else if hasCompleted {
- text := " · completed all"
- if !allCompleted {
- text = fmt.Sprintf(" · completed %d", len(meta.JustCompleted))
- }
- headerText = ratio + sty.Tool.JobDescription.Render(text)
- } else if hasStarted {
- headerText = ratio + sty.Tool.JobDescription.Render(" · starting task")
- } else {
- headerText = ratio
- }
- if allCompleted {
- body = formatTodosList(meta.Todos, width, sty)
- } else if meta.JustStarted != "" {
- body = sty.Tool.IconSuccess.String() + " " + sty.Base.Render(meta.JustStarted)
- }
- }
- return headerText, body
- }
- // AgentToolItem renders agent task execution with nested tool calls.
- type AgentToolItem struct {
- toolItem
- }
- func NewAgentToolItem(ctx ToolCallContext) *AgentToolItem {
- return &AgentToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *AgentToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- var params agent.AgentParams
- unmarshalParams(m.ctx.Call.Input, ¶ms)
- header := renderToolHeader(&m.ctx, "Agent", width)
- body := renderAgentBody(&m.ctx, params.Prompt, "Task", header, width)
- return body
- }
- // renderAgentBody renders agent/agentic_fetch body with prompt tag and nested calls tree.
- func renderAgentBody(ctx *ToolCallContext, prompt, tagLabel, header string, width int) string {
- sty := ctx.Styles
- if ctx.Cancelled {
- if result, done := renderEarlyState(ctx, header, width); done {
- return result
- }
- }
- // Build prompt tag
- prompt = strings.ReplaceAll(prompt, "\n", " ")
- taskTag := sty.Tool.AgentTaskTag.Render(tagLabel)
- tagWidth := lipgloss.Width(taskTag)
- remainingWidth := min(width-tagWidth-2, 120-tagWidth-2)
- promptStyled := sty.Tool.AgentPrompt.Width(remainingWidth).Render(prompt)
- headerWithPrompt := lipgloss.JoinVertical(
- lipgloss.Left,
- header,
- "",
- lipgloss.JoinHorizontal(lipgloss.Left, taskTag, " ", promptStyled),
- )
- // Build tree with nested tool calls
- childTools := tree.Root(headerWithPrompt)
- for _, nestedCtx := range ctx.NestedCalls {
- nestedCtx.IsNested = true
- nestedItem := NewToolItem(nestedCtx)
- childTools.Child(nestedItem.Render(remainingWidth))
- }
- parts := []string{
- childTools.Enumerator(roundedEnumerator(2, tagWidth-5)).String(),
- }
- if !ctx.HasResult() {
- parts = append(parts, "", sty.Tool.StateWaiting.Render("Working..."))
- }
- treeOutput := lipgloss.JoinVertical(lipgloss.Left, parts...)
- if !ctx.HasResult() {
- return treeOutput
- }
- body := renderMarkdownContent(ctx.Result.Content, width-2, sty, nil)
- return joinHeaderBody(treeOutput, body, sty)
- }
- // roundedEnumerator creates a tree enumerator with rounded connectors.
- func roundedEnumerator(lPadding, lineWidth int) tree.Enumerator {
- if lineWidth == 0 {
- lineWidth = 2
- }
- if lPadding == 0 {
- lPadding = 1
- }
- return func(children tree.Children, index int) string {
- line := strings.Repeat("─", lineWidth)
- padding := strings.Repeat(" ", lPadding)
- if children.Length()-1 == index {
- return padding + "╰" + line
- }
- return padding + "├" + line
- }
- }
- // GenericToolItem renders unknown tool types with basic parameter display.
- type GenericToolItem struct {
- toolItem
- }
- func NewGenericToolItem(ctx ToolCallContext) *GenericToolItem {
- return &GenericToolItem{
- toolItem: newToolItem(ctx),
- }
- }
- func (m *GenericToolItem) Render(width int) string {
- if !m.ctx.Call.Finished && !m.ctx.Cancelled {
- return m.renderPending()
- }
- name := prettifyToolName(m.ctx.Call.Name)
- // Handle media content
- if m.ctx.Result != nil && m.ctx.Result.Data != "" {
- if strings.HasPrefix(m.ctx.Result.MIMEType, "image/") {
- args := NewParamBuilder().Main(m.toolItem.ctx.Call.Input).Build()
- header := renderToolHeader(&m.ctx, name, width, args...)
- body := renderImageContent(m.ctx.Result.Data, m.ctx.Result.MIMEType, m.ctx.Result.Content, m.ctx.Styles)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- args := NewParamBuilder().Main(m.toolItem.ctx.Call.Input).Build()
- header := renderToolHeader(&m.ctx, name, width, args...)
- body := renderMediaContent(m.ctx.Result.MIMEType, m.ctx.Result.Content, m.ctx.Styles)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- args := NewParamBuilder().Main(m.toolItem.ctx.Call.Input).Build()
- header := renderToolHeader(&m.ctx, name, width, args...)
- if result, done := renderEarlyState(&m.ctx, header, width); done {
- return result
- }
- if m.ctx.Result == nil || m.ctx.Result.Content == "" {
- return header
- }
- body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
- return joinHeaderBody(header, body, m.ctx.Styles)
- }
- // -----------------------------------------------------------------------------
- // Helper Functions
- // -----------------------------------------------------------------------------
- // prettifyToolName converts tool names to display-friendly format.
- func prettifyToolName(name string) string {
- switch name {
- case agent.AgentToolName:
- return "Agent"
- case tools.BashToolName:
- return "Bash"
- case tools.JobOutputToolName:
- return "Job: Output"
- case tools.JobKillToolName:
- return "Job: Kill"
- case tools.DownloadToolName:
- return "Download"
- case tools.EditToolName:
- return "Edit"
- case tools.MultiEditToolName:
- return "Multi-Edit"
- case tools.FetchToolName:
- return "Fetch"
- case tools.AgenticFetchToolName:
- return "Agentic Fetch"
- case tools.WebFetchToolName:
- return "Fetch"
- case tools.WebSearchToolName:
- return "Search"
- case tools.GlobToolName:
- return "Glob"
- case tools.GrepToolName:
- return "Grep"
- case tools.LSToolName:
- return "List"
- case tools.SourcegraphToolName:
- return "Sourcegraph"
- case tools.TodosToolName:
- return "To-Do"
- case tools.ViewToolName:
- return "View"
- case tools.WriteToolName:
- return "Write"
- case tools.DiagnosticsToolName:
- return "Diagnostics"
- case tools.ReferencesToolName:
- return "References"
- default:
- // Handle MCP tools and others
- name = strings.TrimPrefix(name, "mcp_")
- if name == "" {
- return "Tool"
- }
- return strings.ToUpper(name[:1]) + name[1:]
- }
- }
- // formatTimeout converts timeout seconds to duration string.
- func formatTimeout(timeout int) string {
- if timeout == 0 {
- return ""
- }
- return (time.Duration(timeout) * time.Second).String()
- }
- // truncateText truncates text to fit within width with ellipsis.
- func truncateText(s string, width int) string {
- if lipgloss.Width(s) <= width {
- return s
- }
- for i := len(s) - 1; i >= 0; i-- {
- truncated := s[:i] + "…"
- if lipgloss.Width(truncated) <= width {
- return truncated
- }
- }
- return "…"
- }
- // Update implements list.Updatable.
- func (m *JobOutputToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *JobKillToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *ViewToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *EditToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *MultiEditToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *WriteToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *GlobToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *GrepToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *LSToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *SourcegraphToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *FetchToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *AgenticFetchToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *WebFetchToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *WebSearchToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *DownloadToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *DiagnosticsToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *ReferencesToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *TodosToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *AgentToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
- // Update implements list.Updatable.
- func (m *GenericToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
- cmd, changed := m.updateAnimation(msg)
- if changed {
- return m, cmd
- }
- return m, nil
- }
|