tool_items.go 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328
  1. package chat
  2. import (
  3. "cmp"
  4. "fmt"
  5. "strings"
  6. "time"
  7. tea "charm.land/bubbletea/v2"
  8. "charm.land/lipgloss/v2"
  9. "charm.land/lipgloss/v2/tree"
  10. "github.com/charmbracelet/crush/internal/agent"
  11. "github.com/charmbracelet/crush/internal/agent/tools"
  12. "github.com/charmbracelet/crush/internal/fsext"
  13. "github.com/charmbracelet/crush/internal/ui/list"
  14. )
  15. // NewToolItem creates the appropriate tool item for the given context.
  16. func NewToolItem(ctx ToolCallContext) MessageItem {
  17. switch ctx.Call.Name {
  18. // Bash tools
  19. case tools.BashToolName:
  20. return NewBashToolItem(ctx)
  21. case tools.JobOutputToolName:
  22. return NewJobOutputToolItem(ctx)
  23. case tools.JobKillToolName:
  24. return NewJobKillToolItem(ctx)
  25. // File tools
  26. case tools.ViewToolName:
  27. return NewViewToolItem(ctx)
  28. case tools.EditToolName:
  29. return NewEditToolItem(ctx)
  30. case tools.MultiEditToolName:
  31. return NewMultiEditToolItem(ctx)
  32. case tools.WriteToolName:
  33. return NewWriteToolItem(ctx)
  34. // Search tools
  35. case tools.GlobToolName:
  36. return NewGlobToolItem(ctx)
  37. case tools.GrepToolName:
  38. return NewGrepToolItem(ctx)
  39. case tools.LSToolName:
  40. return NewLSToolItem(ctx)
  41. case tools.SourcegraphToolName:
  42. return NewSourcegraphToolItem(ctx)
  43. // Fetch tools
  44. case tools.FetchToolName:
  45. return NewFetchToolItem(ctx)
  46. case tools.AgenticFetchToolName:
  47. return NewAgenticFetchToolItem(ctx)
  48. case tools.WebFetchToolName:
  49. return NewWebFetchToolItem(ctx)
  50. case tools.WebSearchToolName:
  51. return NewWebSearchToolItem(ctx)
  52. case tools.DownloadToolName:
  53. return NewDownloadToolItem(ctx)
  54. // LSP tools
  55. case tools.DiagnosticsToolName:
  56. return NewDiagnosticsToolItem(ctx)
  57. case tools.ReferencesToolName:
  58. return NewReferencesToolItem(ctx)
  59. // Misc tools
  60. case tools.TodosToolName:
  61. return NewTodosToolItem(ctx)
  62. case agent.AgentToolName:
  63. return NewAgentToolItem(ctx)
  64. default:
  65. return NewGenericToolItem(ctx)
  66. }
  67. }
  68. // -----------------------------------------------------------------------------
  69. // Bash Tools
  70. // -----------------------------------------------------------------------------
  71. // BashToolItem renders bash command execution.
  72. type BashToolItem struct {
  73. toolItem
  74. }
  75. func NewBashToolItem(ctx ToolCallContext) *BashToolItem {
  76. return &BashToolItem{
  77. toolItem: newToolItem(ctx),
  78. }
  79. }
  80. // Update implements list.Updatable.
  81. func (m *BashToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  82. cmd, changed := m.updateAnimation(msg)
  83. if changed {
  84. return m, cmd
  85. }
  86. return m, nil
  87. }
  88. func (m *BashToolItem) Render(width int) string {
  89. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  90. return m.renderPending()
  91. }
  92. var params tools.BashParams
  93. unmarshalParams(m.ctx.Call.Input, &params)
  94. cmd := strings.ReplaceAll(params.Command, "\n", " ")
  95. cmd = strings.ReplaceAll(cmd, "\t", " ")
  96. if m.ctx.Call.Finished && m.ctx.HasResult() {
  97. var meta tools.BashResponseMetadata
  98. unmarshalParams(m.ctx.Result.Metadata, &meta)
  99. if meta.Background {
  100. return m.renderBackgroundJob(params, meta, width)
  101. }
  102. }
  103. args := NewParamBuilder().
  104. Main(cmd).
  105. Flag("background", params.RunInBackground).
  106. Build()
  107. header := renderToolHeader(&m.ctx, "Bash", width, args...)
  108. if result, done := renderEarlyState(&m.ctx, header, width); done {
  109. return result
  110. }
  111. var meta tools.BashResponseMetadata
  112. unmarshalParams(m.ctx.Result.Metadata, &meta)
  113. output := meta.Output
  114. if output == "" && m.ctx.Result.Content != tools.BashNoOutput {
  115. output = m.ctx.Result.Content
  116. }
  117. if output == "" {
  118. return header
  119. }
  120. body := renderPlainContent(output, width-2, m.ctx.Styles, &m.toolItem)
  121. return joinHeaderBody(header, body, m.ctx.Styles)
  122. }
  123. func (m *BashToolItem) renderBackgroundJob(params tools.BashParams, meta tools.BashResponseMetadata, width int) string {
  124. description := cmp.Or(meta.Description, params.Command)
  125. header := renderJobHeader(&m.ctx, "Start", meta.ShellID, description, width)
  126. if m.ctx.IsNested {
  127. return header
  128. }
  129. if result, done := renderEarlyState(&m.ctx, header, width); done {
  130. return result
  131. }
  132. content := "Command: " + params.Command + "\n" + m.ctx.Result.Content
  133. body := renderPlainContent(content, width-2, m.ctx.Styles, &m.toolItem)
  134. return joinHeaderBody(header, body, m.ctx.Styles)
  135. }
  136. // JobOutputToolItem renders job output retrieval.
  137. type JobOutputToolItem struct {
  138. toolItem
  139. }
  140. func NewJobOutputToolItem(ctx ToolCallContext) *JobOutputToolItem {
  141. return &JobOutputToolItem{
  142. toolItem: newToolItem(ctx),
  143. }
  144. }
  145. func (m *JobOutputToolItem) Render(width int) string {
  146. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  147. return m.renderPending()
  148. }
  149. var params tools.JobOutputParams
  150. unmarshalParams(m.ctx.Call.Input, &params)
  151. var meta tools.JobOutputResponseMetadata
  152. var description string
  153. if m.ctx.Result != nil && m.ctx.Result.Metadata != "" {
  154. unmarshalParams(m.ctx.Result.Metadata, &meta)
  155. description = cmp.Or(meta.Description, meta.Command)
  156. }
  157. header := renderJobHeader(&m.ctx, "Output", params.ShellID, description, width)
  158. if m.ctx.IsNested {
  159. return header
  160. }
  161. if result, done := renderEarlyState(&m.ctx, header, width); done {
  162. return result
  163. }
  164. body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
  165. return joinHeaderBody(header, body, m.ctx.Styles)
  166. }
  167. // JobKillToolItem renders job termination.
  168. type JobKillToolItem struct {
  169. toolItem
  170. }
  171. func NewJobKillToolItem(ctx ToolCallContext) *JobKillToolItem {
  172. return &JobKillToolItem{
  173. toolItem: newToolItem(ctx),
  174. }
  175. }
  176. func (m *JobKillToolItem) Render(width int) string {
  177. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  178. return m.renderPending()
  179. }
  180. var params tools.JobKillParams
  181. unmarshalParams(m.ctx.Call.Input, &params)
  182. var meta tools.JobKillResponseMetadata
  183. var description string
  184. if m.ctx.Result != nil && m.ctx.Result.Metadata != "" {
  185. unmarshalParams(m.ctx.Result.Metadata, &meta)
  186. description = cmp.Or(meta.Description, meta.Command)
  187. }
  188. header := renderJobHeader(&m.ctx, "Kill", params.ShellID, description, width)
  189. if m.ctx.IsNested {
  190. return header
  191. }
  192. if result, done := renderEarlyState(&m.ctx, header, width); done {
  193. return result
  194. }
  195. body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
  196. return joinHeaderBody(header, body, m.ctx.Styles)
  197. }
  198. // renderJobHeader builds a job-specific header with action and PID.
  199. func renderJobHeader(ctx *ToolCallContext, action, pid, description string, width int) string {
  200. sty := ctx.Styles
  201. icon := renderToolIcon(ctx.Status(), sty)
  202. jobPart := sty.Tool.JobToolName.Render("Job")
  203. actionPart := sty.Tool.JobAction.Render("(" + action + ")")
  204. pidPart := sty.Tool.JobPID.Render("PID " + pid)
  205. prefix := fmt.Sprintf("%s %s %s %s", icon, jobPart, actionPart, pidPart)
  206. if description == "" {
  207. return prefix
  208. }
  209. descPart := " " + sty.Tool.JobDescription.Render(description)
  210. fullHeader := prefix + descPart
  211. if lipgloss.Width(fullHeader) > width {
  212. availableWidth := width - lipgloss.Width(prefix) - 1
  213. if availableWidth < 10 {
  214. return prefix
  215. }
  216. descPart = " " + sty.Tool.JobDescription.Render(truncateText(description, availableWidth))
  217. fullHeader = prefix + descPart
  218. }
  219. return fullHeader
  220. }
  221. // -----------------------------------------------------------------------------
  222. // File Tools
  223. // -----------------------------------------------------------------------------
  224. // ViewToolItem renders file viewing with syntax highlighting.
  225. type ViewToolItem struct {
  226. toolItem
  227. }
  228. func NewViewToolItem(ctx ToolCallContext) *ViewToolItem {
  229. return &ViewToolItem{
  230. toolItem: newToolItem(ctx),
  231. }
  232. }
  233. func (m *ViewToolItem) Render(width int) string {
  234. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  235. return m.renderPending()
  236. }
  237. var params tools.ViewParams
  238. unmarshalParams(m.ctx.Call.Input, &params)
  239. file := fsext.PrettyPath(params.FilePath)
  240. args := NewParamBuilder().
  241. Main(file).
  242. KeyValue("limit", formatNonZero(params.Limit)).
  243. KeyValue("offset", formatNonZero(params.Offset)).
  244. Build()
  245. header := renderToolHeader(&m.ctx, "View", width, args...)
  246. if result, done := renderEarlyState(&m.ctx, header, width); done {
  247. return result
  248. }
  249. if m.ctx.Result.Data != "" && strings.HasPrefix(m.ctx.Result.MIMEType, "image/") {
  250. body := renderImageContent(m.ctx.Result.Data, m.ctx.Result.MIMEType, "", m.ctx.Styles)
  251. return joinHeaderBody(header, body, m.ctx.Styles)
  252. }
  253. var meta tools.ViewResponseMetadata
  254. unmarshalParams(m.ctx.Result.Metadata, &meta)
  255. body := renderCodeContent(meta.FilePath, meta.Content, params.Offset, width-2, m.ctx.Styles, &m.toolItem)
  256. return joinHeaderBody(header, body, m.ctx.Styles)
  257. }
  258. // EditToolItem renders file editing with diff visualization.
  259. type EditToolItem struct {
  260. toolItem
  261. }
  262. func NewEditToolItem(ctx ToolCallContext) *EditToolItem {
  263. return &EditToolItem{
  264. toolItem: newToolItem(ctx),
  265. }
  266. }
  267. func (m *EditToolItem) Render(width int) string {
  268. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  269. return m.renderPending()
  270. }
  271. var params tools.EditParams
  272. unmarshalParams(m.ctx.Call.Input, &params)
  273. file := fsext.PrettyPath(params.FilePath)
  274. args := NewParamBuilder().Main(file).Build()
  275. header := renderToolHeader(&m.ctx, "Edit", width, args...)
  276. if result, done := renderEarlyState(&m.ctx, header, width); done {
  277. return result
  278. }
  279. var meta tools.EditResponseMetadata
  280. if err := unmarshalParams(m.ctx.Result.Metadata, &meta); err != nil {
  281. body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, nil)
  282. return joinHeaderBody(header, body, m.ctx.Styles)
  283. }
  284. body := renderDiffContent(file, meta.OldContent, meta.NewContent, width-2, m.ctx.Styles, &m.toolItem)
  285. return joinHeaderBody(header, body, m.ctx.Styles)
  286. }
  287. // MultiEditToolItem renders multiple file edits with diff visualization.
  288. type MultiEditToolItem struct {
  289. toolItem
  290. }
  291. func NewMultiEditToolItem(ctx ToolCallContext) *MultiEditToolItem {
  292. return &MultiEditToolItem{
  293. toolItem: newToolItem(ctx),
  294. }
  295. }
  296. func (m *MultiEditToolItem) Render(width int) string {
  297. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  298. return m.renderPending()
  299. }
  300. var params tools.MultiEditParams
  301. unmarshalParams(m.ctx.Call.Input, &params)
  302. file := fsext.PrettyPath(params.FilePath)
  303. args := NewParamBuilder().
  304. Main(file).
  305. KeyValue("edits", fmt.Sprintf("%d", len(params.Edits))).
  306. Build()
  307. header := renderToolHeader(&m.ctx, "Multi-Edit", width, args...)
  308. if result, done := renderEarlyState(&m.ctx, header, width); done {
  309. return result
  310. }
  311. var meta tools.MultiEditResponseMetadata
  312. if err := unmarshalParams(m.ctx.Result.Metadata, &meta); err != nil {
  313. body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, nil)
  314. return joinHeaderBody(header, body, m.ctx.Styles)
  315. }
  316. body := renderDiffContent(file, meta.OldContent, meta.NewContent, width-2, m.ctx.Styles, &m.toolItem)
  317. if len(meta.EditsFailed) > 0 {
  318. sty := m.ctx.Styles
  319. noteTag := sty.Tool.NoteTag.Render("Note")
  320. noteMsg := fmt.Sprintf("%d of %d edits succeeded", meta.EditsApplied, len(params.Edits))
  321. note := fmt.Sprintf("%s %s", noteTag, sty.Tool.NoteMessage.Render(noteMsg))
  322. body = lipgloss.JoinVertical(lipgloss.Left, body, "", note)
  323. }
  324. return joinHeaderBody(header, body, m.ctx.Styles)
  325. }
  326. // WriteToolItem renders file writing with syntax-highlighted content preview.
  327. type WriteToolItem struct {
  328. toolItem
  329. }
  330. func NewWriteToolItem(ctx ToolCallContext) *WriteToolItem {
  331. return &WriteToolItem{
  332. toolItem: newToolItem(ctx),
  333. }
  334. }
  335. func (m *WriteToolItem) Render(width int) string {
  336. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  337. return m.renderPending()
  338. }
  339. var params tools.WriteParams
  340. unmarshalParams(m.ctx.Call.Input, &params)
  341. file := fsext.PrettyPath(params.FilePath)
  342. args := NewParamBuilder().Main(file).Build()
  343. header := renderToolHeader(&m.ctx, "Write", width, args...)
  344. if result, done := renderEarlyState(&m.ctx, header, width); done {
  345. return result
  346. }
  347. body := renderCodeContent(file, params.Content, 0, width-2, m.ctx.Styles, &m.toolItem)
  348. return joinHeaderBody(header, body, m.ctx.Styles)
  349. }
  350. // -----------------------------------------------------------------------------
  351. // Search Tools
  352. // -----------------------------------------------------------------------------
  353. // GlobToolItem renders glob file pattern matching results.
  354. type GlobToolItem struct {
  355. toolItem
  356. }
  357. func NewGlobToolItem(ctx ToolCallContext) *GlobToolItem {
  358. return &GlobToolItem{
  359. toolItem: newToolItem(ctx),
  360. }
  361. }
  362. func (m *GlobToolItem) Render(width int) string {
  363. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  364. return m.renderPending()
  365. }
  366. var params tools.GlobParams
  367. unmarshalParams(m.ctx.Call.Input, &params)
  368. args := NewParamBuilder().
  369. Main(params.Pattern).
  370. KeyValue("path", fsext.PrettyPath(params.Path)).
  371. Build()
  372. header := renderToolHeader(&m.ctx, "Glob", width, args...)
  373. if result, done := renderEarlyState(&m.ctx, header, width); done {
  374. return result
  375. }
  376. body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
  377. return joinHeaderBody(header, body, m.ctx.Styles)
  378. }
  379. // GrepToolItem renders grep content search results.
  380. type GrepToolItem struct {
  381. toolItem
  382. }
  383. func NewGrepToolItem(ctx ToolCallContext) *GrepToolItem {
  384. return &GrepToolItem{
  385. toolItem: newToolItem(ctx),
  386. }
  387. }
  388. func (m *GrepToolItem) Render(width int) string {
  389. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  390. return m.renderPending()
  391. }
  392. var params tools.GrepParams
  393. unmarshalParams(m.ctx.Call.Input, &params)
  394. args := NewParamBuilder().
  395. Main(params.Pattern).
  396. KeyValue("path", fsext.PrettyPath(params.Path)).
  397. KeyValue("include", params.Include).
  398. Flag("literal", params.LiteralText).
  399. Build()
  400. header := renderToolHeader(&m.ctx, "Grep", width, args...)
  401. if result, done := renderEarlyState(&m.ctx, header, width); done {
  402. return result
  403. }
  404. body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
  405. return joinHeaderBody(header, body, m.ctx.Styles)
  406. }
  407. // LSToolItem renders directory listing results.
  408. type LSToolItem struct {
  409. toolItem
  410. }
  411. func NewLSToolItem(ctx ToolCallContext) *LSToolItem {
  412. return &LSToolItem{
  413. toolItem: newToolItem(ctx),
  414. }
  415. }
  416. func (m *LSToolItem) Render(width int) string {
  417. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  418. return m.renderPending()
  419. }
  420. var params tools.LSParams
  421. unmarshalParams(m.ctx.Call.Input, &params)
  422. path := cmp.Or(params.Path, ".")
  423. path = fsext.PrettyPath(path)
  424. args := NewParamBuilder().Main(path).Build()
  425. header := renderToolHeader(&m.ctx, "List", width, args...)
  426. if result, done := renderEarlyState(&m.ctx, header, width); done {
  427. return result
  428. }
  429. body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
  430. return joinHeaderBody(header, body, m.ctx.Styles)
  431. }
  432. // SourcegraphToolItem renders code search results.
  433. type SourcegraphToolItem struct {
  434. toolItem
  435. }
  436. func NewSourcegraphToolItem(ctx ToolCallContext) *SourcegraphToolItem {
  437. return &SourcegraphToolItem{
  438. toolItem: newToolItem(ctx),
  439. }
  440. }
  441. func (m *SourcegraphToolItem) Render(width int) string {
  442. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  443. return m.renderPending()
  444. }
  445. var params tools.SourcegraphParams
  446. unmarshalParams(m.ctx.Call.Input, &params)
  447. args := NewParamBuilder().
  448. Main(params.Query).
  449. KeyValue("count", formatNonZero(params.Count)).
  450. KeyValue("context", formatNonZero(params.ContextWindow)).
  451. Build()
  452. header := renderToolHeader(&m.ctx, "Sourcegraph", width, args...)
  453. if result, done := renderEarlyState(&m.ctx, header, width); done {
  454. return result
  455. }
  456. body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
  457. return joinHeaderBody(header, body, m.ctx.Styles)
  458. }
  459. // -----------------------------------------------------------------------------
  460. // Fetch Tools
  461. // -----------------------------------------------------------------------------
  462. // FetchToolItem renders URL fetching with format-specific content display.
  463. type FetchToolItem struct {
  464. toolItem
  465. }
  466. func NewFetchToolItem(ctx ToolCallContext) *FetchToolItem {
  467. return &FetchToolItem{
  468. toolItem: newToolItem(ctx),
  469. }
  470. }
  471. func (m *FetchToolItem) Render(width int) string {
  472. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  473. return m.renderPending()
  474. }
  475. var params tools.FetchParams
  476. unmarshalParams(m.ctx.Call.Input, &params)
  477. args := NewParamBuilder().
  478. Main(params.URL).
  479. KeyValue("format", params.Format).
  480. KeyValue("timeout", formatTimeout(params.Timeout)).
  481. Build()
  482. header := renderToolHeader(&m.ctx, "Fetch", width, args...)
  483. if result, done := renderEarlyState(&m.ctx, header, width); done {
  484. return result
  485. }
  486. file := "fetch.md"
  487. switch params.Format {
  488. case "text":
  489. file = "fetch.txt"
  490. case "html":
  491. file = "fetch.html"
  492. }
  493. body := renderCodeContent(file, m.ctx.Result.Content, 0, width-2, m.ctx.Styles, &m.toolItem)
  494. return joinHeaderBody(header, body, m.ctx.Styles)
  495. }
  496. // AgenticFetchToolItem renders agentic URL fetching with nested tool calls.
  497. type AgenticFetchToolItem struct {
  498. toolItem
  499. }
  500. func NewAgenticFetchToolItem(ctx ToolCallContext) *AgenticFetchToolItem {
  501. return &AgenticFetchToolItem{
  502. toolItem: newToolItem(ctx),
  503. }
  504. }
  505. func (m *AgenticFetchToolItem) Render(width int) string {
  506. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  507. return m.renderPending()
  508. }
  509. var params tools.AgenticFetchParams
  510. unmarshalParams(m.ctx.Call.Input, &params)
  511. var args []string
  512. if params.URL != "" {
  513. args = NewParamBuilder().Main(params.URL).Build()
  514. }
  515. header := renderToolHeader(&m.ctx, "Agentic Fetch", width, args...)
  516. // Render with nested tool calls tree
  517. body := renderAgentBody(&m.ctx, params.Prompt, "Prompt", header, width)
  518. return body
  519. }
  520. // WebFetchToolItem renders web page fetching.
  521. type WebFetchToolItem struct {
  522. toolItem
  523. }
  524. func NewWebFetchToolItem(ctx ToolCallContext) *WebFetchToolItem {
  525. return &WebFetchToolItem{
  526. toolItem: newToolItem(ctx),
  527. }
  528. }
  529. func (m *WebFetchToolItem) Render(width int) string {
  530. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  531. return m.renderPending()
  532. }
  533. var params tools.WebFetchParams
  534. unmarshalParams(m.ctx.Call.Input, &params)
  535. args := NewParamBuilder().Main(params.URL).Build()
  536. header := renderToolHeader(&m.ctx, "Fetch", width, args...)
  537. if result, done := renderEarlyState(&m.ctx, header, width); done {
  538. return result
  539. }
  540. body := renderMarkdownContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
  541. return joinHeaderBody(header, body, m.ctx.Styles)
  542. }
  543. // WebSearchToolItem renders web search results.
  544. type WebSearchToolItem struct {
  545. toolItem
  546. }
  547. func NewWebSearchToolItem(ctx ToolCallContext) *WebSearchToolItem {
  548. return &WebSearchToolItem{
  549. toolItem: newToolItem(ctx),
  550. }
  551. }
  552. func (m *WebSearchToolItem) Render(width int) string {
  553. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  554. return m.renderPending()
  555. }
  556. var params tools.WebSearchParams
  557. unmarshalParams(m.ctx.Call.Input, &params)
  558. args := NewParamBuilder().Main(params.Query).Build()
  559. header := renderToolHeader(&m.ctx, "Search", width, args...)
  560. if result, done := renderEarlyState(&m.ctx, header, width); done {
  561. return result
  562. }
  563. body := renderMarkdownContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
  564. return joinHeaderBody(header, body, m.ctx.Styles)
  565. }
  566. // DownloadToolItem renders file downloading.
  567. type DownloadToolItem struct {
  568. toolItem
  569. }
  570. func NewDownloadToolItem(ctx ToolCallContext) *DownloadToolItem {
  571. return &DownloadToolItem{
  572. toolItem: newToolItem(ctx),
  573. }
  574. }
  575. func (m *DownloadToolItem) Render(width int) string {
  576. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  577. return m.renderPending()
  578. }
  579. var params tools.DownloadParams
  580. unmarshalParams(m.ctx.Call.Input, &params)
  581. args := NewParamBuilder().
  582. Main(params.URL).
  583. KeyValue("file_path", fsext.PrettyPath(params.FilePath)).
  584. KeyValue("timeout", formatTimeout(params.Timeout)).
  585. Build()
  586. header := renderToolHeader(&m.ctx, "Download", width, args...)
  587. if result, done := renderEarlyState(&m.ctx, header, width); done {
  588. return result
  589. }
  590. body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
  591. return joinHeaderBody(header, body, m.ctx.Styles)
  592. }
  593. // -----------------------------------------------------------------------------
  594. // LSP Tools
  595. // -----------------------------------------------------------------------------
  596. // DiagnosticsToolItem renders project-wide diagnostic information.
  597. type DiagnosticsToolItem struct {
  598. toolItem
  599. }
  600. func NewDiagnosticsToolItem(ctx ToolCallContext) *DiagnosticsToolItem {
  601. return &DiagnosticsToolItem{
  602. toolItem: newToolItem(ctx),
  603. }
  604. }
  605. func (m *DiagnosticsToolItem) Render(width int) string {
  606. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  607. return m.renderPending()
  608. }
  609. args := NewParamBuilder().Main("project").Build()
  610. header := renderToolHeader(&m.ctx, "Diagnostics", width, args...)
  611. if result, done := renderEarlyState(&m.ctx, header, width); done {
  612. return result
  613. }
  614. body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
  615. return joinHeaderBody(header, body, m.ctx.Styles)
  616. }
  617. // ReferencesToolItem renders LSP references search results.
  618. type ReferencesToolItem struct {
  619. toolItem
  620. }
  621. func NewReferencesToolItem(ctx ToolCallContext) *ReferencesToolItem {
  622. return &ReferencesToolItem{
  623. toolItem: newToolItem(ctx),
  624. }
  625. }
  626. func (m *ReferencesToolItem) Render(width int) string {
  627. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  628. return m.renderPending()
  629. }
  630. var params tools.ReferencesParams
  631. unmarshalParams(m.ctx.Call.Input, &params)
  632. args := NewParamBuilder().
  633. Main(params.Symbol).
  634. KeyValue("path", fsext.PrettyPath(params.Path)).
  635. Build()
  636. header := renderToolHeader(&m.ctx, "References", width, args...)
  637. if result, done := renderEarlyState(&m.ctx, header, width); done {
  638. return result
  639. }
  640. body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
  641. return joinHeaderBody(header, body, m.ctx.Styles)
  642. }
  643. // -----------------------------------------------------------------------------
  644. // Misc Tools
  645. // -----------------------------------------------------------------------------
  646. // TodosToolItem renders todo list management.
  647. type TodosToolItem struct {
  648. toolItem
  649. }
  650. func NewTodosToolItem(ctx ToolCallContext) *TodosToolItem {
  651. return &TodosToolItem{
  652. toolItem: newToolItem(ctx),
  653. }
  654. }
  655. func (m *TodosToolItem) Render(width int) string {
  656. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  657. return m.renderPending()
  658. }
  659. sty := m.ctx.Styles
  660. var params tools.TodosParams
  661. var meta tools.TodosResponseMetadata
  662. var headerText string
  663. var body string
  664. // Parse params for pending state
  665. if err := unmarshalParams(m.ctx.Call.Input, &params); err == nil {
  666. completedCount := 0
  667. inProgressTask := ""
  668. for _, todo := range params.Todos {
  669. if todo.Status == "completed" {
  670. completedCount++
  671. }
  672. if todo.Status == "in_progress" {
  673. inProgressTask = cmp.Or(todo.ActiveForm, todo.Content)
  674. }
  675. }
  676. // Default display from params
  677. ratio := sty.Tool.JobAction.Render(fmt.Sprintf("%d/%d", completedCount, len(params.Todos)))
  678. headerText = ratio
  679. if inProgressTask != "" {
  680. headerText = fmt.Sprintf("%s · %s", ratio, inProgressTask)
  681. }
  682. // If we have metadata, use it for richer display
  683. if m.ctx.Result != nil && m.ctx.Result.Metadata != "" {
  684. if err := unmarshalParams(m.ctx.Result.Metadata, &meta); err == nil {
  685. headerText, body = m.formatTodosFromMeta(meta, width)
  686. }
  687. }
  688. }
  689. args := NewParamBuilder().Main(headerText).Build()
  690. header := renderToolHeader(&m.ctx, "To-Do", width, args...)
  691. if result, done := renderEarlyState(&m.ctx, header, width); done {
  692. return result
  693. }
  694. if body == "" {
  695. return header
  696. }
  697. return joinHeaderBody(header, body, m.ctx.Styles)
  698. }
  699. func (m *TodosToolItem) formatTodosFromMeta(meta tools.TodosResponseMetadata, width int) (string, string) {
  700. sty := m.ctx.Styles
  701. var headerText, body string
  702. if meta.IsNew {
  703. if meta.JustStarted != "" {
  704. headerText = fmt.Sprintf("created %d todos, starting first", meta.Total)
  705. } else {
  706. headerText = fmt.Sprintf("created %d todos", meta.Total)
  707. }
  708. body = formatTodosList(meta.Todos, width, sty)
  709. } else {
  710. hasCompleted := len(meta.JustCompleted) > 0
  711. hasStarted := meta.JustStarted != ""
  712. allCompleted := meta.Completed == meta.Total
  713. ratio := sty.Tool.JobAction.Render(fmt.Sprintf("%d/%d", meta.Completed, meta.Total))
  714. if hasCompleted && hasStarted {
  715. text := sty.Tool.JobDescription.Render(fmt.Sprintf(" · completed %d, starting next", len(meta.JustCompleted)))
  716. headerText = ratio + text
  717. } else if hasCompleted {
  718. text := " · completed all"
  719. if !allCompleted {
  720. text = fmt.Sprintf(" · completed %d", len(meta.JustCompleted))
  721. }
  722. headerText = ratio + sty.Tool.JobDescription.Render(text)
  723. } else if hasStarted {
  724. headerText = ratio + sty.Tool.JobDescription.Render(" · starting task")
  725. } else {
  726. headerText = ratio
  727. }
  728. if allCompleted {
  729. body = formatTodosList(meta.Todos, width, sty)
  730. } else if meta.JustStarted != "" {
  731. body = sty.Tool.IconSuccess.String() + " " + sty.Base.Render(meta.JustStarted)
  732. }
  733. }
  734. return headerText, body
  735. }
  736. // AgentToolItem renders agent task execution with nested tool calls.
  737. type AgentToolItem struct {
  738. toolItem
  739. }
  740. func NewAgentToolItem(ctx ToolCallContext) *AgentToolItem {
  741. return &AgentToolItem{
  742. toolItem: newToolItem(ctx),
  743. }
  744. }
  745. func (m *AgentToolItem) Render(width int) string {
  746. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  747. return m.renderPending()
  748. }
  749. var params agent.AgentParams
  750. unmarshalParams(m.ctx.Call.Input, &params)
  751. header := renderToolHeader(&m.ctx, "Agent", width)
  752. body := renderAgentBody(&m.ctx, params.Prompt, "Task", header, width)
  753. return body
  754. }
  755. // renderAgentBody renders agent/agentic_fetch body with prompt tag and nested calls tree.
  756. func renderAgentBody(ctx *ToolCallContext, prompt, tagLabel, header string, width int) string {
  757. sty := ctx.Styles
  758. if ctx.Cancelled {
  759. if result, done := renderEarlyState(ctx, header, width); done {
  760. return result
  761. }
  762. }
  763. // Build prompt tag
  764. prompt = strings.ReplaceAll(prompt, "\n", " ")
  765. taskTag := sty.Tool.AgentTaskTag.Render(tagLabel)
  766. tagWidth := lipgloss.Width(taskTag)
  767. remainingWidth := min(width-tagWidth-2, 120-tagWidth-2)
  768. promptStyled := sty.Tool.AgentPrompt.Width(remainingWidth).Render(prompt)
  769. headerWithPrompt := lipgloss.JoinVertical(
  770. lipgloss.Left,
  771. header,
  772. "",
  773. lipgloss.JoinHorizontal(lipgloss.Left, taskTag, " ", promptStyled),
  774. )
  775. // Build tree with nested tool calls
  776. childTools := tree.Root(headerWithPrompt)
  777. for _, nestedCtx := range ctx.NestedCalls {
  778. nestedCtx.IsNested = true
  779. nestedItem := NewToolItem(nestedCtx)
  780. childTools.Child(nestedItem.Render(remainingWidth))
  781. }
  782. parts := []string{
  783. childTools.Enumerator(roundedEnumerator(2, tagWidth-5)).String(),
  784. }
  785. if !ctx.HasResult() {
  786. parts = append(parts, "", sty.Tool.StateWaiting.Render("Working..."))
  787. }
  788. treeOutput := lipgloss.JoinVertical(lipgloss.Left, parts...)
  789. if !ctx.HasResult() {
  790. return treeOutput
  791. }
  792. body := renderMarkdownContent(ctx.Result.Content, width-2, sty, nil)
  793. return joinHeaderBody(treeOutput, body, sty)
  794. }
  795. // roundedEnumerator creates a tree enumerator with rounded connectors.
  796. func roundedEnumerator(lPadding, lineWidth int) tree.Enumerator {
  797. if lineWidth == 0 {
  798. lineWidth = 2
  799. }
  800. if lPadding == 0 {
  801. lPadding = 1
  802. }
  803. return func(children tree.Children, index int) string {
  804. line := strings.Repeat("─", lineWidth)
  805. padding := strings.Repeat(" ", lPadding)
  806. if children.Length()-1 == index {
  807. return padding + "╰" + line
  808. }
  809. return padding + "├" + line
  810. }
  811. }
  812. // GenericToolItem renders unknown tool types with basic parameter display.
  813. type GenericToolItem struct {
  814. toolItem
  815. }
  816. func NewGenericToolItem(ctx ToolCallContext) *GenericToolItem {
  817. return &GenericToolItem{
  818. toolItem: newToolItem(ctx),
  819. }
  820. }
  821. func (m *GenericToolItem) Render(width int) string {
  822. if !m.ctx.Call.Finished && !m.ctx.Cancelled {
  823. return m.renderPending()
  824. }
  825. name := prettifyToolName(m.ctx.Call.Name)
  826. // Handle media content
  827. if m.ctx.Result != nil && m.ctx.Result.Data != "" {
  828. if strings.HasPrefix(m.ctx.Result.MIMEType, "image/") {
  829. args := NewParamBuilder().Main(m.toolItem.ctx.Call.Input).Build()
  830. header := renderToolHeader(&m.ctx, name, width, args...)
  831. body := renderImageContent(m.ctx.Result.Data, m.ctx.Result.MIMEType, m.ctx.Result.Content, m.ctx.Styles)
  832. return joinHeaderBody(header, body, m.ctx.Styles)
  833. }
  834. args := NewParamBuilder().Main(m.toolItem.ctx.Call.Input).Build()
  835. header := renderToolHeader(&m.ctx, name, width, args...)
  836. body := renderMediaContent(m.ctx.Result.MIMEType, m.ctx.Result.Content, m.ctx.Styles)
  837. return joinHeaderBody(header, body, m.ctx.Styles)
  838. }
  839. args := NewParamBuilder().Main(m.toolItem.ctx.Call.Input).Build()
  840. header := renderToolHeader(&m.ctx, name, width, args...)
  841. if result, done := renderEarlyState(&m.ctx, header, width); done {
  842. return result
  843. }
  844. if m.ctx.Result == nil || m.ctx.Result.Content == "" {
  845. return header
  846. }
  847. body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
  848. return joinHeaderBody(header, body, m.ctx.Styles)
  849. }
  850. // -----------------------------------------------------------------------------
  851. // Helper Functions
  852. // -----------------------------------------------------------------------------
  853. // prettifyToolName converts tool names to display-friendly format.
  854. func prettifyToolName(name string) string {
  855. switch name {
  856. case agent.AgentToolName:
  857. return "Agent"
  858. case tools.BashToolName:
  859. return "Bash"
  860. case tools.JobOutputToolName:
  861. return "Job: Output"
  862. case tools.JobKillToolName:
  863. return "Job: Kill"
  864. case tools.DownloadToolName:
  865. return "Download"
  866. case tools.EditToolName:
  867. return "Edit"
  868. case tools.MultiEditToolName:
  869. return "Multi-Edit"
  870. case tools.FetchToolName:
  871. return "Fetch"
  872. case tools.AgenticFetchToolName:
  873. return "Agentic Fetch"
  874. case tools.WebFetchToolName:
  875. return "Fetch"
  876. case tools.WebSearchToolName:
  877. return "Search"
  878. case tools.GlobToolName:
  879. return "Glob"
  880. case tools.GrepToolName:
  881. return "Grep"
  882. case tools.LSToolName:
  883. return "List"
  884. case tools.SourcegraphToolName:
  885. return "Sourcegraph"
  886. case tools.TodosToolName:
  887. return "To-Do"
  888. case tools.ViewToolName:
  889. return "View"
  890. case tools.WriteToolName:
  891. return "Write"
  892. case tools.DiagnosticsToolName:
  893. return "Diagnostics"
  894. case tools.ReferencesToolName:
  895. return "References"
  896. default:
  897. // Handle MCP tools and others
  898. name = strings.TrimPrefix(name, "mcp_")
  899. if name == "" {
  900. return "Tool"
  901. }
  902. return strings.ToUpper(name[:1]) + name[1:]
  903. }
  904. }
  905. // formatTimeout converts timeout seconds to duration string.
  906. func formatTimeout(timeout int) string {
  907. if timeout == 0 {
  908. return ""
  909. }
  910. return (time.Duration(timeout) * time.Second).String()
  911. }
  912. // truncateText truncates text to fit within width with ellipsis.
  913. func truncateText(s string, width int) string {
  914. if lipgloss.Width(s) <= width {
  915. return s
  916. }
  917. for i := len(s) - 1; i >= 0; i-- {
  918. truncated := s[:i] + "…"
  919. if lipgloss.Width(truncated) <= width {
  920. return truncated
  921. }
  922. }
  923. return "…"
  924. }
  925. // Update implements list.Updatable.
  926. func (m *JobOutputToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  927. cmd, changed := m.updateAnimation(msg)
  928. if changed {
  929. return m, cmd
  930. }
  931. return m, nil
  932. }
  933. // Update implements list.Updatable.
  934. func (m *JobKillToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  935. cmd, changed := m.updateAnimation(msg)
  936. if changed {
  937. return m, cmd
  938. }
  939. return m, nil
  940. }
  941. // Update implements list.Updatable.
  942. func (m *ViewToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  943. cmd, changed := m.updateAnimation(msg)
  944. if changed {
  945. return m, cmd
  946. }
  947. return m, nil
  948. }
  949. // Update implements list.Updatable.
  950. func (m *EditToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  951. cmd, changed := m.updateAnimation(msg)
  952. if changed {
  953. return m, cmd
  954. }
  955. return m, nil
  956. }
  957. // Update implements list.Updatable.
  958. func (m *MultiEditToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  959. cmd, changed := m.updateAnimation(msg)
  960. if changed {
  961. return m, cmd
  962. }
  963. return m, nil
  964. }
  965. // Update implements list.Updatable.
  966. func (m *WriteToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  967. cmd, changed := m.updateAnimation(msg)
  968. if changed {
  969. return m, cmd
  970. }
  971. return m, nil
  972. }
  973. // Update implements list.Updatable.
  974. func (m *GlobToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  975. cmd, changed := m.updateAnimation(msg)
  976. if changed {
  977. return m, cmd
  978. }
  979. return m, nil
  980. }
  981. // Update implements list.Updatable.
  982. func (m *GrepToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  983. cmd, changed := m.updateAnimation(msg)
  984. if changed {
  985. return m, cmd
  986. }
  987. return m, nil
  988. }
  989. // Update implements list.Updatable.
  990. func (m *LSToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  991. cmd, changed := m.updateAnimation(msg)
  992. if changed {
  993. return m, cmd
  994. }
  995. return m, nil
  996. }
  997. // Update implements list.Updatable.
  998. func (m *SourcegraphToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  999. cmd, changed := m.updateAnimation(msg)
  1000. if changed {
  1001. return m, cmd
  1002. }
  1003. return m, nil
  1004. }
  1005. // Update implements list.Updatable.
  1006. func (m *FetchToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  1007. cmd, changed := m.updateAnimation(msg)
  1008. if changed {
  1009. return m, cmd
  1010. }
  1011. return m, nil
  1012. }
  1013. // Update implements list.Updatable.
  1014. func (m *AgenticFetchToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  1015. cmd, changed := m.updateAnimation(msg)
  1016. if changed {
  1017. return m, cmd
  1018. }
  1019. return m, nil
  1020. }
  1021. // Update implements list.Updatable.
  1022. func (m *WebFetchToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  1023. cmd, changed := m.updateAnimation(msg)
  1024. if changed {
  1025. return m, cmd
  1026. }
  1027. return m, nil
  1028. }
  1029. // Update implements list.Updatable.
  1030. func (m *WebSearchToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  1031. cmd, changed := m.updateAnimation(msg)
  1032. if changed {
  1033. return m, cmd
  1034. }
  1035. return m, nil
  1036. }
  1037. // Update implements list.Updatable.
  1038. func (m *DownloadToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  1039. cmd, changed := m.updateAnimation(msg)
  1040. if changed {
  1041. return m, cmd
  1042. }
  1043. return m, nil
  1044. }
  1045. // Update implements list.Updatable.
  1046. func (m *DiagnosticsToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  1047. cmd, changed := m.updateAnimation(msg)
  1048. if changed {
  1049. return m, cmd
  1050. }
  1051. return m, nil
  1052. }
  1053. // Update implements list.Updatable.
  1054. func (m *ReferencesToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  1055. cmd, changed := m.updateAnimation(msg)
  1056. if changed {
  1057. return m, cmd
  1058. }
  1059. return m, nil
  1060. }
  1061. // Update implements list.Updatable.
  1062. func (m *TodosToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  1063. cmd, changed := m.updateAnimation(msg)
  1064. if changed {
  1065. return m, cmd
  1066. }
  1067. return m, nil
  1068. }
  1069. // Update implements list.Updatable.
  1070. func (m *AgentToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  1071. cmd, changed := m.updateAnimation(msg)
  1072. if changed {
  1073. return m, cmd
  1074. }
  1075. return m, nil
  1076. }
  1077. // Update implements list.Updatable.
  1078. func (m *GenericToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
  1079. cmd, changed := m.updateAnimation(msg)
  1080. if changed {
  1081. return m, cmd
  1082. }
  1083. return m, nil
  1084. }