Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Commit 2883256

Browse files
committed
implement tool calls in the ui
1 parent 4b5ea74 commit 2883256

11 files changed

Lines changed: 269 additions & 163 deletions

File tree

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ indent_size = 2
1111

1212
[*.go]
1313
indent_style = tab
14-
indent_size = 8
14+
indent_size = 4
1515

1616
[*.golden]
1717
insert_final_newline = false

internal/llm/agent/agent.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -443,18 +443,14 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
443443
assistantMsg.AppendContent(event.Content)
444444
return a.messages.Update(ctx, *assistantMsg)
445445
case provider.EventToolUseStart:
446+
logging.Info("Tool call started", "toolCall", event.ToolCall)
446447
assistantMsg.AddToolCall(*event.ToolCall)
447448
return a.messages.Update(ctx, *assistantMsg)
448-
// TODO: see how to handle this
449-
// case provider.EventToolUseDelta:
450-
// tm := time.Unix(assistantMsg.UpdatedAt, 0)
451-
// assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input)
452-
// if time.Since(tm) > 1000*time.Millisecond {
453-
// err := a.messages.Update(ctx, *assistantMsg)
454-
// assistantMsg.UpdatedAt = time.Now().Unix()
455-
// return err
456-
// }
449+
case provider.EventToolUseDelta:
450+
assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input)
451+
return a.messages.Update(ctx, *assistantMsg)
457452
case provider.EventToolUseStop:
453+
logging.Info("Finished tool call", "toolCall", event.ToolCall)
458454
assistantMsg.FinishToolCall(event.ToolCall.ID)
459455
return a.messages.Update(ctx, *assistantMsg)
460456
case provider.EventError:

