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/logging"
10 "github.com/opencode-ai/opencode/internal/message"
11 "github.com/opencode-ai/opencode/internal/tui/components/anim"
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 GetToolCall() message.ToolCall
23 GetToolResult() message.ToolResult
24 SetToolResult(message.ToolResult)
25 SetToolCall(message.ToolCall)
26 SetCancelled()
27 ParentMessageId() string
28 Spinning() bool
29}
30
31type toolCallCmp struct {
32 width int
33 focused bool
34
35 parentMessageId string
36 call message.ToolCall
37 result message.ToolResult
38 cancelled bool
39
40 spinning bool
41 anim util.Model
42}
43
44type ToolCallOption func(*toolCallCmp)
45
46func WithToolCallCancelled() ToolCallOption {
47 return func(m *toolCallCmp) {
48 m.cancelled = true
49 }
50}
51
52func WithToolCallResult(result message.ToolResult) ToolCallOption {
53 return func(m *toolCallCmp) {
54 m.result = result
55 }
56}
57
58func NewToolCallCmp(parentMessageId string, tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp {
59 m := &toolCallCmp{
60 call: tc,
61 parentMessageId: parentMessageId,
62 anim: anim.New(15, "Working"),
63 }
64 for _, opt := range opts {
65 opt(m)
66 }
67 return m
68}
69
70func (m *toolCallCmp) Init() tea.Cmd {
71 m.spinning = m.shouldSpin()
72 logging.Info("Initializing tool call spinner", "tool_call", m.call.Name, "spinning", m.spinning)
73 if m.spinning {
74 return m.anim.Init()
75 }
76 return nil
77}
78
79func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
80 logging.Debug("Tool call update", "msg", msg)
81 switch msg := msg.(type) {
82 case anim.ColorCycleMsg, anim.StepCharsMsg:
83 if m.spinning {
84 u, cmd := m.anim.Update(msg)
85 m.anim = u.(util.Model)
86 return m, cmd
87 }
88 }
89 return m, nil
90}
91
92func (m *toolCallCmp) View() string {
93 box := m.style()
94
95 if !m.call.Finished && !m.cancelled {
96 return box.PaddingLeft(1).Render(m.renderPending())
97 }
98
99 r := registry.lookup(m.call.Name)
100 return box.PaddingLeft(1).Render(r.Render(m))
101}
102
103// SetCancelled implements ToolCallCmp.
104func (m *toolCallCmp) SetCancelled() {
105 m.cancelled = true
106}
107
108// SetToolCall implements ToolCallCmp.
109func (m *toolCallCmp) SetToolCall(call message.ToolCall) {
110 m.call = call
111 if m.call.Finished {
112 m.spinning = false
113 }
114}
115
116// ParentMessageId implements ToolCallCmp.
117func (m *toolCallCmp) ParentMessageId() string {
118 return m.parentMessageId
119}
120
121// SetToolResult implements ToolCallCmp.
122func (m *toolCallCmp) SetToolResult(result message.ToolResult) {
123 m.result = result
124 m.spinning = false
125}
126
127// GetToolCall implements ToolCallCmp.
128func (m *toolCallCmp) GetToolCall() message.ToolCall {
129 return m.call
130}
131
132// GetToolResult implements ToolCallCmp.
133func (m *toolCallCmp) GetToolResult() message.ToolResult {
134 return m.result
135}
136
137func (m *toolCallCmp) renderPending() string {
138 return fmt.Sprintf("%s: %s", prettifyToolName(m.call.Name), m.anim.View())
139}
140
141func (m *toolCallCmp) style() lipgloss.Style {
142 t := theme.CurrentTheme()
143 borderStyle := lipgloss.NormalBorder()
144 if m.focused {
145 borderStyle = lipgloss.DoubleBorder()
146 }
147 return styles.BaseStyle().
148 BorderLeft(true).
149 Foreground(t.TextMuted()).
150 BorderForeground(t.TextMuted()).
151 BorderStyle(borderStyle)
152}
153
154func (m *toolCallCmp) textWidth() int {
155 return m.width - 2 // take into account the border and PaddingLeft
156}
157
158func (m *toolCallCmp) fit(content string, width int) string {
159 t := theme.CurrentTheme()
160 lineStyle := lipgloss.NewStyle().Background(t.BackgroundSecondary()).Foreground(t.TextMuted())
161 dots := lineStyle.Render("...")
162 return ansi.Truncate(content, width, dots)
163}
164
165func (m *toolCallCmp) Blur() tea.Cmd {
166 m.focused = false
167 return nil
168}
169
170func (m *toolCallCmp) Focus() tea.Cmd {
171 m.focused = true
172 return nil
173}
174
175// IsFocused implements MessageModel.
176func (m *toolCallCmp) IsFocused() bool {
177 return m.focused
178}
179
180func (m *toolCallCmp) GetSize() (int, int) {
181 return m.width, 0
182}
183
184func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
185 m.width = width
186 return nil
187}
188
189func (m *toolCallCmp) shouldSpin() bool {
190 if !m.call.Finished {
191 return true
192 } else if m.result.ToolCallID != m.call.ID {
193 return true
194 }
195 return false
196}
197
198func (m *toolCallCmp) Spinning() bool {
199 return m.spinning
200}