tools.go 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390
  1. package chat
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "path/filepath"
  6. "strings"
  7. "time"
  8. tea "charm.land/bubbletea/v2"
  9. "charm.land/lipgloss/v2"
  10. "charm.land/lipgloss/v2/tree"
  11. "github.com/charmbracelet/crush/internal/agent"
  12. "github.com/charmbracelet/crush/internal/agent/tools"
  13. "github.com/charmbracelet/crush/internal/diff"
  14. "github.com/charmbracelet/crush/internal/fsext"
  15. "github.com/charmbracelet/crush/internal/message"
  16. "github.com/charmbracelet/crush/internal/stringext"
  17. "github.com/charmbracelet/crush/internal/ui/anim"
  18. "github.com/charmbracelet/crush/internal/ui/common"
  19. "github.com/charmbracelet/crush/internal/ui/styles"
  20. "github.com/charmbracelet/x/ansi"
  21. )
  22. // responseContextHeight limits the number of lines displayed in tool output.
  23. const responseContextHeight = 10
  24. // toolBodyLeftPaddingTotal represents the padding that should be applied to each tool body
  25. const toolBodyLeftPaddingTotal = 2
  26. // ToolStatus represents the current state of a tool call.
  27. type ToolStatus int
  28. const (
  29. ToolStatusAwaitingPermission ToolStatus = iota
  30. ToolStatusRunning
  31. ToolStatusSuccess
  32. ToolStatusError
  33. ToolStatusCanceled
  34. )
  35. // ToolMessageItem represents a tool call message in the chat UI.
  36. type ToolMessageItem interface {
  37. MessageItem
  38. ToolCall() message.ToolCall
  39. SetToolCall(tc message.ToolCall)
  40. SetResult(res *message.ToolResult)
  41. MessageID() string
  42. SetMessageID(id string)
  43. SetStatus(status ToolStatus)
  44. Status() ToolStatus
  45. }
  46. // Compactable is an interface for tool items that can render in a compacted mode.
  47. // When compact mode is enabled, tools render as a compact single-line header.
  48. type Compactable interface {
  49. SetCompact(compact bool)
  50. }
  51. // SpinningState contains the state passed to SpinningFunc for custom spinning logic.
  52. type SpinningState struct {
  53. ToolCall message.ToolCall
  54. Result *message.ToolResult
  55. Status ToolStatus
  56. }
  57. // IsCanceled returns true if the tool status is canceled.
  58. func (s *SpinningState) IsCanceled() bool {
  59. return s.Status == ToolStatusCanceled
  60. }
  61. // HasResult returns true if the result is not nil.
  62. func (s *SpinningState) HasResult() bool {
  63. return s.Result != nil
  64. }
  65. // SpinningFunc is a function type for custom spinning logic.
  66. // Returns true if the tool should show the spinning animation.
  67. type SpinningFunc func(state SpinningState) bool
  68. // DefaultToolRenderContext implements the default [ToolRenderer] interface.
  69. type DefaultToolRenderContext struct{}
  70. // RenderTool implements the [ToolRenderer] interface.
  71. func (d *DefaultToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
  72. return "TODO: Implement Tool Renderer For: " + opts.ToolCall.Name
  73. }
  74. // ToolRenderOpts contains the data needed to render a tool call.
  75. type ToolRenderOpts struct {
  76. ToolCall message.ToolCall
  77. Result *message.ToolResult
  78. Anim *anim.Anim
  79. ExpandedContent bool
  80. Compact bool
  81. IsSpinning bool
  82. Status ToolStatus
  83. }
  84. // IsPending returns true if the tool call is still pending (not finished and
  85. // not canceled).
  86. func (o *ToolRenderOpts) IsPending() bool {
  87. return !o.ToolCall.Finished && !o.IsCanceled()
  88. }
  89. // IsCanceled returns true if the tool status is canceled.
  90. func (o *ToolRenderOpts) IsCanceled() bool {
  91. return o.Status == ToolStatusCanceled
  92. }
  93. // HasResult returns true if the result is not nil.
  94. func (o *ToolRenderOpts) HasResult() bool {
  95. return o.Result != nil
  96. }
  97. // HasEmptyResult returns true if the result is nil or has empty content.
  98. func (o *ToolRenderOpts) HasEmptyResult() bool {
  99. return o.Result == nil || o.Result.Content == ""
  100. }
  101. // ToolRenderer represents an interface for rendering tool calls.
  102. type ToolRenderer interface {
  103. RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string
  104. }
  105. // ToolRendererFunc is a function type that implements the [ToolRenderer] interface.
  106. type ToolRendererFunc func(sty *styles.Styles, width int, opts *ToolRenderOpts) string
  107. // RenderTool implements the ToolRenderer interface.
  108. func (f ToolRendererFunc) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
  109. return f(sty, width, opts)
  110. }
  111. // baseToolMessageItem represents a tool call message that can be displayed in the UI.
  112. type baseToolMessageItem struct {
  113. *highlightableMessageItem
  114. *cachedMessageItem
  115. *focusableMessageItem
  116. toolRenderer ToolRenderer
  117. toolCall message.ToolCall
  118. result *message.ToolResult
  119. messageID string
  120. status ToolStatus
  121. // we use this so we can efficiently cache
  122. // tools that have a capped width (e.x bash.. and others)
  123. hasCappedWidth bool
  124. // isCompact indicates this tool should render in compact mode.
  125. isCompact bool
  126. // spinningFunc allows tools to override the default spinning logic.
  127. // If nil, uses the default: !toolCall.Finished && !canceled.
  128. spinningFunc SpinningFunc
  129. sty *styles.Styles
  130. anim *anim.Anim
  131. expandedContent bool
  132. }
  133. var _ Expandable = (*baseToolMessageItem)(nil)
  134. // newBaseToolMessageItem is the internal constructor for base tool message items.
  135. func newBaseToolMessageItem(
  136. sty *styles.Styles,
  137. toolCall message.ToolCall,
  138. result *message.ToolResult,
  139. toolRenderer ToolRenderer,
  140. canceled bool,
  141. ) *baseToolMessageItem {
  142. // we only do full width for diffs (as far as I know)
  143. hasCappedWidth := toolCall.Name != tools.EditToolName && toolCall.Name != tools.MultiEditToolName
  144. status := ToolStatusRunning
  145. if canceled {
  146. status = ToolStatusCanceled
  147. }
  148. t := &baseToolMessageItem{
  149. highlightableMessageItem: defaultHighlighter(sty),
  150. cachedMessageItem: &cachedMessageItem{},
  151. focusableMessageItem: &focusableMessageItem{},
  152. sty: sty,
  153. toolRenderer: toolRenderer,
  154. toolCall: toolCall,
  155. result: result,
  156. status: status,
  157. hasCappedWidth: hasCappedWidth,
  158. }
  159. t.anim = anim.New(anim.Settings{
  160. ID: toolCall.ID,
  161. Size: 15,
  162. GradColorA: sty.Primary,
  163. GradColorB: sty.Secondary,
  164. LabelColor: sty.FgBase,
  165. CycleColors: true,
  166. })
  167. return t
  168. }
  169. // NewToolMessageItem creates a new [ToolMessageItem] based on the tool call name.
  170. //
  171. // It returns a specific tool message item type if implemented, otherwise it
  172. // returns a generic tool message item. The messageID is the ID of the assistant
  173. // message containing this tool call.
  174. func NewToolMessageItem(
  175. sty *styles.Styles,
  176. messageID string,
  177. toolCall message.ToolCall,
  178. result *message.ToolResult,
  179. canceled bool,
  180. ) ToolMessageItem {
  181. var item ToolMessageItem
  182. switch toolCall.Name {
  183. case tools.BashToolName:
  184. item = NewBashToolMessageItem(sty, toolCall, result, canceled)
  185. case tools.JobOutputToolName:
  186. item = NewJobOutputToolMessageItem(sty, toolCall, result, canceled)
  187. case tools.JobKillToolName:
  188. item = NewJobKillToolMessageItem(sty, toolCall, result, canceled)
  189. case tools.ViewToolName:
  190. item = NewViewToolMessageItem(sty, toolCall, result, canceled)
  191. case tools.WriteToolName:
  192. item = NewWriteToolMessageItem(sty, toolCall, result, canceled)
  193. case tools.EditToolName:
  194. item = NewEditToolMessageItem(sty, toolCall, result, canceled)
  195. case tools.MultiEditToolName:
  196. item = NewMultiEditToolMessageItem(sty, toolCall, result, canceled)
  197. case tools.GlobToolName:
  198. item = NewGlobToolMessageItem(sty, toolCall, result, canceled)
  199. case tools.GrepToolName:
  200. item = NewGrepToolMessageItem(sty, toolCall, result, canceled)
  201. case tools.LSToolName:
  202. item = NewLSToolMessageItem(sty, toolCall, result, canceled)
  203. case tools.DownloadToolName:
  204. item = NewDownloadToolMessageItem(sty, toolCall, result, canceled)
  205. case tools.FetchToolName:
  206. item = NewFetchToolMessageItem(sty, toolCall, result, canceled)
  207. case tools.SourcegraphToolName:
  208. item = NewSourcegraphToolMessageItem(sty, toolCall, result, canceled)
  209. case tools.DiagnosticsToolName:
  210. item = NewDiagnosticsToolMessageItem(sty, toolCall, result, canceled)
  211. case agent.AgentToolName:
  212. item = NewAgentToolMessageItem(sty, toolCall, result, canceled)
  213. case tools.AgenticFetchToolName:
  214. item = NewAgenticFetchToolMessageItem(sty, toolCall, result, canceled)
  215. case tools.WebFetchToolName:
  216. item = NewWebFetchToolMessageItem(sty, toolCall, result, canceled)
  217. case tools.WebSearchToolName:
  218. item = NewWebSearchToolMessageItem(sty, toolCall, result, canceled)
  219. case tools.TodosToolName:
  220. item = NewTodosToolMessageItem(sty, toolCall, result, canceled)
  221. case tools.ReferencesToolName:
  222. item = NewReferencesToolMessageItem(sty, toolCall, result, canceled)
  223. case tools.LSPRestartToolName:
  224. item = NewLSPRestartToolMessageItem(sty, toolCall, result, canceled)
  225. default:
  226. if strings.HasPrefix(toolCall.Name, "mcp_") {
  227. item = NewMCPToolMessageItem(sty, toolCall, result, canceled)
  228. } else {
  229. item = NewGenericToolMessageItem(sty, toolCall, result, canceled)
  230. }
  231. }
  232. item.SetMessageID(messageID)
  233. return item
  234. }
  235. // SetCompact implements the Compactable interface.
  236. func (t *baseToolMessageItem) SetCompact(compact bool) {
  237. t.isCompact = compact
  238. t.clearCache()
  239. }
  240. // ID returns the unique identifier for this tool message item.
  241. func (t *baseToolMessageItem) ID() string {
  242. return t.toolCall.ID
  243. }
  244. // StartAnimation starts the assistant message animation if it should be spinning.
  245. func (t *baseToolMessageItem) StartAnimation() tea.Cmd {
  246. if !t.isSpinning() {
  247. return nil
  248. }
  249. return t.anim.Start()
  250. }
  251. // Animate progresses the assistant message animation if it should be spinning.
  252. func (t *baseToolMessageItem) Animate(msg anim.StepMsg) tea.Cmd {
  253. if !t.isSpinning() {
  254. return nil
  255. }
  256. return t.anim.Animate(msg)
  257. }
  258. // RawRender implements [MessageItem].
  259. func (t *baseToolMessageItem) RawRender(width int) string {
  260. toolItemWidth := width - MessageLeftPaddingTotal
  261. if t.hasCappedWidth {
  262. toolItemWidth = cappedMessageWidth(width)
  263. }
  264. content, height, ok := t.getCachedRender(toolItemWidth)
  265. // if we are spinning or there is no cache rerender
  266. if !ok || t.isSpinning() {
  267. content = t.toolRenderer.RenderTool(t.sty, toolItemWidth, &ToolRenderOpts{
  268. ToolCall: t.toolCall,
  269. Result: t.result,
  270. Anim: t.anim,
  271. ExpandedContent: t.expandedContent,
  272. Compact: t.isCompact,
  273. IsSpinning: t.isSpinning(),
  274. Status: t.computeStatus(),
  275. })
  276. height = lipgloss.Height(content)
  277. // cache the rendered content
  278. t.setCachedRender(content, toolItemWidth, height)
  279. }
  280. return t.renderHighlighted(content, toolItemWidth, height)
  281. }
  282. // Render renders the tool message item at the given width.
  283. func (t *baseToolMessageItem) Render(width int) string {
  284. style := t.sty.Chat.Message.ToolCallBlurred
  285. if t.focused {
  286. style = t.sty.Chat.Message.ToolCallFocused
  287. }
  288. if t.isCompact {
  289. style = t.sty.Chat.Message.ToolCallCompact
  290. }
  291. return style.Render(t.RawRender(width))
  292. }
  293. // ToolCall returns the tool call associated with this message item.
  294. func (t *baseToolMessageItem) ToolCall() message.ToolCall {
  295. return t.toolCall
  296. }
  297. // SetToolCall sets the tool call associated with this message item.
  298. func (t *baseToolMessageItem) SetToolCall(tc message.ToolCall) {
  299. t.toolCall = tc
  300. t.clearCache()
  301. }
  302. // SetResult sets the tool result associated with this message item.
  303. func (t *baseToolMessageItem) SetResult(res *message.ToolResult) {
  304. t.result = res
  305. t.clearCache()
  306. }
  307. // MessageID returns the ID of the message containing this tool call.
  308. func (t *baseToolMessageItem) MessageID() string {
  309. return t.messageID
  310. }
  311. // SetMessageID sets the ID of the message containing this tool call.
  312. func (t *baseToolMessageItem) SetMessageID(id string) {
  313. t.messageID = id
  314. }
  315. // SetStatus sets the tool status.
  316. func (t *baseToolMessageItem) SetStatus(status ToolStatus) {
  317. t.status = status
  318. t.clearCache()
  319. }
  320. // Status returns the current tool status.
  321. func (t *baseToolMessageItem) Status() ToolStatus {
  322. return t.status
  323. }
  324. // computeStatus computes the effective status considering the result.
  325. func (t *baseToolMessageItem) computeStatus() ToolStatus {
  326. if t.result != nil {
  327. if t.result.IsError {
  328. return ToolStatusError
  329. }
  330. return ToolStatusSuccess
  331. }
  332. return t.status
  333. }
  334. // isSpinning returns true if the tool should show animation.
  335. func (t *baseToolMessageItem) isSpinning() bool {
  336. if t.spinningFunc != nil {
  337. return t.spinningFunc(SpinningState{
  338. ToolCall: t.toolCall,
  339. Result: t.result,
  340. Status: t.status,
  341. })
  342. }
  343. return !t.toolCall.Finished && t.status != ToolStatusCanceled
  344. }
  345. // SetSpinningFunc sets a custom function to determine if the tool should spin.
  346. func (t *baseToolMessageItem) SetSpinningFunc(fn SpinningFunc) {
  347. t.spinningFunc = fn
  348. }
  349. // ToggleExpanded toggles the expanded state of the thinking box.
  350. func (t *baseToolMessageItem) ToggleExpanded() bool {
  351. t.expandedContent = !t.expandedContent
  352. t.clearCache()
  353. return t.expandedContent
  354. }
  355. // HandleMouseClick implements MouseClickable.
  356. func (t *baseToolMessageItem) HandleMouseClick(btn ansi.MouseButton, x, y int) bool {
  357. return btn == ansi.MouseLeft
  358. }
  359. // HandleKeyEvent implements KeyEventHandler.
  360. func (t *baseToolMessageItem) HandleKeyEvent(key tea.KeyMsg) (bool, tea.Cmd) {
  361. if k := key.String(); k == "c" || k == "y" {
  362. text := t.formatToolForCopy()
  363. return true, common.CopyToClipboard(text, "Tool content copied to clipboard")
  364. }
  365. return false, nil
  366. }
  367. // pendingTool renders a tool that is still in progress with an animation.
  368. func pendingTool(sty *styles.Styles, name string, anim *anim.Anim) string {
  369. icon := sty.Tool.IconPending.Render()
  370. toolName := sty.Tool.NameNormal.Render(name)
  371. var animView string
  372. if anim != nil {
  373. animView = anim.Render()
  374. }
  375. return fmt.Sprintf("%s %s %s", icon, toolName, animView)
  376. }
  377. // toolEarlyStateContent handles error/cancelled/pending states before content rendering.
  378. // Returns the rendered output and true if early state was handled.
  379. func toolEarlyStateContent(sty *styles.Styles, opts *ToolRenderOpts, width int) (string, bool) {
  380. var msg string
  381. switch opts.Status {
  382. case ToolStatusError:
  383. msg = toolErrorContent(sty, opts.Result, width)
  384. case ToolStatusCanceled:
  385. msg = sty.Tool.StateCancelled.Render("Canceled.")
  386. case ToolStatusAwaitingPermission:
  387. msg = sty.Tool.StateWaiting.Render("Requesting permission...")
  388. case ToolStatusRunning:
  389. msg = sty.Tool.StateWaiting.Render("Waiting for tool response...")
  390. default:
  391. return "", false
  392. }
  393. return msg, true
  394. }
  395. // toolErrorContent formats an error message with ERROR tag.
  396. func toolErrorContent(sty *styles.Styles, result *message.ToolResult, width int) string {
  397. if result == nil {
  398. return ""
  399. }
  400. errContent := strings.ReplaceAll(result.Content, "\n", " ")
  401. errTag := sty.Tool.ErrorTag.Render("ERROR")
  402. tagWidth := lipgloss.Width(errTag)
  403. errContent = ansi.Truncate(errContent, width-tagWidth-3, "…")
  404. return fmt.Sprintf("%s %s", errTag, sty.Tool.ErrorMessage.Render(errContent))
  405. }
  406. // toolIcon returns the status icon for a tool call.
  407. // toolIcon returns the status icon for a tool call based on its status.
  408. func toolIcon(sty *styles.Styles, status ToolStatus) string {
  409. switch status {
  410. case ToolStatusSuccess:
  411. return sty.Tool.IconSuccess.String()
  412. case ToolStatusError:
  413. return sty.Tool.IconError.String()
  414. case ToolStatusCanceled:
  415. return sty.Tool.IconCancelled.String()
  416. default:
  417. return sty.Tool.IconPending.String()
  418. }
  419. }
  420. // toolParamList formats parameters as "main (key=value, ...)" with truncation.
  421. // toolParamList formats tool parameters as "main (key=value, ...)" with truncation.
  422. func toolParamList(sty *styles.Styles, params []string, width int) string {
  423. // minSpaceForMainParam is the min space required for the main param
  424. // if this is less that the value set we will only show the main param nothing else
  425. const minSpaceForMainParam = 30
  426. if len(params) == 0 {
  427. return ""
  428. }
  429. mainParam := params[0]
  430. // Build key=value pairs from remaining params (consecutive key, value pairs).
  431. var kvPairs []string
  432. for i := 1; i+1 < len(params); i += 2 {
  433. if params[i+1] != "" {
  434. kvPairs = append(kvPairs, fmt.Sprintf("%s=%s", params[i], params[i+1]))
  435. }
  436. }
  437. // Try to include key=value pairs if there's enough space.
  438. output := mainParam
  439. if len(kvPairs) > 0 {
  440. partsStr := strings.Join(kvPairs, ", ")
  441. if remaining := width - lipgloss.Width(partsStr) - 3; remaining >= minSpaceForMainParam {
  442. output = fmt.Sprintf("%s (%s)", mainParam, partsStr)
  443. }
  444. }
  445. if width >= 0 {
  446. output = ansi.Truncate(output, width, "…")
  447. }
  448. return sty.Tool.ParamMain.Render(output)
  449. }
  450. // toolHeader builds the tool header line: "● ToolName params..."
  451. func toolHeader(sty *styles.Styles, status ToolStatus, name string, width int, nested bool, params ...string) string {
  452. icon := toolIcon(sty, status)
  453. nameStyle := sty.Tool.NameNormal
  454. if nested {
  455. nameStyle = sty.Tool.NameNested
  456. }
  457. toolName := nameStyle.Render(name)
  458. prefix := fmt.Sprintf("%s %s ", icon, toolName)
  459. prefixWidth := lipgloss.Width(prefix)
  460. remainingWidth := width - prefixWidth
  461. paramsStr := toolParamList(sty, params, remainingWidth)
  462. return prefix + paramsStr
  463. }
  464. // toolOutputPlainContent renders plain text with optional expansion support.
  465. func toolOutputPlainContent(sty *styles.Styles, content string, width int, expanded bool) string {
  466. content = stringext.NormalizeSpace(content)
  467. lines := strings.Split(content, "\n")
  468. maxLines := responseContextHeight
  469. if expanded {
  470. maxLines = len(lines) // Show all
  471. }
  472. var out []string
  473. for i, ln := range lines {
  474. if i >= maxLines {
  475. break
  476. }
  477. ln = " " + ln
  478. if lipgloss.Width(ln) > width {
  479. ln = ansi.Truncate(ln, width, "…")
  480. }
  481. out = append(out, sty.Tool.ContentLine.Width(width).Render(ln))
  482. }
  483. wasTruncated := len(lines) > responseContextHeight
  484. if !expanded && wasTruncated {
  485. out = append(out, sty.Tool.ContentTruncation.
  486. Width(width).
  487. Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-responseContextHeight)))
  488. }
  489. return strings.Join(out, "\n")
  490. }
  491. // toolOutputCodeContent renders code with syntax highlighting and line numbers.
  492. func toolOutputCodeContent(sty *styles.Styles, path, content string, offset, width int, expanded bool) string {
  493. content = stringext.NormalizeSpace(content)
  494. lines := strings.Split(content, "\n")
  495. maxLines := responseContextHeight
  496. if expanded {
  497. maxLines = len(lines)
  498. }
  499. // Truncate if needed.
  500. displayLines := lines
  501. if len(lines) > maxLines {
  502. displayLines = lines[:maxLines]
  503. }
  504. bg := sty.Tool.ContentCodeBg
  505. highlighted, _ := common.SyntaxHighlight(sty, strings.Join(displayLines, "\n"), path, bg)
  506. highlightedLines := strings.Split(highlighted, "\n")
  507. // Calculate line number width.
  508. maxLineNumber := len(displayLines) + offset
  509. maxDigits := getDigits(maxLineNumber)
  510. numFmt := fmt.Sprintf("%%%dd", maxDigits)
  511. bodyWidth := width - toolBodyLeftPaddingTotal
  512. codeWidth := bodyWidth - maxDigits
  513. var out []string
  514. for i, ln := range highlightedLines {
  515. lineNum := sty.Tool.ContentLineNumber.Render(fmt.Sprintf(numFmt, i+1+offset))
  516. // Truncate accounting for padding that will be added.
  517. ln = ansi.Truncate(ln, codeWidth-sty.Tool.ContentCodeLine.GetHorizontalPadding(), "…")
  518. codeLine := sty.Tool.ContentCodeLine.
  519. Width(codeWidth).
  520. Render(ln)
  521. out = append(out, lipgloss.JoinHorizontal(lipgloss.Left, lineNum, codeLine))
  522. }
  523. // Add truncation message if needed.
  524. if len(lines) > maxLines && !expanded {
  525. out = append(out, sty.Tool.ContentCodeTruncation.
  526. Width(width).
  527. Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines)),
  528. )
  529. }
  530. return sty.Tool.Body.Render(strings.Join(out, "\n"))
  531. }
  532. // toolOutputImageContent renders image data with size info.
  533. func toolOutputImageContent(sty *styles.Styles, data, mediaType string) string {
  534. dataSize := len(data) * 3 / 4
  535. sizeStr := formatSize(dataSize)
  536. loaded := sty.Base.Foreground(sty.Green).Render("Loaded")
  537. arrow := sty.Base.Foreground(sty.GreenDark).Render("→")
  538. typeStyled := sty.Base.Render(mediaType)
  539. sizeStyled := sty.Subtle.Render(sizeStr)
  540. return sty.Tool.Body.Render(fmt.Sprintf("%s %s %s %s", loaded, arrow, typeStyled, sizeStyled))
  541. }
  542. // getDigits returns the number of digits in a number.
  543. func getDigits(n int) int {
  544. if n == 0 {
  545. return 1
  546. }
  547. if n < 0 {
  548. n = -n
  549. }
  550. digits := 0
  551. for n > 0 {
  552. n /= 10
  553. digits++
  554. }
  555. return digits
  556. }
  557. // formatSize formats byte size into human readable format.
  558. func formatSize(bytes int) string {
  559. const (
  560. kb = 1024
  561. mb = kb * 1024
  562. )
  563. switch {
  564. case bytes >= mb:
  565. return fmt.Sprintf("%.1f MB", float64(bytes)/float64(mb))
  566. case bytes >= kb:
  567. return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb))
  568. default:
  569. return fmt.Sprintf("%d B", bytes)
  570. }
  571. }
  572. // toolOutputDiffContent renders a diff between old and new content.
  573. func toolOutputDiffContent(sty *styles.Styles, file, oldContent, newContent string, width int, expanded bool) string {
  574. bodyWidth := width - toolBodyLeftPaddingTotal
  575. formatter := common.DiffFormatter(sty).
  576. Before(file, oldContent).
  577. After(file, newContent).
  578. Width(bodyWidth)
  579. // Use split view for wide terminals.
  580. if width > maxTextWidth {
  581. formatter = formatter.Split()
  582. }
  583. formatted := formatter.String()
  584. lines := strings.Split(formatted, "\n")
  585. // Truncate if needed.
  586. maxLines := responseContextHeight
  587. if expanded {
  588. maxLines = len(lines)
  589. }
  590. if len(lines) > maxLines && !expanded {
  591. truncMsg := sty.Tool.DiffTruncation.
  592. Width(bodyWidth).
  593. Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines))
  594. formatted = strings.Join(lines[:maxLines], "\n") + "\n" + truncMsg
  595. }
  596. return sty.Tool.Body.Render(formatted)
  597. }
  598. // formatTimeout converts timeout seconds to a duration string (e.g., "30s").
  599. // Returns empty string if timeout is 0.
  600. func formatTimeout(timeout int) string {
  601. if timeout == 0 {
  602. return ""
  603. }
  604. return fmt.Sprintf("%ds", timeout)
  605. }
  606. // formatNonZero returns string representation of non-zero integers, empty string for zero.
  607. func formatNonZero(value int) string {
  608. if value == 0 {
  609. return ""
  610. }
  611. return fmt.Sprintf("%d", value)
  612. }
  613. // toolOutputMultiEditDiffContent renders a diff with optional failed edits note.
  614. func toolOutputMultiEditDiffContent(sty *styles.Styles, file string, meta tools.MultiEditResponseMetadata, totalEdits, width int, expanded bool) string {
  615. bodyWidth := width - toolBodyLeftPaddingTotal
  616. formatter := common.DiffFormatter(sty).
  617. Before(file, meta.OldContent).
  618. After(file, meta.NewContent).
  619. Width(bodyWidth)
  620. // Use split view for wide terminals.
  621. if width > maxTextWidth {
  622. formatter = formatter.Split()
  623. }
  624. formatted := formatter.String()
  625. lines := strings.Split(formatted, "\n")
  626. // Truncate if needed.
  627. maxLines := responseContextHeight
  628. if expanded {
  629. maxLines = len(lines)
  630. }
  631. if len(lines) > maxLines && !expanded {
  632. truncMsg := sty.Tool.DiffTruncation.
  633. Width(bodyWidth).
  634. Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines))
  635. formatted = truncMsg + "\n" + strings.Join(lines[:maxLines], "\n")
  636. }
  637. // Add failed edits note if any exist.
  638. if len(meta.EditsFailed) > 0 {
  639. noteTag := sty.Tool.NoteTag.Render("Note")
  640. noteMsg := fmt.Sprintf("%d of %d edits succeeded", meta.EditsApplied, totalEdits)
  641. note := fmt.Sprintf("%s %s", noteTag, sty.Tool.NoteMessage.Render(noteMsg))
  642. formatted = formatted + "\n\n" + note
  643. }
  644. return sty.Tool.Body.Render(formatted)
  645. }
  646. // roundedEnumerator creates a tree enumerator with rounded corners.
  647. func roundedEnumerator(lPadding, width int) tree.Enumerator {
  648. if width == 0 {
  649. width = 2
  650. }
  651. if lPadding == 0 {
  652. lPadding = 1
  653. }
  654. return func(children tree.Children, index int) string {
  655. line := strings.Repeat("─", width)
  656. padding := strings.Repeat(" ", lPadding)
  657. if children.Length()-1 == index {
  658. return padding + "╰" + line
  659. }
  660. return padding + "├" + line
  661. }
  662. }
  663. // toolOutputMarkdownContent renders markdown content with optional truncation.
  664. func toolOutputMarkdownContent(sty *styles.Styles, content string, width int, expanded bool) string {
  665. content = stringext.NormalizeSpace(content)
  666. // Cap width for readability.
  667. if width > maxTextWidth {
  668. width = maxTextWidth
  669. }
  670. renderer := common.PlainMarkdownRenderer(sty, width)
  671. rendered, err := renderer.Render(content)
  672. if err != nil {
  673. return toolOutputPlainContent(sty, content, width, expanded)
  674. }
  675. lines := strings.Split(rendered, "\n")
  676. maxLines := responseContextHeight
  677. if expanded {
  678. maxLines = len(lines)
  679. }
  680. var out []string
  681. for i, ln := range lines {
  682. if i >= maxLines {
  683. break
  684. }
  685. out = append(out, ln)
  686. }
  687. if len(lines) > maxLines && !expanded {
  688. out = append(out, sty.Tool.ContentTruncation.
  689. Width(width).
  690. Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines)),
  691. )
  692. }
  693. return sty.Tool.Body.Render(strings.Join(out, "\n"))
  694. }
  695. // formatToolForCopy formats the tool call for clipboard copying.
  696. func (t *baseToolMessageItem) formatToolForCopy() string {
  697. var parts []string
  698. toolName := prettifyToolName(t.toolCall.Name)
  699. parts = append(parts, fmt.Sprintf("## %s Tool Call", toolName))
  700. if t.toolCall.Input != "" {
  701. params := t.formatParametersForCopy()
  702. if params != "" {
  703. parts = append(parts, "### Parameters:")
  704. parts = append(parts, params)
  705. }
  706. }
  707. if t.result != nil && t.result.ToolCallID != "" {
  708. if t.result.IsError {
  709. parts = append(parts, "### Error:")
  710. parts = append(parts, t.result.Content)
  711. } else {
  712. parts = append(parts, "### Result:")
  713. content := t.formatResultForCopy()
  714. if content != "" {
  715. parts = append(parts, content)
  716. }
  717. }
  718. } else if t.status == ToolStatusCanceled {
  719. parts = append(parts, "### Status:")
  720. parts = append(parts, "Cancelled")
  721. } else {
  722. parts = append(parts, "### Status:")
  723. parts = append(parts, "Pending...")
  724. }
  725. return strings.Join(parts, "\n\n")
  726. }
  727. // formatParametersForCopy formats tool parameters for clipboard copying.
  728. func (t *baseToolMessageItem) formatParametersForCopy() string {
  729. switch t.toolCall.Name {
  730. case tools.BashToolName:
  731. var params tools.BashParams
  732. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  733. cmd := strings.ReplaceAll(params.Command, "\n", " ")
  734. cmd = strings.ReplaceAll(cmd, "\t", " ")
  735. return fmt.Sprintf("**Command:** %s", cmd)
  736. }
  737. case tools.ViewToolName:
  738. var params tools.ViewParams
  739. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  740. var parts []string
  741. parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
  742. if params.Limit > 0 {
  743. parts = append(parts, fmt.Sprintf("**Limit:** %d", params.Limit))
  744. }
  745. if params.Offset > 0 {
  746. parts = append(parts, fmt.Sprintf("**Offset:** %d", params.Offset))
  747. }
  748. return strings.Join(parts, "\n")
  749. }
  750. case tools.EditToolName:
  751. var params tools.EditParams
  752. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  753. return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
  754. }
  755. case tools.MultiEditToolName:
  756. var params tools.MultiEditParams
  757. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  758. var parts []string
  759. parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
  760. parts = append(parts, fmt.Sprintf("**Edits:** %d", len(params.Edits)))
  761. return strings.Join(parts, "\n")
  762. }
  763. case tools.WriteToolName:
  764. var params tools.WriteParams
  765. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  766. return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
  767. }
  768. case tools.FetchToolName:
  769. var params tools.FetchParams
  770. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  771. var parts []string
  772. parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
  773. if params.Format != "" {
  774. parts = append(parts, fmt.Sprintf("**Format:** %s", params.Format))
  775. }
  776. if params.Timeout > 0 {
  777. parts = append(parts, fmt.Sprintf("**Timeout:** %ds", params.Timeout))
  778. }
  779. return strings.Join(parts, "\n")
  780. }
  781. case tools.AgenticFetchToolName:
  782. var params tools.AgenticFetchParams
  783. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  784. var parts []string
  785. if params.URL != "" {
  786. parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
  787. }
  788. if params.Prompt != "" {
  789. parts = append(parts, fmt.Sprintf("**Prompt:** %s", params.Prompt))
  790. }
  791. return strings.Join(parts, "\n")
  792. }
  793. case tools.WebFetchToolName:
  794. var params tools.WebFetchParams
  795. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  796. return fmt.Sprintf("**URL:** %s", params.URL)
  797. }
  798. case tools.GrepToolName:
  799. var params tools.GrepParams
  800. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  801. var parts []string
  802. parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
  803. if params.Path != "" {
  804. parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
  805. }
  806. if params.Include != "" {
  807. parts = append(parts, fmt.Sprintf("**Include:** %s", params.Include))
  808. }
  809. if params.LiteralText {
  810. parts = append(parts, "**Literal:** true")
  811. }
  812. return strings.Join(parts, "\n")
  813. }
  814. case tools.GlobToolName:
  815. var params tools.GlobParams
  816. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  817. var parts []string
  818. parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
  819. if params.Path != "" {
  820. parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
  821. }
  822. return strings.Join(parts, "\n")
  823. }
  824. case tools.LSToolName:
  825. var params tools.LSParams
  826. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  827. path := params.Path
  828. if path == "" {
  829. path = "."
  830. }
  831. return fmt.Sprintf("**Path:** %s", fsext.PrettyPath(path))
  832. }
  833. case tools.DownloadToolName:
  834. var params tools.DownloadParams
  835. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  836. var parts []string
  837. parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
  838. parts = append(parts, fmt.Sprintf("**File Path:** %s", fsext.PrettyPath(params.FilePath)))
  839. if params.Timeout > 0 {
  840. parts = append(parts, fmt.Sprintf("**Timeout:** %s", (time.Duration(params.Timeout)*time.Second).String()))
  841. }
  842. return strings.Join(parts, "\n")
  843. }
  844. case tools.SourcegraphToolName:
  845. var params tools.SourcegraphParams
  846. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  847. var parts []string
  848. parts = append(parts, fmt.Sprintf("**Query:** %s", params.Query))
  849. if params.Count > 0 {
  850. parts = append(parts, fmt.Sprintf("**Count:** %d", params.Count))
  851. }
  852. if params.ContextWindow > 0 {
  853. parts = append(parts, fmt.Sprintf("**Context:** %d", params.ContextWindow))
  854. }
  855. return strings.Join(parts, "\n")
  856. }
  857. case tools.DiagnosticsToolName:
  858. return "**Project:** diagnostics"
  859. case agent.AgentToolName:
  860. var params agent.AgentParams
  861. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  862. return fmt.Sprintf("**Task:**\n%s", params.Prompt)
  863. }
  864. }
  865. var params map[string]any
  866. if json.Unmarshal([]byte(t.toolCall.Input), &params) == nil {
  867. var parts []string
  868. for key, value := range params {
  869. displayKey := strings.ReplaceAll(key, "_", " ")
  870. if len(displayKey) > 0 {
  871. displayKey = strings.ToUpper(displayKey[:1]) + displayKey[1:]
  872. }
  873. parts = append(parts, fmt.Sprintf("**%s:** %v", displayKey, value))
  874. }
  875. return strings.Join(parts, "\n")
  876. }
  877. return ""
  878. }
  879. // formatResultForCopy formats tool results for clipboard copying.
  880. func (t *baseToolMessageItem) formatResultForCopy() string {
  881. if t.result == nil {
  882. return ""
  883. }
  884. if t.result.Data != "" {
  885. if strings.HasPrefix(t.result.MIMEType, "image/") {
  886. return fmt.Sprintf("[Image: %s]", t.result.MIMEType)
  887. }
  888. return fmt.Sprintf("[Media: %s]", t.result.MIMEType)
  889. }
  890. switch t.toolCall.Name {
  891. case tools.BashToolName:
  892. return t.formatBashResultForCopy()
  893. case tools.ViewToolName:
  894. return t.formatViewResultForCopy()
  895. case tools.EditToolName:
  896. return t.formatEditResultForCopy()
  897. case tools.MultiEditToolName:
  898. return t.formatMultiEditResultForCopy()
  899. case tools.WriteToolName:
  900. return t.formatWriteResultForCopy()
  901. case tools.FetchToolName:
  902. return t.formatFetchResultForCopy()
  903. case tools.AgenticFetchToolName:
  904. return t.formatAgenticFetchResultForCopy()
  905. case tools.WebFetchToolName:
  906. return t.formatWebFetchResultForCopy()
  907. case agent.AgentToolName:
  908. return t.formatAgentResultForCopy()
  909. case tools.DownloadToolName, tools.GrepToolName, tools.GlobToolName, tools.LSToolName, tools.SourcegraphToolName, tools.DiagnosticsToolName, tools.TodosToolName:
  910. return fmt.Sprintf("```\n%s\n```", t.result.Content)
  911. default:
  912. return t.result.Content
  913. }
  914. }
  915. // formatBashResultForCopy formats bash tool results for clipboard.
  916. func (t *baseToolMessageItem) formatBashResultForCopy() string {
  917. if t.result == nil {
  918. return ""
  919. }
  920. var meta tools.BashResponseMetadata
  921. if t.result.Metadata != "" {
  922. json.Unmarshal([]byte(t.result.Metadata), &meta)
  923. }
  924. output := meta.Output
  925. if output == "" && t.result.Content != tools.BashNoOutput {
  926. output = t.result.Content
  927. }
  928. if output == "" {
  929. return ""
  930. }
  931. return fmt.Sprintf("```bash\n%s\n```", output)
  932. }
  933. // formatViewResultForCopy formats view tool results for clipboard.
  934. func (t *baseToolMessageItem) formatViewResultForCopy() string {
  935. if t.result == nil {
  936. return ""
  937. }
  938. var meta tools.ViewResponseMetadata
  939. if t.result.Metadata != "" {
  940. json.Unmarshal([]byte(t.result.Metadata), &meta)
  941. }
  942. if meta.Content == "" {
  943. return t.result.Content
  944. }
  945. lang := ""
  946. if meta.FilePath != "" {
  947. ext := strings.ToLower(filepath.Ext(meta.FilePath))
  948. switch ext {
  949. case ".go":
  950. lang = "go"
  951. case ".js", ".mjs":
  952. lang = "javascript"
  953. case ".ts":
  954. lang = "typescript"
  955. case ".py":
  956. lang = "python"
  957. case ".rs":
  958. lang = "rust"
  959. case ".java":
  960. lang = "java"
  961. case ".c":
  962. lang = "c"
  963. case ".cpp", ".cc", ".cxx":
  964. lang = "cpp"
  965. case ".sh", ".bash":
  966. lang = "bash"
  967. case ".json":
  968. lang = "json"
  969. case ".yaml", ".yml":
  970. lang = "yaml"
  971. case ".xml":
  972. lang = "xml"
  973. case ".html":
  974. lang = "html"
  975. case ".css":
  976. lang = "css"
  977. case ".md":
  978. lang = "markdown"
  979. }
  980. }
  981. var result strings.Builder
  982. if lang != "" {
  983. fmt.Fprintf(&result, "```%s\n", lang)
  984. } else {
  985. result.WriteString("```\n")
  986. }
  987. result.WriteString(meta.Content)
  988. result.WriteString("\n```")
  989. return result.String()
  990. }
  991. // formatEditResultForCopy formats edit tool results for clipboard.
  992. func (t *baseToolMessageItem) formatEditResultForCopy() string {
  993. if t.result == nil || t.result.Metadata == "" {
  994. if t.result != nil {
  995. return t.result.Content
  996. }
  997. return ""
  998. }
  999. var meta tools.EditResponseMetadata
  1000. if json.Unmarshal([]byte(t.result.Metadata), &meta) != nil {
  1001. return t.result.Content
  1002. }
  1003. var params tools.EditParams
  1004. json.Unmarshal([]byte(t.toolCall.Input), &params)
  1005. var result strings.Builder
  1006. if meta.OldContent != "" || meta.NewContent != "" {
  1007. fileName := params.FilePath
  1008. if fileName != "" {
  1009. fileName = fsext.PrettyPath(fileName)
  1010. }
  1011. diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
  1012. fmt.Fprintf(&result, "Changes: +%d -%d\n", additions, removals)
  1013. result.WriteString("```diff\n")
  1014. result.WriteString(diffContent)
  1015. result.WriteString("\n```")
  1016. }
  1017. return result.String()
  1018. }
  1019. // formatMultiEditResultForCopy formats multi-edit tool results for clipboard.
  1020. func (t *baseToolMessageItem) formatMultiEditResultForCopy() string {
  1021. if t.result == nil || t.result.Metadata == "" {
  1022. if t.result != nil {
  1023. return t.result.Content
  1024. }
  1025. return ""
  1026. }
  1027. var meta tools.MultiEditResponseMetadata
  1028. if json.Unmarshal([]byte(t.result.Metadata), &meta) != nil {
  1029. return t.result.Content
  1030. }
  1031. var params tools.MultiEditParams
  1032. json.Unmarshal([]byte(t.toolCall.Input), &params)
  1033. var result strings.Builder
  1034. if meta.OldContent != "" || meta.NewContent != "" {
  1035. fileName := params.FilePath
  1036. if fileName != "" {
  1037. fileName = fsext.PrettyPath(fileName)
  1038. }
  1039. diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
  1040. fmt.Fprintf(&result, "Changes: +%d -%d\n", additions, removals)
  1041. result.WriteString("```diff\n")
  1042. result.WriteString(diffContent)
  1043. result.WriteString("\n```")
  1044. }
  1045. return result.String()
  1046. }
  1047. // formatWriteResultForCopy formats write tool results for clipboard.
  1048. func (t *baseToolMessageItem) formatWriteResultForCopy() string {
  1049. if t.result == nil {
  1050. return ""
  1051. }
  1052. var params tools.WriteParams
  1053. if json.Unmarshal([]byte(t.toolCall.Input), &params) != nil {
  1054. return t.result.Content
  1055. }
  1056. lang := ""
  1057. if params.FilePath != "" {
  1058. ext := strings.ToLower(filepath.Ext(params.FilePath))
  1059. switch ext {
  1060. case ".go":
  1061. lang = "go"
  1062. case ".js", ".mjs":
  1063. lang = "javascript"
  1064. case ".ts":
  1065. lang = "typescript"
  1066. case ".py":
  1067. lang = "python"
  1068. case ".rs":
  1069. lang = "rust"
  1070. case ".java":
  1071. lang = "java"
  1072. case ".c":
  1073. lang = "c"
  1074. case ".cpp", ".cc", ".cxx":
  1075. lang = "cpp"
  1076. case ".sh", ".bash":
  1077. lang = "bash"
  1078. case ".json":
  1079. lang = "json"
  1080. case ".yaml", ".yml":
  1081. lang = "yaml"
  1082. case ".xml":
  1083. lang = "xml"
  1084. case ".html":
  1085. lang = "html"
  1086. case ".css":
  1087. lang = "css"
  1088. case ".md":
  1089. lang = "markdown"
  1090. }
  1091. }
  1092. var result strings.Builder
  1093. fmt.Fprintf(&result, "File: %s\n", fsext.PrettyPath(params.FilePath))
  1094. if lang != "" {
  1095. fmt.Fprintf(&result, "```%s\n", lang)
  1096. } else {
  1097. result.WriteString("```\n")
  1098. }
  1099. result.WriteString(params.Content)
  1100. result.WriteString("\n```")
  1101. return result.String()
  1102. }
  1103. // formatFetchResultForCopy formats fetch tool results for clipboard.
  1104. func (t *baseToolMessageItem) formatFetchResultForCopy() string {
  1105. if t.result == nil {
  1106. return ""
  1107. }
  1108. var params tools.FetchParams
  1109. if json.Unmarshal([]byte(t.toolCall.Input), &params) != nil {
  1110. return t.result.Content
  1111. }
  1112. var result strings.Builder
  1113. if params.URL != "" {
  1114. fmt.Fprintf(&result, "URL: %s\n", params.URL)
  1115. }
  1116. if params.Format != "" {
  1117. fmt.Fprintf(&result, "Format: %s\n", params.Format)
  1118. }
  1119. if params.Timeout > 0 {
  1120. fmt.Fprintf(&result, "Timeout: %ds\n", params.Timeout)
  1121. }
  1122. result.WriteString("\n")
  1123. result.WriteString(t.result.Content)
  1124. return result.String()
  1125. }
  1126. // formatAgenticFetchResultForCopy formats agentic fetch tool results for clipboard.
  1127. func (t *baseToolMessageItem) formatAgenticFetchResultForCopy() string {
  1128. if t.result == nil {
  1129. return ""
  1130. }
  1131. var params tools.AgenticFetchParams
  1132. if json.Unmarshal([]byte(t.toolCall.Input), &params) != nil {
  1133. return t.result.Content
  1134. }
  1135. var result strings.Builder
  1136. if params.URL != "" {
  1137. fmt.Fprintf(&result, "URL: %s\n", params.URL)
  1138. }
  1139. if params.Prompt != "" {
  1140. fmt.Fprintf(&result, "Prompt: %s\n\n", params.Prompt)
  1141. }
  1142. result.WriteString("```markdown\n")
  1143. result.WriteString(t.result.Content)
  1144. result.WriteString("\n```")
  1145. return result.String()
  1146. }
  1147. // formatWebFetchResultForCopy formats web fetch tool results for clipboard.
  1148. func (t *baseToolMessageItem) formatWebFetchResultForCopy() string {
  1149. if t.result == nil {
  1150. return ""
  1151. }
  1152. var params tools.WebFetchParams
  1153. if json.Unmarshal([]byte(t.toolCall.Input), &params) != nil {
  1154. return t.result.Content
  1155. }
  1156. var result strings.Builder
  1157. result.WriteString(fmt.Sprintf("URL: %s\n\n", params.URL))
  1158. result.WriteString("```markdown\n")
  1159. result.WriteString(t.result.Content)
  1160. result.WriteString("\n```")
  1161. return result.String()
  1162. }
  1163. // formatAgentResultForCopy formats agent tool results for clipboard.
  1164. func (t *baseToolMessageItem) formatAgentResultForCopy() string {
  1165. if t.result == nil {
  1166. return ""
  1167. }
  1168. var result strings.Builder
  1169. if t.result.Content != "" {
  1170. result.WriteString(fmt.Sprintf("```markdown\n%s\n```", t.result.Content))
  1171. }
  1172. return result.String()
  1173. }
  1174. // prettifyToolName returns a human-readable name for tool names.
  1175. func prettifyToolName(name string) string {
  1176. switch name {
  1177. case agent.AgentToolName:
  1178. return "Agent"
  1179. case tools.BashToolName:
  1180. return "Bash"
  1181. case tools.JobOutputToolName:
  1182. return "Job: Output"
  1183. case tools.JobKillToolName:
  1184. return "Job: Kill"
  1185. case tools.DownloadToolName:
  1186. return "Download"
  1187. case tools.EditToolName:
  1188. return "Edit"
  1189. case tools.MultiEditToolName:
  1190. return "Multi-Edit"
  1191. case tools.FetchToolName:
  1192. return "Fetch"
  1193. case tools.AgenticFetchToolName:
  1194. return "Agentic Fetch"
  1195. case tools.WebFetchToolName:
  1196. return "Fetch"
  1197. case tools.WebSearchToolName:
  1198. return "Search"
  1199. case tools.GlobToolName:
  1200. return "Glob"
  1201. case tools.GrepToolName:
  1202. return "Grep"
  1203. case tools.LSToolName:
  1204. return "List"
  1205. case tools.SourcegraphToolName:
  1206. return "Sourcegraph"
  1207. case tools.TodosToolName:
  1208. return "To-Do"
  1209. case tools.ViewToolName:
  1210. return "View"
  1211. case tools.WriteToolName:
  1212. return "Write"
  1213. default:
  1214. return genericPrettyName(name)
  1215. }
  1216. }