Skip to content

Commit 2d68814

Browse files
committed
feat: better collapsed tool call visuals
1 parent a5da512 commit 2d68814

File tree

2 files changed

+70
-26
lines changed

2 files changed

+70
-26
lines changed

packages/tui/internal/components/chat/message.go

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -201,17 +201,20 @@ func renderContentBlock(content string, options ...renderingOption) string {
201201
return content
202202
}
203203

204-
func renderText(message client.MessageInfo, text string, author string) string {
205-
t := theme.CurrentTheme()
206-
width := layout.Current.Container.Width
207-
padding := 0
204+
func calculatePadding() int {
208205
if layout.Current.Viewport.Width < 80 {
209-
padding = 5
206+
return 5
210207
} else if layout.Current.Viewport.Width < 120 {
211-
padding = 15
208+
return 15
212209
} else {
213-
padding = 20
210+
return 20
214211
}
212+
}
213+
214+
func renderText(message client.MessageInfo, text string, author string) string {
215+
t := theme.CurrentTheme()
216+
width := layout.Current.Container.Width
217+
padding := calculatePadding()
215218

216219
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
217220
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
@@ -222,9 +225,11 @@ func renderText(message client.MessageInfo, text string, author string) string {
222225

223226
textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
224227
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
228+
if message.Role == client.Assistant {
229+
markdownWidth = width - padding - 4
230+
}
225231
content := toMarkdown(text, markdownWidth, t.BackgroundSubtle())
226232
content = strings.Join([]string{content, info}, "\n")
227-
// content = lipgloss.JoinVertical(align, content, info)
228233

229234
switch message.Role {
230235
case client.User:
@@ -246,6 +251,7 @@ func renderToolInvocation(
246251
result *string,
247252
metadata client.MessageInfo_Metadata_Tool_AdditionalProperties,
248253
showResult bool,
254+
isLast bool,
249255
) string {
250256
ignoredTools := []string{"opencode_todoread"}
251257
if slices.Contains(ignoredTools, toolCall.ToolName) {
@@ -333,15 +339,15 @@ func renderToolInvocation(
333339
switch toolCall.ToolName {
334340
case "opencode_read":
335341
toolArgs = renderArgs(&toolArgsMap, "filePath")
336-
title = fmt.Sprintf("Read: %s %s", toolArgs, elapsed)
342+
title = fmt.Sprintf("READ %s %s", toolArgs, elapsed)
337343
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
338344
filename := toolArgsMap["filePath"].(string)
339345
body = preview.(string)
340346
body = renderFile(filename, body, WithTruncate(6))
341347
}
342348
case "opencode_edit":
343349
if filename, ok := toolArgsMap["filePath"].(string); ok {
344-
title = fmt.Sprintf("Edit: %s %s", relative(filename), elapsed)
350+
title = fmt.Sprintf("EDIT %s %s", relative(filename), elapsed)
345351
if d, ok := metadata.Get("diff"); ok {
346352
patch := d.(string)
347353
var formattedDiff string
@@ -382,14 +388,14 @@ func renderToolInvocation(
382388
}
383389
case "opencode_write":
384390
if filename, ok := toolArgsMap["filePath"].(string); ok {
385-
title = fmt.Sprintf("Write: %s %s", relative(filename), elapsed)
391+
title = fmt.Sprintf("WRITE %s %s", relative(filename), elapsed)
386392
if content, ok := toolArgsMap["content"].(string); ok {
387393
body = renderFile(filename, content)
388394
}
389395
}
390396
case "opencode_bash":
391397
if description, ok := toolArgsMap["description"].(string); ok {
392-
title = fmt.Sprintf("Shell: %s %s", description, elapsed)
398+
title = fmt.Sprintf("SHELL %s %s", description, elapsed)
393399
}
394400
if stdout, ok := metadata.Get("stdout"); ok {
395401
command := toolArgsMap["command"].(string)
@@ -400,7 +406,7 @@ func renderToolInvocation(
400406
}
401407
case "opencode_webfetch":
402408
toolArgs = renderArgs(&toolArgsMap, "url")
403-
title = fmt.Sprintf("Fetching: %s %s", toolArgs, elapsed)
409+
title = fmt.Sprintf("FETCH %s %s", toolArgs, elapsed)
404410
if format, ok := toolArgsMap["format"].(string); ok {
405411
body = *result
406412
body = truncateHeight(body, 10)
@@ -410,7 +416,7 @@ func renderToolInvocation(
410416
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
411417
}
412418
case "opencode_todowrite":
413-
title = fmt.Sprintf("Planning %s", elapsed)
419+
title = fmt.Sprintf("PLAN %s", elapsed)
414420

415421
if to, ok := metadata.Get("todos"); ok && finished {
416422
todos := to.([]any)
@@ -431,12 +437,27 @@ func renderToolInvocation(
431437
}
432438
default:
433439
toolName := renderToolName(toolCall.ToolName)
434-
title = fmt.Sprintf("%s: %s %s", toolName, toolArgs, elapsed)
440+
title = fmt.Sprintf("%s %s %s", toolName, toolArgs, elapsed)
435441
body = *result
436442
body = truncateHeight(body, 10)
437443
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
438444
}
439445

446+
if !showResult {
447+
padding := calculatePadding()
448+
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundSubtle())
449+
paddingBottom := 0
450+
if isLast {
451+
paddingBottom = 1
452+
}
453+
return renderContentBlock(style.Render(title),
454+
WithAlign(lipgloss.Left),
455+
WithBorderColor(t.Accent()),
456+
WithPaddingTop(0),
457+
WithPaddingBottom(paddingBottom),
458+
)
459+
}
460+
440461
if body == "" && error == "" {
441462
body = *result
442463
body = truncateHeight(body, 10)
@@ -464,19 +485,17 @@ func renderToolName(name string) string {
464485
// case agent.AgentToolName:
465486
// return "Task"
466487
case "opencode_ls":
467-
return "List"
488+
return "LIST"
468489
case "opencode_webfetch":
469-
return "Fetch"
470-
case "opencode_todoread":
471-
return "Planning"
490+
return "FETCH"
472491
case "opencode_todowrite":
473-
return "Planning"
492+
return "PLAN"
474493
default:
475494
normalizedName := name
476495
if strings.HasPrefix(name, "opencode_") {
477496
normalizedName = strings.TrimPrefix(name, "opencode_")
478497
}
479-
return cases.Title(language.Und).String(normalizedName)
498+
return cases.Upper(language.Und).String(normalizedName)
480499
}
481500
}
482501

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

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package chat
22

33
import (
4+
"slices"
45
"strings"
56
"time"
67

@@ -144,6 +145,17 @@ func (m *messagesComponent) renderView() {
144145
for _, message := range m.app.Messages {
145146
var content string
146147
var cached bool
148+
lastToolIndex := 0
149+
lastToolIndices := []int{}
150+
for i, p := range message.Parts {
151+
part, _ := p.ValueByDiscriminator()
152+
switch part.(type) {
153+
case client.MessagePartText:
154+
lastToolIndices = append(lastToolIndices, lastToolIndex)
155+
case client.MessagePartToolInvocation:
156+
lastToolIndex = i
157+
}
158+
}
147159

148160
author := ""
149161
switch message.Role {
@@ -153,7 +165,7 @@ func (m *messagesComponent) renderView() {
153165
author = message.Metadata.Assistant.ModelID
154166
}
155167

156-
for _, p := range message.Parts {
168+
for i, p := range message.Parts {
157169
part, err := p.ValueByDiscriminator()
158170
if err != nil {
159171
continue //TODO: handle error?
@@ -180,6 +192,7 @@ func (m *messagesComponent) renderView() {
180192
previousBlockType = assistantTextBlock
181193
}
182194
case client.MessagePartToolInvocation:
195+
isLastToolInvocation := slices.Contains(lastToolIndices, i)
183196
toolInvocationPart := part.(client.MessagePartToolInvocation)
184197
toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall()
185198
metadata := client.MessageInfo_Metadata_Tool_AdditionalProperties{}
@@ -200,15 +213,27 @@ func (m *messagesComponent) renderView() {
200213
)
201214
content, cached = m.cache.Get(key)
202215
if !cached {
203-
content = renderToolInvocation(toolCall, result, metadata, m.showToolResults)
216+
content = renderToolInvocation(
217+
toolCall,
218+
result,
219+
metadata,
220+
m.showToolResults,
221+
isLastToolInvocation,
222+
)
204223
m.cache.Set(key, content)
205224
}
206225
} else {
207-
// if the tool call isn't finished, never cache
208-
content = renderToolInvocation(toolCall, result, metadata, m.showToolResults)
226+
// if the tool call isn't finished, don't cache
227+
content = renderToolInvocation(
228+
toolCall,
229+
result,
230+
metadata,
231+
m.showToolResults,
232+
isLastToolInvocation,
233+
)
209234
}
210235

211-
if previousBlockType != toolInvocationBlock {
236+
if previousBlockType != toolInvocationBlock && m.showToolResults {
212237
blocks = append(blocks, "")
213238
}
214239
blocks = append(blocks, content)

0 commit comments

Comments
 (0)