1package messages
2
3import (
4 "fmt"
5
6 tea "github.com/charmbracelet/bubbletea/v2"
7 "github.com/charmbracelet/lipgloss/v2"
8 "github.com/charmbracelet/x/ansi"
9 "github.com/opencode-ai/opencode/internal/llm/agent"
10 "github.com/opencode-ai/opencode/internal/llm/tools"
11 "github.com/opencode-ai/opencode/internal/message"
12 "github.com/opencode-ai/opencode/internal/tui/layout"
13 "github.com/opencode-ai/opencode/internal/tui/styles"
14 "github.com/opencode-ai/opencode/internal/tui/theme"
15 "github.com/opencode-ai/opencode/internal/tui/util"
16)
17
18type ToolCallCmp interface {
19 util.Model
20 layout.Sizeable
21 layout.Focusable
22}
23
24type toolCallCmp struct {
25 width int
26 focused bool
27
28 call message.ToolCall
29 result message.ToolResult
30 cancelled bool
31}
32
33type ToolCallOption func(*toolCallCmp)
34
35func WithToolCallCancelled() ToolCallOption {
36 return func(m *toolCallCmp) {
37 m.cancelled = true
38 }
39}
40
41func WithToolCallResult(result message.ToolResult) ToolCallOption {
42 return func(m *toolCallCmp) {
43 m.result = result
44 }
45}
46
47func NewToolCallCmp(tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp {
48 m := &toolCallCmp{
49 call: tc,
50 }
51 for _, opt := range opts {
52 opt(m)
53 }
54 return m
55}
56
57func (m *toolCallCmp) Init() tea.Cmd {
58 return nil
59}
60
61func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
62 return m, nil
63}
64
65func (m *toolCallCmp) View() string {
66 box := m.style()
67
68 if !m.call.Finished && !m.cancelled {
69 return box.PaddingLeft(1).Render(m.renderPending())
70 }
71
72 r := registry.lookup(m.call.Name)
73 return box.PaddingLeft(1).Render(r.Render(m))
74}
75
76func (v *toolCallCmp) renderPending() string {
77 return fmt.Sprintf("%s: %s", prettifyToolName(v.call.Name), toolAction(v.call.Name))
78}
79
80func (msg *toolCallCmp) style() lipgloss.Style {
81 t := theme.CurrentTheme()
82 borderStyle := lipgloss.NormalBorder()
83 if msg.focused {
84 borderStyle = lipgloss.DoubleBorder()
85 }
86 return styles.BaseStyle().
87 BorderLeft(true).
88 Foreground(t.TextMuted()).
89 BorderForeground(t.TextMuted()).
90 BorderStyle(borderStyle)
91}
92
93func (m *toolCallCmp) textWidth() int {
94 return m.width - 2 // take into account the border and PaddingLeft
95}
96
97func (m *toolCallCmp) fit(content string, width int) string {
98 t := theme.CurrentTheme()
99 lineStyle := lipgloss.NewStyle().Background(t.BackgroundSecondary()).Foreground(t.TextMuted())
100 dots := lineStyle.Render("...")
101 return ansi.Truncate(content, width, dots)
102}
103
104func (m *toolCallCmp) toolName() string {
105 switch m.call.Name {
106 case agent.AgentToolName:
107 return "Task"
108 case tools.BashToolName:
109 return "Bash"
110 case tools.EditToolName:
111 return "Edit"
112 case tools.FetchToolName:
113 return "Fetch"
114 case tools.GlobToolName:
115 return "Glob"
116 case tools.GrepToolName:
117 return "Grep"
118 case tools.LSToolName:
119 return "List"
120 case tools.SourcegraphToolName:
121 return "Sourcegraph"
122 case tools.ViewToolName:
123 return "View"
124 case tools.WriteToolName:
125 return "Write"
126 case tools.PatchToolName:
127 return "Patch"
128 default:
129 return m.call.Name
130 }
131}
132
133func (m *toolCallCmp) Blur() tea.Cmd {
134 m.focused = false
135 return nil
136}
137
138func (m *toolCallCmp) Focus() tea.Cmd {
139 m.focused = true
140 return nil
141}
142
143// IsFocused implements MessageModel.
144func (m *toolCallCmp) IsFocused() bool {
145 return m.focused
146}
147
148func (m *toolCallCmp) GetSize() (int, int) {
149 return m.width, 0
150}
151
152func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
153 m.width = width
154 return nil
155}