feat: update user and agent messages width and alignment (#515)

Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
This commit is contained in:
Timo Clasen 2025-06-30 18:57:56 +02:00 committed by GitHub
parent 68e82e4d94
commit d090c08ef0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 510 additions and 571 deletions

View file

@ -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[]> = {

View file

@ -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 {

File diff suppressed because it is too large Load diff

View file

@ -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)
}
}
if previousBlockType != toolInvocationBlock && m.showToolDetails {
blocks = append(blocks, "")
}
blocks = append(blocks, content)
previousBlockType = toolInvocationBlock
}
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 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)
}
}
}
}
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()),
))
}

View file

@ -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...)
}
}