summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorTimo Clasen <[email protected]>2025-06-30 18:57:56 +0200
committerGitHub <[email protected]>2025-06-30 11:57:56 -0500
commitd090c08ef0940d974305adc29ea931e046626786 (patch)
treea32b147b65686baa149dafce8dc61fff3e943e67 /packages
parent68e82e4d94a0a10f420a78c60f277f55b9f2fdd5 (diff)
downloadopencode-d090c08ef0940d974305adc29ea931e046626786.tar.gz
opencode-d090c08ef0940d974305adc29ea931e046626786.zip
feat: update user and agent messages width and alignment (#515)
Co-authored-by: adamdottv <[email protected]>
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/provider/provider.ts9
-rw-r--r--packages/tui/internal/app/app.go2
-rw-r--r--packages/tui/internal/components/chat/message.go668
-rw-r--r--packages/tui/internal/components/chat/messages.go224
-rw-r--r--packages/tui/internal/util/util.go2
5 files changed, 422 insertions, 483 deletions
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index cae40889a..f65eeae56 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -11,8 +11,6 @@ import { WebFetchTool } from "../tool/webfetch"
import { GlobTool } from "../tool/glob"
import { GrepTool } from "../tool/grep"
import { ListTool } from "../tool/ls"
-import { LspDiagnosticTool } from "../tool/lsp-diagnostics"
-import { LspHoverTool } from "../tool/lsp-hover"
import { PatchTool } from "../tool/patch"
import { ReadTool } from "../tool/read"
import type { Tool } from "../tool/tool"
@@ -23,6 +21,7 @@ import { AuthCopilot } from "../auth/copilot"
import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
import { Auth } from "../auth"
+// import { TaskTool } from "../tool/task"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -447,16 +446,16 @@ export namespace Provider {
GlobTool,
GrepTool,
ListTool,
- LspDiagnosticTool,
- LspHoverTool,
+ // LspDiagnosticTool,
+ // LspHoverTool,
PatchTool,
ReadTool,
EditTool,
// MultiEditTool,
WriteTool,
TodoWriteTool,
- // TaskTool,
TodoReadTool,
+ // TaskTool,
]
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index 3bd48f02a..2369b196a 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -21,6 +21,7 @@ import (
)
var RootPath string
+var CwdPath string
type App struct {
Info opencode.App
@@ -61,6 +62,7 @@ func New(
httpClient *opencode.Client,
) (*App, error) {
RootPath = appInfo.Path.Root
+ CwdPath = appInfo.Path.Cwd
configInfo, err := httpClient.Config.Get(ctx)
if err != nil {
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index cf4662d53..cc7dabf19 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -24,7 +24,7 @@ import (
)
func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
- r := styles.GetMarkdownRenderer(width, backgroundColor)
+ r := styles.GetMarkdownRenderer(width-7, backgroundColor)
content = strings.ReplaceAll(content, app.RootPath+"/", "")
rendered, _ := r.Render(content)
lines := strings.Split(rendered, "\n")
@@ -50,9 +50,8 @@ func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor)
}
type blockRenderer struct {
- align *lipgloss.Position
+ border bool
borderColor *compat.AdaptiveColor
- fullWidth bool
paddingTop int
paddingBottom int
paddingLeft int
@@ -63,15 +62,9 @@ type blockRenderer struct {
type renderingOption func(*blockRenderer)
-func WithFullWidth() renderingOption {
+func WithNoBorder() renderingOption {
return func(c *blockRenderer) {
- c.fullWidth = true
- }
-}
-
-func WithAlign(align lipgloss.Position) renderingOption {
- return func(c *blockRenderer) {
- c.align = &align
+ c.border = false
}
}
@@ -93,6 +86,15 @@ func WithMarginBottom(padding int) renderingOption {
}
}
+func WithPadding(padding int) renderingOption {
+ return func(c *blockRenderer) {
+ c.paddingTop = padding
+ c.paddingBottom = padding
+ c.paddingLeft = padding
+ c.paddingRight = padding
+ }
+}
+
func WithPaddingLeft(padding int) renderingOption {
return func(c *blockRenderer) {
c.paddingLeft = padding
@@ -117,10 +119,15 @@ func WithPaddingBottom(padding int) renderingOption {
}
}
-func renderContentBlock(content string, options ...renderingOption) string {
+func renderContentBlock(
+ content string,
+ width int,
+ align lipgloss.Position,
+ options ...renderingOption,
+) string {
t := theme.CurrentTheme()
renderer := &blockRenderer{
- fullWidth: false,
+ border: true,
paddingTop: 1,
paddingBottom: 1,
paddingLeft: 2,
@@ -130,59 +137,42 @@ func renderContentBlock(content string, options ...renderingOption) string {
option(renderer)
}
- style := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).
- // MarginTop(renderer.marginTop).
- // MarginBottom(renderer.marginBottom).
- PaddingTop(renderer.paddingTop).
- PaddingBottom(renderer.paddingBottom).
- PaddingLeft(renderer.paddingLeft).
- PaddingRight(renderer.paddingRight).
- BorderStyle(lipgloss.ThickBorder())
-
- align := lipgloss.Left
- if renderer.align != nil {
- align = *renderer.align
- }
-
borderColor := t.BackgroundPanel()
if renderer.borderColor != nil {
borderColor = *renderer.borderColor
}
- switch align {
- case lipgloss.Left:
+ style := styles.NewStyle().
+ Foreground(t.TextMuted()).
+ Background(t.BackgroundPanel()).
+ Width(width).
+ PaddingTop(renderer.paddingTop).
+ PaddingBottom(renderer.paddingBottom).
+ PaddingLeft(renderer.paddingLeft).
+ PaddingRight(renderer.paddingRight).
+ AlignHorizontal(lipgloss.Left)
+
+ if renderer.border {
style = style.
+ BorderStyle(lipgloss.ThickBorder()).
BorderLeft(true).
BorderRight(true).
- AlignHorizontal(align).
BorderLeftForeground(borderColor).
BorderLeftBackground(t.Background()).
BorderRightForeground(t.BackgroundPanel()).
BorderRightBackground(t.Background())
- case lipgloss.Right:
- style = style.
- BorderRight(true).
- BorderLeft(true).
- AlignHorizontal(align).
- BorderRightForeground(borderColor).
- BorderRightBackground(t.Background()).
- BorderLeftForeground(t.BackgroundPanel()).
- BorderLeftBackground(t.Background())
}
- if renderer.fullWidth {
- style = style.Width(layout.Current.Container.Width)
- }
content = style.Render(content)
content = lipgloss.PlaceHorizontal(
- layout.Current.Container.Width,
- align,
+ width,
+ lipgloss.Left,
content,
styles.WhitespaceStyle(t.Background()),
)
content = lipgloss.PlaceHorizontal(
layout.Current.Viewport.Width,
- lipgloss.Center,
+ align,
content,
styles.WhitespaceStyle(t.Background()),
)
@@ -196,24 +186,19 @@ func renderContentBlock(content string, options ...renderingOption) string {
content = content + "\n"
}
}
-
return content
}
-func calculatePadding() int {
- if layout.Current.Viewport.Width < 80 {
- return 5
- } else if layout.Current.Viewport.Width < 120 {
- return 15
- } else {
- return 20
- }
-}
-
-func renderText(message opencode.Message, text string, author string) string {
+func renderText(
+ message opencode.Message,
+ text string,
+ author string,
+ showToolDetails bool,
+ width int,
+ align lipgloss.Position,
+ toolCalls ...opencode.ToolInvocationPart,
+) string {
t := theme.CurrentTheme()
- width := layout.Current.Container.Width
- padding := calculatePadding()
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
@@ -222,175 +207,120 @@ func renderText(message opencode.Message, text string, author string) string {
}
info := fmt.Sprintf("%s (%s)", author, timestamp)
- textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
- markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
- if message.Role == opencode.MessageRoleAssistant {
- markdownWidth = width - padding - 4 - 3
- }
- minWidth := max(markdownWidth, (width-4)/2)
messageStyle := styles.NewStyle().
- Width(minWidth).
Background(t.BackgroundPanel()).
Foreground(t.Text())
- if textWidth < minWidth {
- messageStyle = messageStyle.AlignHorizontal(lipgloss.Right)
+ if message.Role == opencode.MessageRoleUser {
+ messageStyle = messageStyle.Width(width - 6)
}
+
content := messageStyle.Render(text)
if message.Role == opencode.MessageRoleAssistant {
- content = toMarkdown(text, markdownWidth, t.BackgroundPanel())
+ content = toMarkdown(text, width, t.BackgroundPanel())
+ }
+
+ if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 {
+ content = content + "\n\n"
+ for _, toolCall := range toolCalls {
+ title := renderToolTitle(toolCall, message.Metadata, width)
+ metadata := opencode.MessageMetadataTool{}
+ if _, ok := message.Metadata.Tool[toolCall.ToolInvocation.ToolCallID]; ok {
+ metadata = message.Metadata.Tool[toolCall.ToolInvocation.ToolCallID]
+ }
+ style := styles.NewStyle()
+ if _, ok := metadata.ExtraFields["error"]; ok {
+ style = style.Foreground(t.Error())
+ }
+ title = style.Render(title)
+ title = "∟ " + title + "\n"
+ content = content + title
+ }
}
+
content = strings.Join([]string{content, info}, "\n")
switch message.Role {
case opencode.MessageRoleUser:
- return renderContentBlock(content,
- WithAlign(lipgloss.Right),
+ return renderContentBlock(
+ content,
+ width,
+ align,
WithBorderColor(t.Secondary()),
)
case opencode.MessageRoleAssistant:
- return renderContentBlock(content,
- WithAlign(lipgloss.Left),
+ return renderContentBlock(
+ content,
+ width,
+ align,
WithBorderColor(t.Accent()),
)
}
return ""
}
-func renderToolInvocation(
+func renderToolDetails(
toolCall opencode.ToolInvocationPart,
- result *string,
- metadata opencode.MessageMetadataTool,
- showDetails bool,
- isLast bool,
- contentOnly bool,
messageMetadata opencode.MessageMetadata,
+ width int,
+ align lipgloss.Position,
) string {
ignoredTools := []string{"todoread"}
if slices.Contains(ignoredTools, toolCall.ToolInvocation.ToolName) {
return ""
}
- outerWidth := layout.Current.Container.Width
- innerWidth := outerWidth - 6
- paddingTop := 0
- paddingBottom := 0
- if showDetails {
- paddingTop = 1
- if result == nil || *result == "" {
- paddingBottom = 1
- }
+ toolCallID := toolCall.ToolInvocation.ToolCallID
+ metadata := opencode.MessageMetadataTool{}
+ if _, ok := messageMetadata.Tool[toolCallID]; ok {
+ metadata = messageMetadata.Tool[toolCallID]
}
- t := theme.CurrentTheme()
- style := styles.NewStyle().
- Foreground(t.TextMuted()).
- Background(t.BackgroundPanel()).
- Width(outerWidth).
- PaddingTop(paddingTop).
- PaddingBottom(paddingBottom).
- PaddingLeft(2).
- PaddingRight(2).
- BorderLeft(true).
- BorderRight(true).
- BorderBackground(t.Background()).
- BorderForeground(t.BackgroundPanel()).
- BorderStyle(lipgloss.ThickBorder())
+ var result *string
+ if toolCall.ToolInvocation.Result != "" {
+ result = &toolCall.ToolInvocation.Result
+ }
if toolCall.ToolInvocation.State == "partial-call" {
- title := renderToolAction(toolCall.ToolInvocation.ToolName)
- if !showDetails {
- title = "∟ " + title
- padding := calculatePadding()
- style := styles.NewStyle().
- Background(t.BackgroundPanel()).
- Width(outerWidth - padding - 4 - 3)
- return renderContentBlock(style.Render(title),
- WithAlign(lipgloss.Left),
- WithBorderColor(t.Accent()),
- WithPaddingTop(0),
- WithPaddingBottom(1),
- )
- }
-
- style = style.Foreground(t.TextMuted())
- return style.Render(title)
+ title := renderToolTitle(toolCall, messageMetadata, width)
+ return renderContentBlock(title, width, align)
}
- toolArgs := ""
toolArgsMap := make(map[string]any)
if toolCall.ToolInvocation.Args != nil {
value := toolCall.ToolInvocation.Args
if m, ok := value.(map[string]any); ok {
toolArgsMap = m
-
keys := make([]string, 0, len(toolArgsMap))
for key := range toolArgsMap {
keys = append(keys, key)
}
slices.Sort(keys)
- firstKey := ""
- if len(keys) > 0 {
- firstKey = keys[0]
- }
-
- toolArgs = renderArgs(&toolArgsMap, firstKey)
}
}
body := ""
- error := ""
finished := result != nil && *result != ""
+ t := theme.CurrentTheme()
- er := messageMetadata.Error.AsUnion()
- switch er.(type) {
- case nil:
- default:
- clientError := er.(opencode.UnknownError)
- error = clientError.Data.Message
- }
-
- if error != "" {
- style = style.BorderLeftForeground(t.Error())
- error = styles.NewStyle().
- Foreground(t.Error()).
- Background(t.BackgroundPanel()).
- Render(error)
- error = renderContentBlock(
- error,
- WithFullWidth(),
- WithBorderColor(t.Error()),
- WithMarginBottom(1),
- )
- }
-
- title := ""
switch toolCall.ToolInvocation.ToolName {
case "read":
- toolArgs = renderArgs(&toolArgsMap, "filePath")
- title = fmt.Sprintf("READ %s", toolArgs)
preview := metadata.ExtraFields["preview"]
if preview != nil && toolArgsMap["filePath"] != nil {
filename := toolArgsMap["filePath"].(string)
body = preview.(string)
- body = renderFile(filename, body, WithTruncate(6))
+ body = renderFile(filename, body, width, WithTruncate(6))
}
case "edit":
if filename, ok := toolArgsMap["filePath"].(string); ok {
- title = fmt.Sprintf("EDIT %s", relative(filename))
diffField := metadata.ExtraFields["diff"]
if diffField != nil {
patch := diffField.(string)
var formattedDiff string
- if layout.Current.Viewport.Width < 80 {
- formattedDiff, _ = diff.FormatUnifiedDiff(
- filename,
- patch,
- diff.WithWidth(layout.Current.Container.Width-2),
- )
- } else {
- diffWidth := min(layout.Current.Viewport.Width-2, 120)
- formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
- }
+ formattedDiff, _ = diff.FormatUnifiedDiff(
+ filename,
+ patch,
+ diff.WithWidth(width-2),
+ )
formattedDiff = strings.TrimSpace(formattedDiff)
formattedDiff = styles.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
@@ -400,67 +330,51 @@ func renderToolInvocation(
BorderRight(true).
Render(formattedDiff)
- if showDetails {
- style = style.Width(lipgloss.Width(formattedDiff))
- title += "\n"
- }
-
body = strings.TrimSpace(formattedDiff)
- body = lipgloss.Place(
- layout.Current.Viewport.Width,
- lipgloss.Height(body)+1,
- lipgloss.Center,
- lipgloss.Top,
+ body = renderContentBlock(
body,
- styles.WhitespaceStyle(t.Background()),
+ width,
+ align,
+ WithNoBorder(),
+ WithPadding(0),
)
- // Add diagnostics at the bottom if they exist
- if diagnostics := renderDiagnostics(messageMetadata, filename); diagnostics != "" {
- body += "\n" + renderContentBlock(diagnostics, WithFullWidth(), WithBorderColor(t.Error()))
+ if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
+ body += "\n" + renderContentBlock(diagnostics, width, align)
}
+
+ title := renderToolTitle(toolCall, messageMetadata, width)
+ title = renderContentBlock(title, width, align)
+ content := title + "\n" + body
+ return content
}
}
case "write":
if filename, ok := toolArgsMap["filePath"].(string); ok {
- title = fmt.Sprintf("WRITE %s", relative(filename))
if content, ok := toolArgsMap["content"].(string); ok {
- body = renderFile(filename, content)
-
- // Add diagnostics at the bottom if they exist
- if diagnostics := renderDiagnostics(messageMetadata, filename); diagnostics != "" {
- body += "\n" + renderContentBlock(diagnostics, WithFullWidth(), WithBorderColor(t.Error()))
+ body = renderFile(filename, content, width)
+ if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
+ body += "\n\n" + diagnostics
}
}
}
case "bash":
- if description, ok := toolArgsMap["description"].(string); ok {
- title = fmt.Sprintf("SHELL %s", description)
- }
stdout := metadata.JSON.ExtraFields["stdout"]
if !stdout.IsNull() {
command := toolArgsMap["command"].(string)
stdout := stdout.Raw()
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
- body = toMarkdown(body, innerWidth, t.BackgroundPanel())
- body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
+ body = toMarkdown(body, width, t.BackgroundPanel())
}
case "webfetch":
- toolArgs = renderArgs(&toolArgsMap, "url")
- title = fmt.Sprintf("FETCH %s", toolArgs)
- if format, ok := toolArgsMap["format"].(string); ok {
- if result != nil {
- body = *result
- body = truncateHeight(body, 10)
- if format == "html" || format == "markdown" {
- body = toMarkdown(body, innerWidth, t.BackgroundPanel())
- }
- body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
+ if format, ok := toolArgsMap["format"].(string); ok && result != nil {
+ body = *result
+ body = truncateHeight(body, 10)
+ if format == "html" || format == "markdown" {
+ body = toMarkdown(body, width, t.BackgroundPanel())
}
}
case "todowrite":
- title = fmt.Sprintf("PLAN")
-
todos := metadata.JSON.ExtraFields["todos"]
if !todos.IsNull() && finished {
strTodos := todos.Raw()
@@ -476,120 +390,168 @@ func renderToolInvocation(
body += fmt.Sprintf("- [ ] %s\n", content)
}
}
- body = toMarkdown(body, innerWidth, t.BackgroundPanel())
- body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
+ body = toMarkdown(body, width, t.BackgroundPanel())
}
case "task":
- if description, ok := toolArgsMap["description"].(string); ok {
- title = fmt.Sprintf("TASK %s", description)
- summary := metadata.JSON.ExtraFields["summary"]
- if !summary.IsNull() {
- strValue := summary.Raw()
- toolcalls := gjson.Parse(strValue).Array()
-
- steps := []string{}
- for _, toolcall := range toolcalls {
- call := toolcall.Value().(map[string]any)
- if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok {
- data, _ := json.Marshal(toolInvocation)
- var toolCall opencode.ToolInvocationPart
- _ = json.Unmarshal(data, &toolCall)
-
- if metadata, ok := call["metadata"].(map[string]any); ok {
- data, _ = json.Marshal(metadata)
- var toolMetadata opencode.MessageMetadataTool
- _ = json.Unmarshal(data, &toolMetadata)
-
- step := renderToolInvocation(
- toolCall,
- nil,
- toolMetadata,
- false,
- false,
- true,
- messageMetadata,
- )
- steps = append(steps, step)
- }
+ summary := metadata.JSON.ExtraFields["summary"]
+ if !summary.IsNull() {
+ strValue := summary.Raw()
+ toolcalls := gjson.Parse(strValue).Array()
+
+ steps := []string{}
+ for _, toolcall := range toolcalls {
+ call := toolcall.Value().(map[string]any)
+ if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok {
+ data, _ := json.Marshal(toolInvocation)
+ var toolCall opencode.ToolInvocationPart
+ _ = json.Unmarshal(data, &toolCall)
+
+ if metadata, ok := call["metadata"].(map[string]any); ok {
+ data, _ = json.Marshal(metadata)
+ var toolMetadata opencode.MessageMetadataTool
+ _ = json.Unmarshal(data, &toolMetadata)
+
+ step := renderToolTitle(toolCall, messageMetadata, width)
+ step = "∟ " + step
+ steps = append(steps, step)
}
}
- body = strings.Join(steps, "\n")
- body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
+ body = strings.Join(steps, "\n")
}
-
default:
- toolName := renderToolName(toolCall.ToolInvocation.ToolName)
- title = fmt.Sprintf("%s %s", toolName, toolArgs)
if result == nil {
empty := ""
result = &empty
}
body = *result
body = truncateHeight(body, 10)
- body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
- if contentOnly {
- title = "∟ " + title
- return title
+ error := ""
+ if err, ok := metadata.ExtraFields["error"].(bool); ok && err {
+ if message, ok := metadata.ExtraFields["message"].(string); ok {
+ error = message
+ }
}
- if !showDetails {
- title = "∟ " + title
- padding := calculatePadding()
- style := styles.NewStyle().Background(t.BackgroundPanel()).Width(outerWidth - padding - 4 - 3)
- paddingBottom := 0
- if isLast {
- paddingBottom = 1
- }
- return renderContentBlock(style.Render(title),
- WithAlign(lipgloss.Left),
- WithBorderColor(t.Accent()),
- WithPaddingTop(0),
- WithPaddingBottom(paddingBottom),
- )
+ if error != "" {
+ body = styles.NewStyle().
+ Foreground(t.Error()).
+ Background(t.BackgroundPanel()).
+ Render(error)
}
if body == "" && error == "" && result != nil {
body = *result
body = truncateHeight(body, 10)
- body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
- content := style.Render(title)
- content = lipgloss.PlaceHorizontal(
- layout.Current.Viewport.Width,
- lipgloss.Center,
- content,
- styles.WhitespaceStyle(t.Background()),
- )
- if showDetails && body != "" && error == "" {
- content += "\n" + body
- }
- if showDetails && error != "" {
- content += "\n" + error
- }
- return content
+ title := renderToolTitle(toolCall, messageMetadata, width)
+ content := title + "\n\n" + body
+ return renderContentBlock(content, width, align)
}
func renderToolName(name string) string {
switch name {
- case "list":
- return "LIST"
case "webfetch":
- return "FETCH"
- case "todowrite":
- return "PLAN"
+ return "Fetch"
+ case "todowrite", "todoread":
+ return "Plan"
default:
normalizedName := name
if strings.HasPrefix(name, "opencode_") {
normalizedName = strings.TrimPrefix(name, "opencode_")
}
- return cases.Upper(language.Und).String(normalizedName)
+ return cases.Title(language.Und).String(normalizedName)
}
}
+func renderToolTitle(
+ toolCall opencode.ToolInvocationPart,
+ messageMetadata opencode.MessageMetadata,
+ width int,
+) string {
+ // TODO: handle truncate to width
+
+ if toolCall.ToolInvocation.State == "partial-call" {
+ return renderToolAction(toolCall.ToolInvocation.ToolName)
+ }
+
+ toolArgs := ""
+ toolArgsMap := make(map[string]any)
+ if toolCall.ToolInvocation.Args != nil {
+ value := toolCall.ToolInvocation.Args
+ if m, ok := value.(map[string]any); ok {
+ toolArgsMap = m
+
+ keys := make([]string, 0, len(toolArgsMap))
+ for key := range toolArgsMap {
+ keys = append(keys, key)
+ }
+ slices.Sort(keys)
+ firstKey := ""
+ if len(keys) > 0 {
+ firstKey = keys[0]
+ }
+
+ toolArgs = renderArgs(&toolArgsMap, firstKey)
+ }
+ }
+
+ title := renderToolName(toolCall.ToolInvocation.ToolName)
+ switch toolCall.ToolInvocation.ToolName {
+ case "read":
+ toolArgs = renderArgs(&toolArgsMap, "filePath")
+ title = fmt.Sprintf("%s %s", title, toolArgs)
+ case "edit", "write":
+ if filename, ok := toolArgsMap["filePath"].(string); ok {
+ title = fmt.Sprintf("%s %s", title, relative(filename))
+ }
+ case "bash", "task":
+ if description, ok := toolArgsMap["description"].(string); ok {
+ title = fmt.Sprintf("%s %s", title, description)
+ }
+ case "webfetch":
+ toolArgs = renderArgs(&toolArgsMap, "url")
+ title = fmt.Sprintf("%s %s", title, toolArgs)
+ case "todowrite", "todoread":
+ // title is just the tool name
+ default:
+ toolName := renderToolName(toolCall.ToolInvocation.ToolName)
+ title = fmt.Sprintf("%s %s", toolName, toolArgs)
+ }
+ return title
+}
+
+func renderToolAction(name string) string {
+ switch name {
+ case "task":
+ return "Searching..."
+ case "bash":
+ return "Writing command..."
+ case "edit":
+ return "Preparing edit..."
+ case "webfetch":
+ return "Fetching from the web..."
+ case "glob":
+ return "Finding files..."
+ case "grep":
+ return "Searching content..."
+ case "list":
+ return "Listing directory..."
+ case "read":
+ return "Reading file..."
+ case "write":
+ return "Preparing write..."
+ case "todowrite", "todoread":
+ return "Planning..."
+ case "patch":
+ return "Preparing patch..."
+ }
+ return "Working..."
+}
+
type fileRenderer struct {
filename string
content string
@@ -604,7 +566,11 @@ func WithTruncate(height int) fileRenderingOption {
}
}
-func renderFile(filename string, content string, options ...fileRenderingOption) string {
+func renderFile(
+ filename string,
+ content string,
+ width int,
+ options ...fileRenderingOption) string {
t := theme.CurrentTheme()
renderer := &fileRenderer{
filename: filename,
@@ -622,44 +588,12 @@ func renderFile(filename string, content string, options ...fileRenderingOption)
}
content = strings.Join(lines, "\n")
- width := layout.Current.Container.Width - 8
if renderer.height > 0 {
content = truncateHeight(content, renderer.height)
}
content = fmt.Sprintf("```%s\n%s\n```", extension(renderer.filename), content)
content = toMarkdown(content, width, t.BackgroundPanel())
-
- return renderContentBlock(content, WithFullWidth(), WithMarginBottom(1))
-}
-
-func renderToolAction(name string) string {
- switch name {
- case "task":
- return "Searching..."
- case "bash":
- return "Building command..."
- case "edit":
- return "Preparing edit..."
- case "webfetch":
- return "Fetching from the web..."
- case "glob":
- return "Finding files..."
- case "grep":
- return "Searching content..."
- case "list":
- return "Listing directory..."
- case "read":
- return "Reading file..."
- case "write":
- return "Preparing write..."
- case "todowrite", "todoread":
- return "Planning..."
- case "patch":
- return "Preparing patch..."
- case "batch":
- return "Running batch operations..."
- }
- return "Working..."
+ return content
}
func renderArgs(args *map[string]any, titleKey string) string {
@@ -704,6 +638,7 @@ func truncateHeight(content string, height int) string {
}
func relative(path string) string {
+ path = strings.TrimPrefix(path, app.CwdPath+"/")
return strings.TrimPrefix(path, app.RootPath+"/")
}
@@ -730,64 +665,59 @@ type Diagnostic struct {
}
// renderDiagnostics formats LSP diagnostics for display in the TUI
-func renderDiagnostics(metadata opencode.MessageMetadata, filePath string) string {
- diagnosticsData := metadata.JSON.ExtraFields["diagnostics"]
- if diagnosticsData.IsNull() {
- return ""
- }
-
- // diagnosticsData should be a map[string][]Diagnostic
- strDiagnosticsData := diagnosticsData.Raw()
- diagnosticsMap := gjson.Parse(strDiagnosticsData).Value().(map[string]any)
- fileDiagnostics, ok := diagnosticsMap[filePath]
- if !ok {
- return ""
- }
-
- diagnosticsList, ok := fileDiagnostics.([]any)
- if !ok {
- return ""
- }
-
- var errorDiagnostics []string
- for _, diagInterface := range diagnosticsList {
- diagMap, ok := diagInterface.(map[string]any)
- if !ok {
- continue
- }
-
- // Parse the diagnostic
- var diag Diagnostic
- diagBytes, err := json.Marshal(diagMap)
- if err != nil {
- continue
- }
- if err := json.Unmarshal(diagBytes, &diag); err != nil {
- continue
- }
-
- // Only show error diagnostics (severity === 1)
- if diag.Severity != 1 {
- continue
+func renderDiagnostics(metadata opencode.MessageMetadataTool, filePath string) string {
+ if diagnosticsData, ok := metadata.ExtraFields["diagnostics"].(map[string]any); ok {
+ if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok {
+ var errorDiagnostics []string
+ for _, diagInterface := range fileDiagnostics {
+ diagMap, ok := diagInterface.(map[string]any)
+ if !ok {
+ continue
+ }
+ // Parse the diagnostic
+ var diag Diagnostic
+ diagBytes, err := json.Marshal(diagMap)
+ if err != nil {
+ continue
+ }
+ if err := json.Unmarshal(diagBytes, &diag); err != nil {
+ continue
+ }
+ // Only show error diagnostics (severity === 1)
+ if diag.Severity != 1 {
+ continue
+ }
+ line := diag.Range.Start.Line + 1 // 1-based
+ column := diag.Range.Start.Character + 1 // 1-based
+ errorDiagnostics = append(errorDiagnostics, fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message))
+ }
+ if len(errorDiagnostics) == 0 {
+ return ""
+ }
+ t := theme.CurrentTheme()
+ var result strings.Builder
+ for _, diagnostic := range errorDiagnostics {
+ if result.Len() > 0 {
+ result.WriteString("\n")
+ }
+ result.WriteString(styles.NewStyle().Foreground(t.Error()).Render(diagnostic))
+ }
+ return result.String()
}
-
- line := diag.Range.Start.Line + 1 // 1-based
- column := diag.Range.Start.Character + 1 // 1-based
- errorDiagnostics = append(errorDiagnostics, fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message))
- }
-
- if len(errorDiagnostics) == 0 {
- return ""
}
+ return ""
- t := theme.CurrentTheme()
- var result strings.Builder
- for _, diagnostic := range errorDiagnostics {
- if result.Len() > 0 {
- result.WriteString("\n")
- }
- result.WriteString(styles.NewStyle().Foreground(t.Error()).Render(diagnostic))
- }
+ // diagnosticsData should be a map[string][]Diagnostic
+ // strDiagnosticsData := diagnosticsData.Raw()
+ // diagnosticsMap := gjson.Parse(strDiagnosticsData).Value().(map[string]any)
+ // fileDiagnostics, ok := diagnosticsMap[filePath]
+ // if !ok {
+ // return ""
+ // }
+
+ // diagnosticsList, ok := fileDiagnostics.([]any)
+ // if !ok {
+ // return ""
+ // }
- return result.String()
}
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index 356eba9f6..0a1aaa8f8 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -1,7 +1,6 @@
package chat
import (
- "slices"
"strings"
"time"
@@ -107,16 +106,6 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
-type blockType int
-
-const (
- none blockType = iota
- userTextBlock
- assistantTextBlock
- toolInvocationBlock
- errorBlock
-)
-
func (m *messagesComponent) renderView() {
if m.width == 0 {
return
@@ -127,128 +116,147 @@ func (m *messagesComponent) renderView() {
t := theme.CurrentTheme()
blocks := make([]string, 0)
- previousBlockType := none
+
+ align := lipgloss.Center
+ width := layout.Current.Container.Width
for _, message := range m.app.Messages {
var content string
var cached bool
- lastToolIndex := 0
- lastToolIndices := []int{}
- for i, p := range message.Parts {
- switch p.Type {
- case opencode.MessagePartTypeText:
- lastToolIndices = append(lastToolIndices, lastToolIndex)
- case opencode.MessagePartTypeToolInvocation:
- lastToolIndex = i
- }
- }
- author := ""
switch message.Role {
case opencode.MessageRoleUser:
- author = m.app.Info.User
- case opencode.MessageRoleAssistant:
- author = message.Metadata.Assistant.ModelID
- }
-
- for i, p := range message.Parts {
- switch part := p.AsUnion().(type) {
- // case client.MessagePartStepStart:
- // messages = append(messages, "")
- case opencode.TextPart:
- key := m.cache.GenerateKey(message.ID, p.Text, layout.Current.Viewport.Width)
- content, cached = m.cache.Get(key)
- if !cached {
- content = renderText(message, p.Text, author)
- m.cache.Set(key, content)
- }
- if previousBlockType != none {
- blocks = append(blocks, "")
- }
- blocks = append(blocks, content)
- if message.Role == opencode.MessageRoleUser {
- previousBlockType = userTextBlock
- } else if message.Role == opencode.MessageRoleAssistant {
- previousBlockType = assistantTextBlock
- }
- case opencode.ToolInvocationPart:
- isLastToolInvocation := slices.Contains(lastToolIndices, i)
- metadata := opencode.MessageMetadataTool{}
-
- toolCallID := part.ToolInvocation.ToolCallID
- // var toolCallID string
- // var result *string
- // switch toolCall := part.ToolInvocation.AsUnion().(type) {
- // case opencode.ToolCall:
- // toolCallID = toolCall.ToolCallID
- // case opencode.ToolPartialCall:
- // toolCallID = toolCall.ToolCallID
- // case opencode.ToolResult:
- // toolCallID = toolCall.ToolCallID
- // result = &toolCall.Result
- // }
-
- if _, ok := message.Metadata.Tool[toolCallID]; ok {
- metadata = message.Metadata.Tool[toolCallID]
- }
-
- var result *string
- if part.ToolInvocation.Result != "" {
- result = &part.ToolInvocation.Result
- }
-
- if part.ToolInvocation.State == "result" {
- key := m.cache.GenerateKey(message.ID,
- part.ToolInvocation.ToolCallID,
- m.showToolDetails,
- layout.Current.Viewport.Width,
- )
+ for _, part := range message.Parts {
+ switch part := part.AsUnion().(type) {
+ case opencode.TextPart:
+ key := m.cache.GenerateKey(message.ID, part.Text, layout.Current.Viewport.Width)
content, cached = m.cache.Get(key)
if !cached {
- content = renderToolInvocation(
- part,
- result,
- metadata,
+ content = renderText(
+ message,
+ part.Text,
+ m.app.Info.User,
m.showToolDetails,
- isLastToolInvocation,
- false,
- message.Metadata,
+ width,
+ align,
)
m.cache.Set(key, content)
}
- } else {
- // if the tool call isn't finished, don't cache
- content = renderToolInvocation(
- part,
- result,
- metadata,
- m.showToolDetails,
- isLastToolInvocation,
- false,
- message.Metadata,
- )
+ if content != "" {
+ blocks = append(blocks, content)
+ }
}
+ }
+
+ case opencode.MessageRoleAssistant:
+ for i, p := range message.Parts {
+ switch part := p.AsUnion().(type) {
+ case opencode.TextPart:
+ finished := message.Metadata.Time.Completed > 0
+ remainingParts := message.Parts[i+1:]
+ toolCallParts := make([]opencode.ToolInvocationPart, 0)
+ for _, part := range remainingParts {
+ switch part := part.AsUnion().(type) {
+ case opencode.TextPart:
+ // we only want tool calls associated with the current text part.
+ // if we hit another text part, we're done.
+ break
+ case opencode.ToolInvocationPart:
+ toolCallParts = append(toolCallParts, part)
+ if part.ToolInvocation.State != "result" {
+ // i don't think there's a case where a tool call isn't in result state
+ // and the message time is 0, but just in case
+ finished = false
+ }
+ }
+ }
- if previousBlockType != toolInvocationBlock && m.showToolDetails {
- blocks = append(blocks, "")
+ if finished {
+ key := m.cache.GenerateKey(message.ID, p.Text, layout.Current.Viewport.Width, m.showToolDetails)
+ content, cached = m.cache.Get(key)
+ if !cached {
+ content = renderText(
+ message,
+ p.Text,
+ message.Metadata.Assistant.ModelID,
+ m.showToolDetails,
+ width,
+ align,
+ toolCallParts...,
+ )
+ m.cache.Set(key, content)
+ }
+ } else {
+ content = renderText(
+ message,
+ p.Text,
+ message.Metadata.Assistant.ModelID,
+ m.showToolDetails,
+ width,
+ align,
+ toolCallParts...,
+ )
+ }
+ if content != "" {
+ blocks = append(blocks, content)
+ }
+ case opencode.ToolInvocationPart:
+ if !m.showToolDetails {
+ continue
+ }
+
+ if part.ToolInvocation.State == "result" {
+ key := m.cache.GenerateKey(message.ID,
+ part.ToolInvocation.ToolCallID,
+ m.showToolDetails,
+ layout.Current.Viewport.Width,
+ )
+ content, cached = m.cache.Get(key)
+ if !cached {
+ content = renderToolDetails(
+ part,
+ message.Metadata,
+ width,
+ align,
+ )
+ m.cache.Set(key, content)
+ }
+ } else {
+ // if the tool call isn't finished, don't cache
+ content = renderToolDetails(
+ part,
+ message.Metadata,
+ width,
+ align,
+ )
+ }
+ if content != "" {
+ blocks = append(blocks, content)
+ }
}
- blocks = append(blocks, content)
- previousBlockType = toolInvocationBlock
}
+
}
error := ""
switch err := message.Metadata.Error.AsUnion().(type) {
case nil:
- default:
- clientError := err.(opencode.UnknownError)
- error = clientError.Data.Message
+ case opencode.MessageMetadataErrorMessageOutputLengthError:
+ error = "Message output length exceeded"
+ case opencode.ProviderAuthError:
+ error = err.Data.Message
+ case opencode.UnknownError:
+ error = err.Data.Message
}
if error != "" {
- error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
+ error = renderContentBlock(
+ error,
+ width,
+ align,
+ WithBorderColor(t.Error()),
+ )
blocks = append(blocks, error)
- previousBlockType = errorBlock
}
}
@@ -257,7 +265,7 @@ func (m *messagesComponent) renderView() {
centered = append(centered, lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
- block,
+ block+"\n",
styles.WhitespaceStyle(t.Background()),
))
}
diff --git a/packages/tui/internal/util/util.go b/packages/tui/internal/util/util.go
index da12cc5b3..c7898acb5 100644
--- a/packages/tui/internal/util/util.go
+++ b/packages/tui/internal/util/util.go
@@ -42,6 +42,6 @@ func Measure(tag string) func(...any) {
startTime := time.Now()
return func(tags ...any) {
args := append([]any{"timeTakenMs", time.Since(startTime).Milliseconds()}, tags...)
- slog.Info(tag, args...)
+ slog.Debug(tag, args...)
}
}