summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/components/chat
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-06-27 07:46:42 -0500
committeradamdottv <[email protected]>2025-06-27 14:26:25 -0500
commit79bbf90b727f35915e665b99ba13f260b0dc94fe (patch)
tree4ee5e4d79635429a45b9035955323e3e03078e78 /packages/tui/internal/components/chat
parent226a4a7f3610860d437ddf8d7d8216e909297418 (diff)
downloadopencode-79bbf90b727f35915e665b99ba13f260b0dc94fe.tar.gz
opencode-79bbf90b727f35915e665b99ba13f260b0dc94fe.zip
chore: rework openapi spec and use stainless sdk
Diffstat (limited to 'packages/tui/internal/components/chat')
-rw-r--r--packages/tui/internal/components/chat/message.go129
-rw-r--r--packages/tui/internal/components/chat/messages.go102
2 files changed, 125 insertions, 106 deletions
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index 6fa52c1f4..cf4662d53 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -12,12 +12,13 @@ import (
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/charmbracelet/x/ansi"
+ "github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/diff"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
- "github.com/sst/opencode/pkg/client"
+ "github.com/tidwall/gjson"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
@@ -209,7 +210,7 @@ func calculatePadding() int {
}
}
-func renderText(message client.MessageInfo, text string, author string) string {
+func renderText(message opencode.Message, text string, author string) string {
t := theme.CurrentTheme()
width := layout.Current.Container.Width
padding := calculatePadding()
@@ -223,7 +224,7 @@ func renderText(message client.MessageInfo, text string, author string) string {
textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
- if message.Role == client.Assistant {
+ if message.Role == opencode.MessageRoleAssistant {
markdownWidth = width - padding - 4 - 3
}
minWidth := max(markdownWidth, (width-4)/2)
@@ -235,18 +236,18 @@ func renderText(message client.MessageInfo, text string, author string) string {
messageStyle = messageStyle.AlignHorizontal(lipgloss.Right)
}
content := messageStyle.Render(text)
- if message.Role == client.Assistant {
+ if message.Role == opencode.MessageRoleAssistant {
content = toMarkdown(text, markdownWidth, t.BackgroundPanel())
}
content = strings.Join([]string{content, info}, "\n")
switch message.Role {
- case client.User:
+ case opencode.MessageRoleUser:
return renderContentBlock(content,
WithAlign(lipgloss.Right),
WithBorderColor(t.Secondary()),
)
- case client.Assistant:
+ case opencode.MessageRoleAssistant:
return renderContentBlock(content,
WithAlign(lipgloss.Left),
WithBorderColor(t.Accent()),
@@ -256,15 +257,16 @@ func renderText(message client.MessageInfo, text string, author string) string {
}
func renderToolInvocation(
- toolCall client.MessageToolInvocationToolCall,
+ toolCall opencode.ToolInvocationPart,
result *string,
- metadata client.MessageMetadata_Tool_AdditionalProperties,
+ metadata opencode.MessageMetadataTool,
showDetails bool,
isLast bool,
contentOnly bool,
+ messageMetadata opencode.MessageMetadata,
) string {
ignoredTools := []string{"todoread"}
- if slices.Contains(ignoredTools, toolCall.ToolName) {
+ if slices.Contains(ignoredTools, toolCall.ToolInvocation.ToolName) {
return ""
}
@@ -294,8 +296,8 @@ func renderToolInvocation(
BorderForeground(t.BackgroundPanel()).
BorderStyle(lipgloss.ThickBorder())
- if toolCall.State == "partial-call" {
- title := renderToolAction(toolCall.ToolName)
+ if toolCall.ToolInvocation.State == "partial-call" {
+ title := renderToolAction(toolCall.ToolInvocation.ToolName)
if !showDetails {
title = "∟ " + title
padding := calculatePadding()
@@ -316,8 +318,8 @@ func renderToolInvocation(
toolArgs := ""
toolArgsMap := make(map[string]any)
- if toolCall.Args != nil {
- value := *toolCall.Args
+ if toolCall.ToolInvocation.Args != nil {
+ value := toolCall.ToolInvocation.Args
if m, ok := value.(map[string]any); ok {
toolArgsMap = m
@@ -339,28 +341,35 @@ func renderToolInvocation(
error := ""
finished := result != nil && *result != ""
- if e, ok := metadata.Get("error"); ok && e.(bool) == true {
- if m, ok := metadata.Get("message"); ok {
- style = style.BorderLeftForeground(t.Error())
- error = styles.NewStyle().
- Foreground(t.Error()).
- Background(t.BackgroundPanel()).
- Render(m.(string))
- error = renderContentBlock(
- error,
- WithFullWidth(),
- WithBorderColor(t.Error()),
- WithMarginBottom(1),
- )
- }
+ 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.ToolName {
+ switch toolCall.ToolInvocation.ToolName {
case "read":
toolArgs = renderArgs(&toolArgsMap, "filePath")
title = fmt.Sprintf("READ %s", toolArgs)
- if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
+ preview := metadata.ExtraFields["preview"]
+ if preview != nil && toolArgsMap["filePath"] != nil {
filename := toolArgsMap["filePath"].(string)
body = preview.(string)
body = renderFile(filename, body, WithTruncate(6))
@@ -368,8 +377,9 @@ func renderToolInvocation(
case "edit":
if filename, ok := toolArgsMap["filePath"].(string); ok {
title = fmt.Sprintf("EDIT %s", relative(filename))
- if d, ok := metadata.Get("diff"); ok {
- patch := d.(string)
+ diffField := metadata.ExtraFields["diff"]
+ if diffField != nil {
+ patch := diffField.(string)
var formattedDiff string
if layout.Current.Viewport.Width < 80 {
formattedDiff, _ = diff.FormatUnifiedDiff(
@@ -406,7 +416,7 @@ func renderToolInvocation(
)
// Add diagnostics at the bottom if they exist
- if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
+ if diagnostics := renderDiagnostics(messageMetadata, filename); diagnostics != "" {
body += "\n" + renderContentBlock(diagnostics, WithFullWidth(), WithBorderColor(t.Error()))
}
}
@@ -418,7 +428,7 @@ func renderToolInvocation(
body = renderFile(filename, content)
// Add diagnostics at the bottom if they exist
- if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
+ if diagnostics := renderDiagnostics(messageMetadata, filename); diagnostics != "" {
body += "\n" + renderContentBlock(diagnostics, WithFullWidth(), WithBorderColor(t.Error()))
}
}
@@ -427,9 +437,10 @@ func renderToolInvocation(
if description, ok := toolArgsMap["description"].(string); ok {
title = fmt.Sprintf("SHELL %s", description)
}
- if stdout, ok := metadata.Get("stdout"); ok {
+ stdout := metadata.JSON.ExtraFields["stdout"]
+ if !stdout.IsNull() {
command := toolArgsMap["command"].(string)
- stdout := stdout.(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))
@@ -450,12 +461,13 @@ func renderToolInvocation(
case "todowrite":
title = fmt.Sprintf("PLAN")
- if to, ok := metadata.Get("todos"); ok && finished {
- todos := to.([]any)
- for _, todo := range todos {
- t := todo.(map[string]any)
- content := t["content"].(string)
- switch t["status"].(string) {
+ todos := metadata.JSON.ExtraFields["todos"]
+ if !todos.IsNull() && finished {
+ strTodos := todos.Raw()
+ todos := gjson.Parse(strTodos)
+ for _, todo := range todos.Array() {
+ content := todo.Get("content").String()
+ switch todo.Get("status").String() {
case "completed":
body += fmt.Sprintf("- [x] %s\n", content)
// case "in-progress":
@@ -470,21 +482,22 @@ func renderToolInvocation(
case "task":
if description, ok := toolArgsMap["description"].(string); ok {
title = fmt.Sprintf("TASK %s", description)
- if summary, ok := metadata.Get("summary"); ok {
- toolcalls := summary.([]any)
- // toolcalls :=
+ summary := metadata.JSON.ExtraFields["summary"]
+ if !summary.IsNull() {
+ strValue := summary.Raw()
+ toolcalls := gjson.Parse(strValue).Array()
steps := []string{}
for _, toolcall := range toolcalls {
- call := toolcall.(map[string]any)
+ call := toolcall.Value().(map[string]any)
if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok {
data, _ := json.Marshal(toolInvocation)
- var toolCall client.MessageToolInvocationToolCall
+ var toolCall opencode.ToolInvocationPart
_ = json.Unmarshal(data, &toolCall)
if metadata, ok := call["metadata"].(map[string]any); ok {
data, _ = json.Marshal(metadata)
- var toolMetadata client.MessageMetadata_Tool_AdditionalProperties
+ var toolMetadata opencode.MessageMetadataTool
_ = json.Unmarshal(data, &toolMetadata)
step := renderToolInvocation(
@@ -494,6 +507,7 @@ func renderToolInvocation(
false,
false,
true,
+ messageMetadata,
)
steps = append(steps, step)
}
@@ -505,7 +519,7 @@ func renderToolInvocation(
}
default:
- toolName := renderToolName(toolCall.ToolName)
+ toolName := renderToolName(toolCall.ToolInvocation.ToolName)
title = fmt.Sprintf("%s %s", toolName, toolArgs)
if result == nil {
empty := ""
@@ -537,7 +551,7 @@ func renderToolInvocation(
)
}
- if body == "" && error == "" {
+ if body == "" && error == "" && result != nil {
body = *result
body = truncateHeight(body, 10)
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
@@ -716,31 +730,28 @@ type Diagnostic struct {
}
// renderDiagnostics formats LSP diagnostics for display in the TUI
-func renderDiagnostics(metadata client.MessageMetadata_Tool_AdditionalProperties, filePath string) string {
- diagnosticsData, ok := metadata.Get("diagnostics")
- if !ok {
+func renderDiagnostics(metadata opencode.MessageMetadata, filePath string) string {
+ diagnosticsData := metadata.JSON.ExtraFields["diagnostics"]
+ if diagnosticsData.IsNull() {
return ""
}
// diagnosticsData should be a map[string][]Diagnostic
- diagnosticsMap, ok := diagnosticsData.(map[string]interface{})
- if !ok {
- return ""
- }
-
+ strDiagnosticsData := diagnosticsData.Raw()
+ diagnosticsMap := gjson.Parse(strDiagnosticsData).Value().(map[string]any)
fileDiagnostics, ok := diagnosticsMap[filePath]
if !ok {
return ""
}
- diagnosticsList, ok := fileDiagnostics.([]interface{})
+ diagnosticsList, ok := fileDiagnostics.([]any)
if !ok {
return ""
}
var errorDiagnostics []string
for _, diagInterface := range diagnosticsList {
- diagMap, ok := diagInterface.(map[string]interface{})
+ diagMap, ok := diagInterface.(map[string]any)
if !ok {
continue
}
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index da45545c9..e1c7a9d14 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -9,13 +9,13 @@ import (
"github.com/charmbracelet/bubbles/v2/viewport"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
+ "github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/commands"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
- "github.com/sst/opencode/pkg/client"
)
type MessagesComponent interface {
@@ -83,7 +83,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.tail {
m.viewport.GotoBottom()
}
- case client.EventSessionUpdated, client.EventMessageUpdated:
+ case opencode.EventListResponseEventSessionUpdated, opencode.EventListResponseEventMessageUpdated:
m.renderView()
if m.tail {
m.viewport.GotoBottom()
@@ -130,90 +130,97 @@ func (m *messagesComponent) renderView() {
lastToolIndex := 0
lastToolIndices := []int{}
for i, p := range message.Parts {
- part, _ := p.ValueByDiscriminator()
- switch part.(type) {
- case client.MessagePartText:
+ switch p.Type {
+ case opencode.MessagePartTypeText:
lastToolIndices = append(lastToolIndices, lastToolIndex)
- case client.MessagePartToolInvocation:
+ case opencode.MessagePartTypeToolInvocation:
lastToolIndex = i
}
}
author := ""
switch message.Role {
- case client.User:
+ case opencode.MessageRoleUser:
author = m.app.Info.User
- case client.Assistant:
+ case opencode.MessageRoleAssistant:
author = message.Metadata.Assistant.ModelID
}
for i, p := range message.Parts {
- part, err := p.ValueByDiscriminator()
- if err != nil {
- continue //TODO: handle error?
- }
-
- switch part.(type) {
+ switch part := p.AsUnion().(type) {
// case client.MessagePartStepStart:
// messages = append(messages, "")
- case client.MessagePartText:
- text := part.(client.MessagePartText)
- key := m.cache.GenerateKey(message.Id, text.Text, layout.Current.Viewport.Width)
+ 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, text.Text, author)
+ content = renderText(message, p.Text, author)
m.cache.Set(key, content)
}
if previousBlockType != none {
blocks = append(blocks, "")
}
blocks = append(blocks, content)
- if message.Role == client.User {
+ if message.Role == opencode.MessageRoleUser {
previousBlockType = userTextBlock
- } else if message.Role == client.Assistant {
+ } else if message.Role == opencode.MessageRoleAssistant {
previousBlockType = assistantTextBlock
}
- case client.MessagePartToolInvocation:
+ case opencode.ToolInvocationPart:
isLastToolInvocation := slices.Contains(lastToolIndices, i)
- toolInvocationPart := part.(client.MessagePartToolInvocation)
- toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall()
- metadata := client.MessageMetadata_Tool_AdditionalProperties{}
- if _, ok := message.Metadata.Tool[toolCall.ToolCallId]; ok {
- metadata = message.Metadata.Tool[toolCall.ToolCallId]
+ 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
- resultPart, resultError := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolResult()
- if resultError == nil {
- result = &resultPart.Result
+ if part.ToolInvocation.Result != "" {
+ result = &part.ToolInvocation.Result
}
- if toolCall.State == "result" {
- key := m.cache.GenerateKey(message.Id,
- toolCall.ToolCallId,
+ 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 = renderToolInvocation(
- toolCall,
+ part,
result,
metadata,
m.showToolDetails,
isLastToolInvocation,
false,
+ message.Metadata,
)
m.cache.Set(key, content)
}
} else {
// if the tool call isn't finished, don't cache
content = renderToolInvocation(
- toolCall,
+ part,
result,
metadata,
m.showToolDetails,
isLastToolInvocation,
false,
+ message.Metadata,
)
}
@@ -226,16 +233,17 @@ func (m *messagesComponent) renderView() {
}
error := ""
- if message.Metadata.Error != nil {
- errorValue, _ := message.Metadata.Error.ValueByDiscriminator()
- switch errorValue.(type) {
- case client.UnknownError:
- clientError := errorValue.(client.UnknownError)
- error = clientError.Data.Message
- error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
- blocks = append(blocks, error)
- previousBlockType = errorBlock
- }
+ switch err := message.Metadata.Error.AsUnion().(type) {
+ case nil:
+ default:
+ clientError := err.(opencode.UnknownError)
+ error = clientError.Data.Message
+ }
+
+ if error != "" {
+ error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
+ blocks = append(blocks, error)
+ previousBlockType = errorBlock
}
}
@@ -254,7 +262,7 @@ func (m *messagesComponent) renderView() {
}
func (m *messagesComponent) header() string {
- if m.app.Session.Id == "" {
+ if m.app.Session.ID == "" {
return ""
}
@@ -264,8 +272,8 @@ func (m *messagesComponent) header() string {
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
headerLines := []string{}
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
- if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
- headerLines = append(headerLines, muted(m.app.Session.Share.Url))
+ if m.app.Session.Share.URL != "" {
+ headerLines = append(headerLines, muted(m.app.Session.Share.URL))
} else {
headerLines = append(headerLines, base("/share")+muted(" to create a shareable link"))
}