tool.go

  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}