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

Commit 9a69a31

Browse files
committed
handle agent tool
1 parent 3eff8fa commit 9a69a31

6 files changed

Lines changed: 227 additions & 22 deletions

File tree

internal/tui/components/anim/anim.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ func cycleColors(id string) tea.Cmd {
7575
})
7676
}
7777

78+
type Animation interface {
79+
util.Model
80+
ID() string
81+
}
82+
7883
// anim is the model that manages the animation that displays while the
7984
// output is being generated.
8085
type anim struct {
@@ -88,7 +93,15 @@ type anim struct {
8893
id string
8994
}
9095

91-
func New(cyclingCharsSize uint, label string) util.Model {
96+
type animOption func(*anim)
97+
98+
func WithId(id string) animOption {
99+
return func(a *anim) {
100+
a.id = id
101+
}
102+
}
103+
104+
func New(cyclingCharsSize uint, label string, opts ...animOption) Animation {
92105
// #nosec G115
93106
n := min(int(cyclingCharsSize), maxCyclingChars)
94107

@@ -105,6 +118,10 @@ func New(cyclingCharsSize uint, label string) util.Model {
105118
id: id.String(),
106119
}
107120

121+
for _, opt := range opts {
122+
opt(&c)
123+
}
124+
108125
// If we're in truecolor mode (and there are enough cycling characters)
109126
// color the cycling characters with a gradient ramp.
110127
const minRampSize = 3
@@ -204,6 +221,10 @@ func (a anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
204221
}
205222
}
206223

224+
func (a anim) ID() string {
225+
return a.id
226+
}
227+
207228
func (a *anim) updateChars(chars *[]cyclingChar) {
208229
for i, c := range *chars {
209230
switch c.state(a.start) {

internal/tui/components/chat/list.go

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
tea "github.com/charmbracelet/bubbletea/v2"
99
"github.com/charmbracelet/lipgloss/v2"
1010
"github.com/opencode-ai/opencode/internal/app"
11-
"github.com/opencode-ai/opencode/internal/logging"
11+
"github.com/opencode-ai/opencode/internal/llm/agent"
1212
"github.com/opencode-ai/opencode/internal/message"
1313
"github.com/opencode-ai/opencode/internal/pubsub"
1414
"github.com/opencode-ai/opencode/internal/session"
@@ -106,26 +106,71 @@ func (m *messageListCmp) View() string {
106106
}
107107

108108
// handleChildSession handles messages from child sessions (agent tools).
109-
// TODO: update the agent tool message with the changes
110-
func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message]) {
111-
// Implementation pending
109+
func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message]) tea.Cmd {
110+
var cmds []tea.Cmd
111+
if len(event.Payload.ToolCalls()) == 0 {
112+
return nil
113+
}
114+
items := m.listCmp.Items()
115+
toolCallInx := NotFound
116+
var toolCall messages.ToolCallCmp
117+
for i := len(items) - 1; i >= 0; i-- {
118+
if msg, ok := items[i].(messages.ToolCallCmp); ok {
119+
if msg.GetToolCall().ID == event.Payload.SessionID {
120+
toolCallInx = i
121+
toolCall = msg
122+
}
123+
}
124+
}
125+
if toolCallInx == NotFound {
126+
return nil
127+
}
128+
nestedToolCalls := toolCall.GetNestedToolCalls()
129+
for _, tc := range event.Payload.ToolCalls() {
130+
found := false
131+
for existingInx, existingTC := range nestedToolCalls {
132+
if existingTC.GetToolCall().ID == tc.ID {
133+
nestedToolCalls[existingInx].SetToolCall(tc)
134+
found = true
135+
break
136+
}
137+
}
138+
if !found {
139+
nestedCall := messages.NewToolCallCmp(
140+
event.Payload.ID,
141+
tc,
142+
messages.WithToolCallNested(true),
143+
)
144+
cmds = append(cmds, nestedCall.Init())
145+
nestedToolCalls = append(
146+
nestedToolCalls,
147+
nestedCall,
148+
)
149+
}
150+
}
151+
toolCall.SetNestedToolCalls(nestedToolCalls)
152+
m.listCmp.UpdateItem(
153+
toolCallInx,
154+
toolCall,
155+
)
156+
return tea.Batch(cmds...)
112157
}
113158

114159
// handleMessageEvent processes different types of message events (created/updated).
115160
func (m *messageListCmp) handleMessageEvent(event pubsub.Event[message.Message]) tea.Cmd {
116161
switch event.Type {
117162
case pubsub.CreatedEvent:
118163
if event.Payload.SessionID != m.session.ID {
119-
m.handleChildSession(event)
120-
return nil
164+
return m.handleChildSession(event)
121165
}
122-
123166
if m.messageExists(event.Payload.ID) {
124167
return nil
125168
}
126-
127169
return m.handleNewMessage(event.Payload)
128170
case pubsub.UpdatedEvent:
171+
if event.Payload.SessionID != m.session.ID {
172+
return m.handleChildSession(event)
173+
}
129174
return m.handleUpdateAssistantMessage(event.Payload)
130175
}
131176
return nil
@@ -196,8 +241,6 @@ func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.C
196241
// Find existing assistant message and tool calls for this message
197242
assistantIndex, existingToolCalls := m.findAssistantMessageAndToolCalls(items, msg.ID)
198243

199-
logging.Info("Update Assistant Message", "msg", msg, "assistantMessageInx", assistantIndex, "toolCalls", existingToolCalls)
200-
201244
// Handle assistant message content
202245
if cmd := m.updateAssistantMessageContent(msg, assistantIndex); cmd != nil {
203246
cmds = append(cmds, cmd)
@@ -389,6 +432,19 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult
389432
for _, tc := range msg.ToolCalls() {
390433
options := m.buildToolCallOptions(tc, msg, toolResultMap)
391434
uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, options...))
435+
// If this tool call is the agent tool, fetch nested tool calls
436+
if tc.Name == agent.AgentToolName {
437+
nestedMessages, _ := m.app.Messages.List(context.Background(), tc.ID)
438+
nestedUIMessages := m.convertMessagesToUI(nestedMessages, make(map[string]message.ToolResult))
439+
nestedToolCalls := make([]messages.ToolCallCmp, 0, len(nestedUIMessages))
440+
for _, nestedMsg := range nestedUIMessages {
441+
if toolCall, ok := nestedMsg.(messages.ToolCallCmp); ok {
442+
toolCall.SetIsNested(true)
443+
nestedToolCalls = append(nestedToolCalls, toolCall)
444+
}
445+
}
446+
uiMessages[len(uiMessages)-1].(messages.ToolCallCmp).SetNestedToolCalls(nestedToolCalls)
447+
}
392448
}
393449

394450
return uiMessages

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88
"time"
99

10+
"github.com/charmbracelet/bubbles/v2/spinner"
1011
tea "github.com/charmbracelet/bubbletea/v2"
1112
"github.com/charmbracelet/lipgloss/v2"
1213
"github.com/opencode-ai/opencode/internal/llm/models"
@@ -80,7 +81,7 @@ func (m *messageCmp) Init() tea.Cmd {
8081
// Manages animation updates for spinning messages and stops animation when appropriate.
8182
func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8283
switch msg := msg.(type) {
83-
case anim.ColorCycleMsg, anim.StepCharsMsg:
84+
case anim.ColorCycleMsg, anim.StepCharsMsg, spinner.TickMsg:
8485
m.spinning = m.shouldSpin()
8586
if m.spinning {
8687
u, cmd := m.anim.Update(msg)

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

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,14 @@ func (pb *paramBuilder) build() []string {
9191

9292
// renderWithParams provides a common rendering pattern for tools with parameters
9393
func (br baseRenderer) renderWithParams(v *toolCallCmp, toolName string, args []string, contentRenderer func() string) string {
94-
header := makeHeader(toolName, v.textWidth(), args...)
94+
width := v.textWidth()
95+
if v.isNested {
96+
width -= 3 // Adjust for nested tool call indentation
97+
}
98+
header := makeHeader(toolName, width, args...)
99+
if v.isNested {
100+
return v.style().Render(header)
101+
}
95102
if res, done := earlyState(header, v); done {
96103
return res
97104
}
@@ -117,6 +124,7 @@ func init() {
117124
registry.register(tools.SourcegraphToolName, func() renderer { return sourcegraphRenderer{} })
118125
registry.register(tools.PatchToolName, func() renderer { return patchRenderer{} })
119126
registry.register(tools.DiagnosticsToolName, func() renderer { return diagnosticsRenderer{} })
127+
registry.register(agent.AgentToolName, func() renderer { return agentRenderer{} })
120128
}
121129

122130
// -----------------------------------------------------------------------------
@@ -467,6 +475,51 @@ func (dr diagnosticsRenderer) Render(v *toolCallCmp) string {
467475
})
468476
}
469477

478+
// -----------------------------------------------------------------------------
479+
// Task renderer
480+
// -----------------------------------------------------------------------------
481+
482+
// agentRenderer handles project-wide diagnostic information
483+
type agentRenderer struct {
484+
baseRenderer
485+
}
486+
487+
// Render displays agent task parameters and result content
488+
func (tr agentRenderer) Render(v *toolCallCmp) string {
489+
var params agent.AgentParams
490+
if err := tr.unmarshalParams(v.call.Input, &params); err != nil {
491+
return tr.renderError(v, "Invalid task parameters")
492+
}
493+
prompt := params.Prompt
494+
prompt = strings.ReplaceAll(prompt, "\n", " ")
495+
args := newParamBuilder().addMain(prompt).build()
496+
497+
header := makeHeader("Task", v.textWidth(), args...)
498+
parts := []string{header}
499+
for _, call := range v.nestedToolCalls {
500+
parts = append(parts, call.View())
501+
}
502+
503+
if v.result.ToolCallID == "" {
504+
v.spinning = true
505+
parts = append(parts, v.anim.View())
506+
} else {
507+
v.spinning = false
508+
}
509+
510+
header = lipgloss.JoinVertical(
511+
lipgloss.Left,
512+
parts...,
513+
)
514+
515+
if v.result.ToolCallID == "" {
516+
return header
517+
}
518+
519+
body := renderPlainContent(v, v.result.Content)
520+
return joinHeaderBody(header, body)
521+
}
522+
470523
// makeHeader builds "<Tool>: param (key=value)" and truncates as needed.
471524
func makeHeader(tool string, width int, params ...string) string {
472525
prefix := tool + ": "

0 commit comments

Comments
 (0)