@@ -1,9 +1,19 @@
1package messages
1package messages
10+ "github.com/atotto/clipboard"
11+ "github.com/charmbracelet/bubbles/v2/key"
6 tea "github.com/charmbracelet/bubbletea/v2"
12 tea "github.com/charmbracelet/bubbletea/v2"
13+ "github.com/charmbracelet/crush/internal/diff"
14+ "github.com/charmbracelet/crush/internal/fsext"
15+ "github.com/charmbracelet/crush/internal/llm/agent"
16+ "github.com/charmbracelet/crush/internal/llm/tools"
7 "github.com/charmbracelet/crush/internal/message"
17 "github.com/charmbracelet/crush/internal/message"
8 "github.com/charmbracelet/crush/internal/permission"
18 "github.com/charmbracelet/crush/internal/permission"
9 "github.com/charmbracelet/crush/internal/tui/components/anim"
19 "github.com/charmbracelet/crush/internal/tui/components/anim"
@@ -154,6 +164,10 @@ func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
154 cmds = append(cmds, cmd)
164 cmds = append(cmds, cmd)
156 return m, tea.Batch(cmds...)
166 return m, tea.Batch(cmds...)
167+ case tea.KeyPressMsg:
168+ if key.Matches(msg, copyKey) {
169+ return m, m.copyTool()
158 return m, nil
172 return m, nil
@@ -182,6 +196,456 @@ func (m *toolCallCmp) SetCancelled() {
182 m.cancelled = true
196 m.cancelled = true
199+func (m *toolCallCmp) copyTool() tea.Cmd {
200+ content := m.formatToolForCopy()
201+ err := clipboard.WriteAll(content)
203+ return util.ReportError(fmt.Errorf("failed to copy tool content to clipboard: %w", err))
208+func (m *toolCallCmp) formatToolForCopy() string {
211+ toolName := prettifyToolName(m.call.Name)
212+ parts = append(parts, fmt.Sprintf("## %s Tool Call", toolName))
214+ if m.call.Input != "" {
215+ params := m.formatParametersForCopy()
217+ parts = append(parts, "### Parameters:")
218+ parts = append(parts, params)
222+ if m.result.ToolCallID != "" {
223+ if m.result.IsError {
224+ parts = append(parts, "### Error:")
225+ parts = append(parts, m.result.Content)
227+ parts = append(parts, "### Result:")
228+ content := m.formatResultForCopy()
230+ parts = append(parts, content)
233+ } else if m.cancelled {
234+ parts = append(parts, "### Status:")
235+ parts = append(parts, "Cancelled")
237+ parts = append(parts, "### Status:")
238+ parts = append(parts, "Pending...")
241+ return strings.Join(parts, "\n\n")
244+func (m *toolCallCmp) formatParametersForCopy() string {
245+ switch m.call.Name {
246+ case tools.BashToolName:
247+ var params tools.BashParams
248+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
249+ cmd := strings.ReplaceAll(params.Command, "\n", " ")
250+ cmd = strings.ReplaceAll(cmd, "\t", " ")
251+ return fmt.Sprintf("**Command:** %s", cmd)
253+ case tools.ViewToolName:
254+ var params tools.ViewParams
255+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
257+ parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
258+ if params.Limit > 0 {
259+ parts = append(parts, fmt.Sprintf("**Limit:** %d", params.Limit))
261+ if params.Offset > 0 {
262+ parts = append(parts, fmt.Sprintf("**Offset:** %d", params.Offset))
264+ return strings.Join(parts, "\n")
266+ case tools.EditToolName:
267+ var params tools.EditParams
268+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
269+ return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
271+ case tools.MultiEditToolName:
272+ var params tools.MultiEditParams
273+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
275+ parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
276+ parts = append(parts, fmt.Sprintf("**Edits:** %d", len(params.Edits)))
277+ return strings.Join(parts, "\n")
279+ case tools.WriteToolName:
280+ var params tools.WriteParams
281+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
282+ return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
284+ case tools.FetchToolName:
285+ var params tools.FetchParams
286+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
288+ parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
289+ if params.Format != "" {
290+ parts = append(parts, fmt.Sprintf("**Format:** %s", params.Format))
292+ if params.Timeout > 0 {
293+ parts = append(parts, fmt.Sprintf("**Timeout:** %s", (time.Duration(params.Timeout)*time.Second).String()))
295+ return strings.Join(parts, "\n")
297+ case tools.GrepToolName:
298+ var params tools.GrepParams
299+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
301+ parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
302+ if params.Path != "" {
303+ parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
305+ if params.Include != "" {
306+ parts = append(parts, fmt.Sprintf("**Include:** %s", params.Include))
308+ if params.LiteralText {
309+ parts = append(parts, "**Literal:** true")
311+ return strings.Join(parts, "\n")
313+ case tools.GlobToolName:
314+ var params tools.GlobParams
315+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
317+ parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
318+ if params.Path != "" {
319+ parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
321+ return strings.Join(parts, "\n")
323+ case tools.LSToolName:
324+ var params tools.LSParams
325+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
330+ return fmt.Sprintf("**Path:** %s", fsext.PrettyPath(path))
332+ case tools.DownloadToolName:
333+ var params tools.DownloadParams
334+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
336+ parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
337+ parts = append(parts, fmt.Sprintf("**File Path:** %s", fsext.PrettyPath(params.FilePath)))
338+ if params.Timeout > 0 {
339+ parts = append(parts, fmt.Sprintf("**Timeout:** %s", (time.Duration(params.Timeout)*time.Second).String()))
341+ return strings.Join(parts, "\n")
343+ case tools.SourcegraphToolName:
344+ var params tools.SourcegraphParams
345+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
347+ parts = append(parts, fmt.Sprintf("**Query:** %s", params.Query))
348+ if params.Count > 0 {
349+ parts = append(parts, fmt.Sprintf("**Count:** %d", params.Count))
351+ if params.ContextWindow > 0 {
352+ parts = append(parts, fmt.Sprintf("**Context:** %d", params.ContextWindow))
354+ return strings.Join(parts, "\n")
356+ case tools.DiagnosticsToolName:
357+ return "**Project:** diagnostics"
358+ case agent.AgentToolName:
359+ var params agent.AgentParams
360+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
361+ return fmt.Sprintf("**Task:**\n%s", params.Prompt)
365+ var params map[string]any
366+ if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
368+ for key, value := range params {
369+ displayKey := strings.ReplaceAll(key, "_", " ")
370+ if len(displayKey) > 0 {
371+ displayKey = strings.ToUpper(displayKey[:1]) + displayKey[1:]
373+ parts = append(parts, fmt.Sprintf("**%s:** %v", displayKey, value))
375+ return strings.Join(parts, "\n")
381+func (m *toolCallCmp) formatResultForCopy() string {
382+ switch m.call.Name {
383+ case tools.BashToolName:
384+ return m.formatBashResultForCopy()
385+ case tools.ViewToolName:
386+ return m.formatViewResultForCopy()
387+ case tools.EditToolName:
388+ return m.formatEditResultForCopy()
389+ case tools.MultiEditToolName:
390+ return m.formatMultiEditResultForCopy()
391+ case tools.WriteToolName:
392+ return m.formatWriteResultForCopy()
393+ case tools.FetchToolName:
394+ return m.formatFetchResultForCopy()
395+ case agent.AgentToolName:
396+ return m.formatAgentResultForCopy()
397+ case tools.DownloadToolName, tools.GrepToolName, tools.GlobToolName, tools.LSToolName, tools.SourcegraphToolName, tools.DiagnosticsToolName:
398+ return fmt.Sprintf("```\n%s\n```", m.result.Content)
400+ return m.result.Content
404+func (m *toolCallCmp) formatBashResultForCopy() string {
405+ var meta tools.BashResponseMetadata
406+ if m.result.Metadata != "" {
407+ json.Unmarshal([]byte(m.result.Metadata), &meta)
410+ output := meta.Output
411+ if output == "" && m.result.Content != tools.BashNoOutput {
412+ output = m.result.Content
419+ return fmt.Sprintf("```bash\n%s\n```", output)
422+func (m *toolCallCmp) formatViewResultForCopy() string {
423+ var meta tools.ViewResponseMetadata
424+ if m.result.Metadata != "" {
425+ json.Unmarshal([]byte(m.result.Metadata), &meta)
428+ if meta.Content == "" {
429+ return m.result.Content
433+ if meta.FilePath != "" {
434+ ext := strings.ToLower(filepath.Ext(meta.FilePath))
450+ case ".cpp", ".cc", ".cxx":
452+ case ".sh", ".bash":
456+ case ".yaml", ".yml":
469+ var result strings.Builder
471+ result.WriteString(fmt.Sprintf("```%s\n", lang))
473+ result.WriteString("```\n")
475+ result.WriteString(meta.Content)
476+ result.WriteString("\n```")
478+ return result.String()
481+func (m *toolCallCmp) formatEditResultForCopy() string {
482+ var meta tools.EditResponseMetadata
483+ if m.result.Metadata == "" {
484+ return m.result.Content
487+ if json.Unmarshal([]byte(m.result.Metadata), &meta) != nil {
488+ return m.result.Content
491+ var params tools.EditParams
492+ json.Unmarshal([]byte(m.call.Input), ¶ms)
494+ var result strings.Builder
496+ if meta.OldContent != "" || meta.NewContent != "" {
497+ fileName := params.FilePath
499+ fileName = fsext.PrettyPath(fileName)
501+ diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
503+ result.WriteString(fmt.Sprintf("Changes: +%d -%d\n", additions, removals))
504+ result.WriteString("```diff\n")
505+ result.WriteString(diffContent)
506+ result.WriteString("\n```")
509+ return result.String()
512+func (m *toolCallCmp) formatMultiEditResultForCopy() string {
513+ var meta tools.MultiEditResponseMetadata
514+ if m.result.Metadata == "" {
515+ return m.result.Content
518+ if json.Unmarshal([]byte(m.result.Metadata), &meta) != nil {
519+ return m.result.Content
522+ var params tools.MultiEditParams
523+ json.Unmarshal([]byte(m.call.Input), ¶ms)
525+ var result strings.Builder
526+ if meta.OldContent != "" || meta.NewContent != "" {
527+ fileName := params.FilePath
529+ fileName = fsext.PrettyPath(fileName)
531+ diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
533+ result.WriteString(fmt.Sprintf("Changes: +%d -%d\n", additions, removals))
534+ result.WriteString("```diff\n")
535+ result.WriteString(diffContent)
536+ result.WriteString("\n```")
539+ return result.String()
542+func (m *toolCallCmp) formatWriteResultForCopy() string {
543+ var params tools.WriteParams
544+ if json.Unmarshal([]byte(m.call.Input), ¶ms) != nil {
545+ return m.result.Content
549+ if params.FilePath != "" {
550+ ext := strings.ToLower(filepath.Ext(params.FilePath))
566+ case ".cpp", ".cc", ".cxx":
568+ case ".sh", ".bash":
572+ case ".yaml", ".yml":
585+ var result strings.Builder
586+ result.WriteString(fmt.Sprintf("File: %s\n", fsext.PrettyPath(params.FilePath)))
588+ result.WriteString(fmt.Sprintf("```%s\n", lang))
590+ result.WriteString("```\n")
592+ result.WriteString(params.Content)
593+ result.WriteString("\n```")
595+ return result.String()
598+func (m *toolCallCmp) formatFetchResultForCopy() string {
599+ var params tools.FetchParams
600+ if json.Unmarshal([]byte(m.call.Input), ¶ms) != nil {
601+ return m.result.Content
604+ var result strings.Builder
605+ if params.URL != "" {
606+ result.WriteString(fmt.Sprintf("URL: %s\n", params.URL))
609+ switch params.Format {
611+ result.WriteString("```html\n")
613+ result.WriteString("```\n")
614+ default: // markdown
615+ result.WriteString("```markdown\n")
617+ result.WriteString(m.result.Content)
618+ result.WriteString("\n```")
620+ return result.String()
623+func (m *toolCallCmp) formatAgentResultForCopy() string {
624+ var result strings.Builder
626+ if len(m.nestedToolCalls) > 0 {
627+ result.WriteString("### Nested Tool Calls:\n")
628+ for i, nestedCall := range m.nestedToolCalls {
629+ nestedContent := nestedCall.(*toolCallCmp).formatToolForCopy()
630+ indentedContent := strings.ReplaceAll(nestedContent, "\n", "\n ")
631+ result.WriteString(fmt.Sprintf("%d. %s\n", i+1, indentedContent))
632+ if i < len(m.nestedToolCalls)-1 {
633+ result.WriteString("\n")
637+ if m.result.Content != "" {
638+ result.WriteString("\n### Final Result:\n")
642+ if m.result.Content != "" {
643+ result.WriteString(fmt.Sprintf("```\n%s\n```", m.result.Content))
646+ return result.String()
185// SetToolCall updates the tool call data and stops spinning if finished
649// SetToolCall updates the tool call data and stops spinning if finished
186func (m *toolCallCmp) SetToolCall(call message.ToolCall) {
650func (m *toolCallCmp) SetToolCall(call message.ToolCall) {
187 m.call = call
651 m.call = call