internal/llm/prompt/coder.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ When making changes to files, first understand the file's code conventions. Mimi
153153
154154
# Doing tasks
155155
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
156-
1. Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.
156+
1. Use the available search tools to understand the codebase and the user's query.
157157
2. Implement the solution using all tools available to you
158158
3. Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
159159
4. VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to opencode.md so that you will know to run it next time.
@@ -162,7 +162,7 @@ NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTAN
162162
163163
# Tool usage policy
164164
- When doing file search, prefer to use the Agent tool in order to reduce context usage.
165-
- If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in the same function_calls block.
165+
- If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in parallel.
166166
- IMPORTANT: The user does not see the full output of the tool responses, so if you need the output of the tool for the response make sure to summarize it for the user.
167167
168168
You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.`

internal/llm/provider/anthropic.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
305305
ToolCall: &message.ToolCall{
306306
ID: currentToolCallID,
307307
Finished: false,
308-
Input: event.Delta.JSON.PartialJSON.Raw(),
308+
Input: event.Delta.PartialJSON,
309309
},
310310
}
311311
}

internal/tui/components/anim/anim.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/charmbracelet/bubbles/v2/spinner"
1010
tea "github.com/charmbracelet/bubbletea/v2"
1111
"github.com/charmbracelet/lipgloss/v2"
12+
"github.com/google/uuid"
1213
"github.com/lucasb-eyer/go-colorful"
1314
"github.com/opencode-ai/opencode/internal/tui/styles"
1415
"github.com/opencode-ai/opencode/internal/tui/theme"
@@ -54,19 +55,23 @@ func (c cyclingChar) state(start time.Time) charState {
5455
return charCyclingState
5556
}
5657

57-
type StepCharsMsg struct{}
58+
type StepCharsMsg struct {
59+
id string
60+
}
5861

59-
func stepChars() tea.Cmd {
62+
func stepChars(id string) tea.Cmd {
6063
return tea.Tick(charCyclingFPS, func(time.Time) tea.Msg {
61-
return StepCharsMsg{}
64+
return StepCharsMsg{id}
6265
})
6366
}
6467

65-
type ColorCycleMsg struct{}
68+
type ColorCycleMsg struct {
69+
id string
70+
}
6671

67-
func cycleColors() tea.Cmd {
72+
func cycleColors(id string) tea.Cmd {
6873
return tea.Tick(colorCycleFPS, func(time.Time) tea.Msg {
69-
return ColorCycleMsg{}
74+
return ColorCycleMsg{id}
7075
})
7176
}
7277

@@ -80,6 +85,7 @@ type anim struct {
8085
label []rune
8186
ellipsis spinner.Model
8287
ellipsisStarted bool
88+
id string
8389
}
8490

8591
func New(cyclingCharsSize uint, label string) util.Model {
@@ -91,10 +97,12 @@ func New(cyclingCharsSize uint, label string) util.Model {
9197
gap = ""
9298
}
9399

100+
id := uuid.New()
94101
c := anim{
95102
start: time.Now(),
96103
label: []rune(gap + label),
97104
ellipsis: spinner.New(spinner.WithSpinner(spinner.Ellipsis)),
105+
id: id.String(),
98106
}
99107

100108
// If we're in truecolor mode (and there are enough cycling characters)
@@ -144,15 +152,18 @@ func New(cyclingCharsSize uint, label string) util.Model {
144152
}
145153

146154
// Init initializes the animation.
147-
func (anim) Init() tea.Cmd {
148-
return tea.Batch(stepChars(), cycleColors())
155+
func (a anim) Init() tea.Cmd {
156+
return tea.Batch(stepChars(a.id), cycleColors(a.id))
149157
}
150158

151159
// Update handles messages.
152160
func (a anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
153161
var cmd tea.Cmd
154-
switch msg.(type) {
162+
switch msg := msg.(type) {
155163
case StepCharsMsg:
164+
if msg.id != a.id {
165+
return a, nil
166+
}
156167
a.updateChars(&a.cyclingChars)
157168
a.updateChars(&a.labelChars)
158169

@@ -173,14 +184,17 @@ func (a anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
173184
}
174185
}
175186

176-
return a, tea.Batch(stepChars(), cmd)
187+
return a, tea.Batch(stepChars(a.id), cmd)
177188
case ColorCycleMsg:
189+
if msg.id != a.id {
190+
return a, nil
191+
}
178192
const minColorCycleSize = 2
179193
if len(a.ramp) < minColorCycleSize {
180194
return a, nil
181195
}
182196
a.ramp = append(a.ramp[1:], a.ramp[0])
183-
return a, cycleColors()
197+
return a, cycleColors(a.id)
184198
case spinner.TickMsg:
185199
var cmd tea.Cmd
186200
a.ellipsis, cmd = a.ellipsis.Update(msg)
@@ -216,7 +230,7 @@ func (a anim) View() string {
216230
b.WriteRune(c.currentValue)
217231
}
218232

219-
if len(a.label) > 1 {
233+
if len(a.labelChars) > 1 {
220234
textStyle := styles.BaseStyle().
221235
Foreground(t.Text())
222236
for _, c := range a.labelChars {

internal/tui/components/chat/list_v2.go

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
tea "github.com/charmbracelet/bubbletea/v2"
88
"github.com/charmbracelet/lipgloss/v2"
99
"github.com/opencode-ai/opencode/internal/app"
10+
"github.com/opencode-ai/opencode/internal/logging"
1011
"github.com/opencode-ai/opencode/internal/message"
1112
"github.com/opencode-ai/opencode/internal/pubsub"
1213
"github.com/opencode-ai/opencode/internal/session"
@@ -61,7 +62,8 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6162
return m, m.listCmp.SetItems([]util.Model{})
6263

6364
case pubsub.Event[message.Message]:
64-
return m, m.handleMessageEvent(msg)
65+
cmd := m.handleMessageEvent(msg)
66+
return m, cmd
6567
default:
6668
var cmds []tea.Cmd
6769
u, cmd := m.listCmp.Update(msg)
@@ -92,8 +94,8 @@ func (m *messageListCmp) handleMessageEvent(event pubsub.Event[message.Message])
9294
// more likely to be at the end of the list
9395
items := m.listCmp.Items()
9496
for i := len(items) - 1; i >= 0; i-- {
95-
msg := items[i].(messages.MessageCmp)
96-
if msg.GetMessage().ID == event.Payload.ID {
97+
msg, ok := items[i].(messages.MessageCmp)
98+
if ok && msg.GetMessage().ID == event.Payload.ID {
9799
messageExists = true
98100
break
99101
}
@@ -109,7 +111,6 @@ func (m *messageListCmp) handleMessageEvent(event pubsub.Event[message.Message])
109111
case message.Tool:
110112
return m.handleToolMessage(event.Payload)
111113
}
112-
// TODO: handle tools
113114
case pubsub.UpdatedEvent:
114115
return m.handleUpdateAssistantMessage(event.Payload)
115116
}
@@ -122,30 +123,79 @@ func (m *messageListCmp) handleNewUserMessage(msg message.Message) tea.Cmd {
122123
}
123124

124125
func (m *messageListCmp) handleToolMessage(msg message.Message) tea.Cmd {
126+
items := m.listCmp.Items()
127+
for _, tr := range msg.ToolResults() {
128+
for i := len(items) - 1; i >= 0; i-- {
129+
message := items[i]
130+
if toolCall, ok := message.(messages.ToolCallCmp); ok {
131+
if toolCall.GetToolCall().ID == tr.ToolCallID {
132+
toolCall.SetToolResult(tr)
133+
m.listCmp.UpdateItem(
134+
i,
135+
toolCall,
136+
)
137+
break
138+
}
139+
}
140+
}
141+
}
125142
return nil
126143
}
127144

128145
func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.Cmd {
146+
var cmds []tea.Cmd
129147
// Simple update the content
130148
items := m.listCmp.Items()
131-
lastItem := items[len(items)-1].(messages.MessageCmp)
132-
// TODO:handle tool calls
133-
if lastItem.GetMessage().ID != msg.ID {
134-
return nil
149+
assistantMessageInx := -1
150+
toolCalls := map[int]messages.ToolCallCmp{}
151+
152+
// we go backwards because the messages are most likely at the end of the list
153+
for i := len(items) - 1; i >= 0; i-- {
154+
message := items[i]
155+
if asMsg, ok := message.(messages.MessageCmp); ok {
156+
if asMsg.GetMessage().ID == msg.ID {
157+
assistantMessageInx = i
158+
}
159+
} else if tc, ok := message.(messages.ToolCallCmp); ok {
160+
if tc.ParentMessageId() == msg.ID {
161+
toolCalls[i] = tc
162+
}
163+
}
135164
}
136-
// for now just updet the last message
137-
if len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking() {
165+
166+
logging.Info("Update Assistant Message", "msg", msg, "assistantMessageInx", assistantMessageInx, "toolCalls", toolCalls)
167+
168+
if assistantMessageInx > -1 && (len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking()) {
138169
m.listCmp.UpdateItem(
139-
len(items)-1,
170+
assistantMessageInx,
140171
messages.NewMessageCmp(
141172
msg,
142173
messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
143174
),
144175
)
145-
} else if len(msg.ToolCalls()) > 0 && msg.Content().Text == "" {
146-
m.listCmp.DeleteItem(len(items) - 1)
176+
} else if assistantMessageInx > -1 && len(msg.ToolCalls()) > 0 && msg.Content().Text == "" {
177+
m.listCmp.DeleteItem(assistantMessageInx)
147178
}
148-
return nil
179+
for _, tc := range msg.ToolCalls() {
180+
found := false
181+
for inx, tcc := range toolCalls {
182+
if tc.ID == tcc.GetToolCall().ID {
183+
tcc.SetToolCall(tc)
184+
m.listCmp.UpdateItem(
185+
inx,
186+
tcc,
187+
)
188+
found = true
189+
break
190+
}
191+
}
192+
if !found {
193+
cmd := m.listCmp.AppendItem(messages.NewToolCallCmp(msg.ID, tc))
194+
cmds = append(cmds, cmd)
195+
}
196+
}
197+
198+
return tea.Batch(cmds...)
149199
}
150200

151201
func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd {
@@ -161,7 +211,7 @@ func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd
161211
cmds = append(cmds, cmd)
162212
}
163213
for _, tc := range msg.ToolCalls() {
164-
cmd := m.listCmp.AppendItem(messages.NewToolCallCmp(tc))
214+
cmd := m.listCmp.AppendItem(messages.NewToolCallCmp(msg.ID, tc))
165215
cmds = append(cmds, cmd)
166216
}
167217
return tea.Batch(cmds...)
@@ -206,10 +256,10 @@ func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
206256
if tr, ok := toolResultMap[tc.ID]; ok {
207257
options = append(options, messages.WithToolCallResult(tr))
208258
}
209-
if msg.FinishPart().Reason == message.FinishReasonCanceled {
259+
if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonCanceled {
210260
options = append(options, messages.WithToolCallCancelled())
211261
}
212-
uiMessages = append(uiMessages, messages.NewToolCallCmp(tc, options...))
262+
uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, options...))
213263
}
214264
}
215265
}

internal/tui/components/chat/messages/messages.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,16 @@ func (m *messageCmp) Init() tea.Cmd {
6565
}
6666

6767
func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
68-
u, cmd := m.anim.Update(msg)
69-
m.anim = u.(util.Model)
70-
return m, cmd
68+
switch msg := msg.(type) {
69+
case anim.ColorCycleMsg, anim.StepCharsMsg:
70+
m.spinning = m.shouldSpin()
71+
if m.spinning {
72+
u, cmd := m.anim.Update(msg)
73+
m.anim = u.(util.Model)
74+
return m, cmd
75+
}
76+
}
77+
return m, nil
7178
}
7279

7380
func (m *messageCmp) View() string {

internal/tui/components/chat/messages/renderer.go

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -524,32 +524,3 @@ func prettifyToolName(name string) string {
524524
return name
525525
}
526526
}
527-
528-
func toolAction(name string) string {
529-
switch name {
530-
case agent.AgentToolName:
531-
return "Preparing prompt..."
532-
case tools.BashToolName:
533-
return "Building command..."
534-
case tools.EditToolName:
535-
return "Preparing edit..."
536-
case tools.FetchToolName:
537-
return "Writing fetch..."
538-
case tools.GlobToolName:
539-
return "Finding files..."
540-
case tools.GrepToolName:
541-
return "Searching content..."
542-
case tools.LSToolName:
543-
return "Listing directory..."
544-
case tools.SourcegraphToolName:
545-
return "Searching code..."
546-
case tools.ViewToolName:
547-
return "Reading file..."
548-
case tools.WriteToolName:
549-
return "Preparing write..."
550-
case tools.PatchToolName:
551-
return "Preparing patch..."
552-
default:
553-
return "Working..."
554-
}
555-
}

0 commit comments

Comments
 (0)