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/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}