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