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
 18// ToolCallCmp defines the interface for tool call components in the chat interface.
 19// It manages the display of tool execution including pending states, results, and errors.
 20type ToolCallCmp interface {
 21	util.Model                         // Basic Bubble Tea model interface
 22	layout.Sizeable                    // Width/height management
 23	layout.Focusable                   // Focus state management
 24	GetToolCall() message.ToolCall     // Access to tool call data
 25	GetToolResult() message.ToolResult // Access to tool result data
 26	SetToolResult(message.ToolResult)  // Update tool result
 27	SetToolCall(message.ToolCall)      // Update tool call
 28	SetCancelled()                     // Mark as cancelled
 29	ParentMessageId() string           // Get parent message ID
 30	Spinning() bool                    // Animation state for pending tools
 31}
 32
 33// toolCallCmp implements the ToolCallCmp interface for displaying tool calls.
 34// It handles rendering of tool execution states including pending, completed, and error states.
 35type toolCallCmp struct {
 36	width   int  // Component width for text wrapping
 37	focused bool // Focus state for border styling
 38
 39	// Tool call data and state
 40	parentMessageId string             // ID of the message that initiated this tool call
 41	call            message.ToolCall   // The tool call being executed
 42	result          message.ToolResult // The result of the tool execution
 43	cancelled       bool               // Whether the tool call was cancelled
 44
 45	// Animation state for pending tool calls
 46	spinning bool       // Whether to show loading animation
 47	anim     util.Model // Animation component for pending states
 48}
 49
 50// ToolCallOption provides functional options for configuring tool call components
 51type ToolCallOption func(*toolCallCmp)
 52
 53// WithToolCallCancelled marks the tool call as cancelled
 54func WithToolCallCancelled() ToolCallOption {
 55	return func(m *toolCallCmp) {
 56		m.cancelled = true
 57	}
 58}
 59
 60// WithToolCallResult sets the initial tool result
 61func WithToolCallResult(result message.ToolResult) ToolCallOption {
 62	return func(m *toolCallCmp) {
 63		m.result = result
 64	}
 65}
 66
 67// NewToolCallCmp creates a new tool call component with the given parent message ID,
 68// tool call, and optional configuration
 69func NewToolCallCmp(parentMessageId string, tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp {
 70	m := &toolCallCmp{
 71		call:            tc,
 72		parentMessageId: parentMessageId,
 73		anim:            anim.New(15, "Working"),
 74	}
 75	for _, opt := range opts {
 76		opt(m)
 77	}
 78	return m
 79}
 80
 81// Init initializes the tool call component and starts animations if needed.
 82// Returns a command to start the animation for pending tool calls.
 83func (m *toolCallCmp) Init() tea.Cmd {
 84	m.spinning = m.shouldSpin()
 85	logging.Info("Initializing tool call spinner", "tool_call", m.call.Name, "spinning", m.spinning)
 86	if m.spinning {
 87		return m.anim.Init()
 88	}
 89	return nil
 90}
 91
 92// Update handles incoming messages and updates the component state.
 93// Manages animation updates for pending tool calls.
 94func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 95	logging.Debug("Tool call update", "msg", msg)
 96	switch msg := msg.(type) {
 97	case anim.ColorCycleMsg, anim.StepCharsMsg:
 98		if m.spinning {
 99			u, cmd := m.anim.Update(msg)
100			m.anim = u.(util.Model)
101			return m, cmd
102		}
103	}
104	return m, nil
105}
106
107// View renders the tool call component based on its current state.
108// Shows either a pending animation or the tool-specific rendered result.
109func (m *toolCallCmp) View() string {
110	box := m.style()
111
112	if !m.call.Finished && !m.cancelled {
113		return box.PaddingLeft(1).Render(m.renderPending())
114	}
115
116	r := registry.lookup(m.call.Name)
117	return box.PaddingLeft(1).Render(r.Render(m))
118}
119
120// State management methods
121
122// SetCancelled marks the tool call as cancelled
123func (m *toolCallCmp) SetCancelled() {
124	m.cancelled = true
125}
126
127// SetToolCall updates the tool call data and stops spinning if finished
128func (m *toolCallCmp) SetToolCall(call message.ToolCall) {
129	m.call = call
130	if m.call.Finished {
131		m.spinning = false
132	}
133}
134
135// ParentMessageId returns the ID of the message that initiated this tool call
136func (m *toolCallCmp) ParentMessageId() string {
137	return m.parentMessageId
138}
139
140// SetToolResult updates the tool result and stops the spinning animation
141func (m *toolCallCmp) SetToolResult(result message.ToolResult) {
142	m.result = result
143	m.spinning = false
144}
145
146// GetToolCall returns the current tool call data
147func (m *toolCallCmp) GetToolCall() message.ToolCall {
148	return m.call
149}
150
151// GetToolResult returns the current tool result data
152func (m *toolCallCmp) GetToolResult() message.ToolResult {
153	return m.result
154}
155
156// Rendering methods
157
158// renderPending displays the tool name with a loading animation for pending tool calls
159func (m *toolCallCmp) renderPending() string {
160	return fmt.Sprintf("%s: %s", prettifyToolName(m.call.Name), m.anim.View())
161}
162
163// style returns the lipgloss style for the tool call component.
164// Applies muted colors and focus-dependent border styles.
165func (m *toolCallCmp) style() lipgloss.Style {
166	t := theme.CurrentTheme()
167	borderStyle := lipgloss.NormalBorder()
168	if m.focused {
169		borderStyle = lipgloss.DoubleBorder()
170	}
171	return styles.BaseStyle().
172		BorderLeft(true).
173		Foreground(t.TextMuted()).
174		BorderForeground(t.TextMuted()).
175		BorderStyle(borderStyle)
176}
177
178// textWidth calculates the available width for text content,
179// accounting for borders and padding
180func (m *toolCallCmp) textWidth() int {
181	return m.width - 2 // take into account the border and PaddingLeft
182}
183
184// fit truncates content to fit within the specified width with ellipsis
185func (m *toolCallCmp) fit(content string, width int) string {
186	t := theme.CurrentTheme()
187	lineStyle := lipgloss.NewStyle().Background(t.BackgroundSecondary()).Foreground(t.TextMuted())
188	dots := lineStyle.Render("...")
189	return ansi.Truncate(content, width, dots)
190}
191
192// Focus management methods
193
194// Blur removes focus from the tool call component
195func (m *toolCallCmp) Blur() tea.Cmd {
196	m.focused = false
197	return nil
198}
199
200// Focus sets focus on the tool call component
201func (m *toolCallCmp) Focus() tea.Cmd {
202	m.focused = true
203	return nil
204}
205
206// IsFocused returns whether the tool call component is currently focused
207func (m *toolCallCmp) IsFocused() bool {
208	return m.focused
209}
210
211// Size management methods
212
213// GetSize returns the current dimensions of the tool call component
214func (m *toolCallCmp) GetSize() (int, int) {
215	return m.width, 0
216}
217
218// SetSize updates the width of the tool call component for text wrapping
219func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
220	m.width = width
221	return nil
222}
223
224// shouldSpin determines whether the tool call should show a loading animation.
225// Returns true if the tool call is not finished or if the result doesn't match the call ID.
226func (m *toolCallCmp) shouldSpin() bool {
227	if !m.call.Finished {
228		return true
229	} else if m.result.ToolCallID != m.call.ID {
230		return true
231	}
232	return false
233}
234
235// Spinning returns whether the tool call is currently showing a loading animation
236func (m *toolCallCmp) Spinning() bool {
237	return m.spinning
238}