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