mirror of
https://github.com/sst/opencode.git
synced 2025-08-04 13:30:52 +00:00
go stuff
This commit is contained in:
parent
3e2fe176f9
commit
8eaf25b319
13 changed files with 383 additions and 416 deletions
|
@ -3,7 +3,7 @@ import { Bus } from "../bus"
|
|||
import { Provider } from "../provider/provider"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Message } from "./message"
|
||||
import type { AssistantContent, ModelMessage, UserContent } from "ai"
|
||||
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
|
||||
|
||||
export namespace MessageV2 {
|
||||
export const OutputLengthError = NamedError.create(
|
||||
|
@ -132,7 +132,6 @@ export namespace MessageV2 {
|
|||
export type UserPart = z.infer<typeof UserPart>
|
||||
|
||||
export const User = Base.extend({
|
||||
id: z.string(),
|
||||
role: z.literal("user"),
|
||||
parts: z.array(UserPart),
|
||||
time: z.object({
|
||||
|
@ -151,7 +150,6 @@ export namespace MessageV2 {
|
|||
export type AssistantPart = z.infer<typeof AssistantPart>
|
||||
|
||||
export const Assistant = Base.extend({
|
||||
id: z.string(),
|
||||
role: z.literal("assistant"),
|
||||
parts: z.array(AssistantPart),
|
||||
time: z.object({
|
||||
|
@ -244,6 +242,13 @@ export namespace MessageV2 {
|
|||
},
|
||||
]
|
||||
}
|
||||
if (part.type === "step-start") {
|
||||
return [
|
||||
{
|
||||
type: "step-start",
|
||||
},
|
||||
]
|
||||
}
|
||||
if (part.type === "tool-invocation") {
|
||||
return [
|
||||
{
|
||||
|
@ -325,13 +330,15 @@ export namespace MessageV2 {
|
|||
}
|
||||
|
||||
export function toModelMessage(input: Info[]): ModelMessage[] {
|
||||
const result: ModelMessage[] = []
|
||||
const result: UIMessage[] = []
|
||||
|
||||
for (const msg of input) {
|
||||
if (msg.parts.length === 0) continue
|
||||
if (msg.role === "user") {
|
||||
result.push({
|
||||
id: msg.id,
|
||||
role: "user",
|
||||
content: msg.parts.flatMap((part): Exclude<UserContent, string> => {
|
||||
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
|
||||
if (part.type === "text")
|
||||
return [
|
||||
{
|
||||
|
@ -343,7 +350,7 @@ export namespace MessageV2 {
|
|||
return [
|
||||
{
|
||||
type: "file",
|
||||
data: part.url,
|
||||
url: part.url,
|
||||
mediaType: part.mime,
|
||||
filename: part.filename,
|
||||
},
|
||||
|
@ -355,62 +362,51 @@ export namespace MessageV2 {
|
|||
|
||||
if (msg.role === "assistant") {
|
||||
result.push({
|
||||
id: msg.id,
|
||||
role: "assistant",
|
||||
content: msg.parts.flatMap(
|
||||
(part): Exclude<AssistantContent, string> => {
|
||||
if (part.type === "text")
|
||||
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
|
||||
if (part.type === "text")
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: part.text,
|
||||
},
|
||||
]
|
||||
if (part.type === "step-start")
|
||||
return [
|
||||
{
|
||||
type: "step-start",
|
||||
},
|
||||
]
|
||||
if (part.type === "tool") {
|
||||
if (part.state.status === "completed")
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: part.text,
|
||||
type: ("tool-" + part.tool) as `tool-${string}`,
|
||||
state: "output-available",
|
||||
toolCallId: part.id,
|
||||
input: part.state.input,
|
||||
output: part.state.output,
|
||||
},
|
||||
]
|
||||
if (part.type === "tool") {
|
||||
if (part.state.status === "completed")
|
||||
return [
|
||||
{
|
||||
type: "tool-call",
|
||||
input: part.state.input,
|
||||
toolName: part.tool,
|
||||
toolCallId: part.id,
|
||||
},
|
||||
{
|
||||
type: "tool-result",
|
||||
toolCallId: part.id,
|
||||
toolName: part.tool,
|
||||
output: {
|
||||
type: "text",
|
||||
value: part.state.output,
|
||||
},
|
||||
},
|
||||
]
|
||||
if (part.state.status === "error")
|
||||
return [
|
||||
{
|
||||
type: "tool-call",
|
||||
input: part.state.input,
|
||||
toolName: part.tool,
|
||||
toolCallId: part.id,
|
||||
},
|
||||
{
|
||||
type: "tool-result",
|
||||
toolCallId: part.id,
|
||||
toolName: part.tool,
|
||||
output: {
|
||||
type: "text",
|
||||
value: part.state.error,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
if (part.state.status === "error")
|
||||
return [
|
||||
{
|
||||
type: ("tool-" + part.tool) as `tool-${string}`,
|
||||
state: "output-error",
|
||||
toolCallId: part.id,
|
||||
input: part.state.input,
|
||||
errorText: part.state.error,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
),
|
||||
return []
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return convertToModelMessages(result)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import z from "zod"
|
||||
import { Bus } from "../bus"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { NamedError } from "../util/error"
|
||||
|
||||
|
@ -197,28 +196,4 @@ export namespace Message {
|
|||
ref: "Message",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export const Event = {
|
||||
Updated: Bus.event(
|
||||
"message.updated",
|
||||
z.object({
|
||||
info: Info,
|
||||
}),
|
||||
),
|
||||
Removed: Bus.event(
|
||||
"message.removed",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
}),
|
||||
),
|
||||
PartUpdated: Bus.event(
|
||||
"message.part.updated",
|
||||
z.object({
|
||||
part: MessagePart,
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,13 @@ require (
|
|||
github.com/charmbracelet/glamour v0.10.0
|
||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
|
||||
github.com/charmbracelet/x/ansi v0.8.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lithammer/fuzzysearch v1.1.8
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/muesli/termenv v0.16.0
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
||||
github.com/stainless-sdks/opencode-go dev
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.8
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
rsc.io/qr v0.2.0
|
||||
)
|
||||
|
@ -37,7 +38,6 @@ require (
|
|||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/goccy/go-yaml v1.17.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/invopop/yaml v0.3.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
|
|
|
@ -30,7 +30,7 @@ type App struct {
|
|||
Provider *opencode.Provider
|
||||
Model *opencode.Model
|
||||
Session *opencode.Session
|
||||
Messages []opencode.Message
|
||||
Messages []opencode.MessageUnion
|
||||
Commands commands.CommandRegistry
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ type SendMsg struct {
|
|||
Attachments []opencode.FilePartParam
|
||||
}
|
||||
type OptimisticMessageAddedMsg struct {
|
||||
Message opencode.Message
|
||||
Message opencode.MessageUnion
|
||||
}
|
||||
type FileRenderedMsg struct {
|
||||
FilePath string
|
||||
|
@ -116,7 +116,7 @@ func New(
|
|||
State: appState,
|
||||
Client: httpClient,
|
||||
Session: &opencode.Session{},
|
||||
Messages: []opencode.Message{},
|
||||
Messages: []opencode.MessageUnion{},
|
||||
Commands: commands.LoadFromConfig(configInfo),
|
||||
}
|
||||
|
||||
|
@ -223,7 +223,10 @@ func (a *App) IsBusy() bool {
|
|||
}
|
||||
|
||||
lastMessage := a.Messages[len(a.Messages)-1]
|
||||
return lastMessage.Metadata.Time.Completed == 0
|
||||
if casted, ok := lastMessage.(opencode.AssistantMessage); ok {
|
||||
return casted.Time.Completed == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) SaveState() {
|
||||
|
@ -304,30 +307,28 @@ func (a *App) SendChatMessage(
|
|||
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
|
||||
}
|
||||
|
||||
optimisticParts := []opencode.MessagePart{{
|
||||
Type: opencode.MessagePartTypeText,
|
||||
optimisticParts := []opencode.UserMessagePart{{
|
||||
Type: opencode.UserMessagePartTypeText,
|
||||
Text: text,
|
||||
}}
|
||||
if len(attachments) > 0 {
|
||||
for _, attachment := range attachments {
|
||||
optimisticParts = append(optimisticParts, opencode.MessagePart{
|
||||
Type: opencode.MessagePartTypeFile,
|
||||
Filename: attachment.Filename.Value,
|
||||
MediaType: attachment.MediaType.Value,
|
||||
URL: attachment.URL.Value,
|
||||
optimisticParts = append(optimisticParts, opencode.UserMessagePart{
|
||||
Type: opencode.UserMessagePartTypeFile,
|
||||
Filename: attachment.Filename.Value,
|
||||
Mime: attachment.Mime.Value,
|
||||
URL: attachment.URL.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
optimisticMessage := opencode.Message{
|
||||
ID: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
|
||||
Role: opencode.MessageRoleUser,
|
||||
Parts: optimisticParts,
|
||||
Metadata: opencode.MessageMetadata{
|
||||
SessionID: a.Session.ID,
|
||||
Time: opencode.MessageMetadataTime{
|
||||
Created: float64(time.Now().Unix()),
|
||||
},
|
||||
optimisticMessage := opencode.UserMessage{
|
||||
ID: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
|
||||
Role: opencode.UserMessageRoleUser,
|
||||
Parts: optimisticParts,
|
||||
SessionID: a.Session.ID,
|
||||
Time: opencode.UserMessageTime{
|
||||
Created: float64(time.Now().Unix()),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -335,7 +336,7 @@ func (a *App) SendChatMessage(
|
|||
cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
|
||||
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
parts := []opencode.MessagePartUnionParam{
|
||||
parts := []opencode.UserMessagePartUnionParam{
|
||||
opencode.TextPartParam{
|
||||
Type: opencode.F(opencode.TextPartTypeText),
|
||||
Text: opencode.F(text),
|
||||
|
@ -344,10 +345,10 @@ func (a *App) SendChatMessage(
|
|||
if len(attachments) > 0 {
|
||||
for _, attachment := range attachments {
|
||||
parts = append(parts, opencode.FilePartParam{
|
||||
MediaType: attachment.MediaType,
|
||||
Type: attachment.Type,
|
||||
URL: attachment.URL,
|
||||
Filename: attachment.Filename,
|
||||
Mime: attachment.Mime,
|
||||
Type: attachment.Type,
|
||||
URL: attachment.URL,
|
||||
Filename: attachment.Filename,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -248,10 +248,10 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
|||
fileParts := make([]opencode.FilePartParam, 0)
|
||||
for _, attachment := range attachments {
|
||||
fileParts = append(fileParts, opencode.FilePartParam{
|
||||
Type: opencode.F(opencode.FilePartTypeFile),
|
||||
MediaType: opencode.F(attachment.MediaType),
|
||||
URL: opencode.F(attachment.URL),
|
||||
Filename: opencode.F(attachment.Filename),
|
||||
Type: opencode.F(opencode.FilePartTypeFile),
|
||||
Mime: opencode.F(attachment.MediaType),
|
||||
URL: opencode.F(attachment.URL),
|
||||
Filename: opencode.F(attachment.Filename),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import (
|
|||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
"github.com/tidwall/gjson"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
@ -217,18 +216,35 @@ func renderContentBlock(
|
|||
|
||||
func renderText(
|
||||
app *app.App,
|
||||
message opencode.Message,
|
||||
message opencode.MessageUnion,
|
||||
text string,
|
||||
author string,
|
||||
showToolDetails bool,
|
||||
highlight bool,
|
||||
width int,
|
||||
extra string,
|
||||
toolCalls ...opencode.ToolInvocationPart,
|
||||
toolCalls ...opencode.ToolPart,
|
||||
) string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)).
|
||||
var ts time.Time
|
||||
backgroundColor := t.BackgroundPanel()
|
||||
if highlight {
|
||||
backgroundColor = t.BackgroundElement()
|
||||
}
|
||||
messageStyle := styles.NewStyle().Background(backgroundColor)
|
||||
content := messageStyle.Render(text)
|
||||
|
||||
switch casted := message.(type) {
|
||||
case opencode.AssistantMessage:
|
||||
ts = time.UnixMilli(int64(casted.Time.Created))
|
||||
content = util.ToMarkdown(text, width, backgroundColor)
|
||||
case opencode.UserMessage:
|
||||
ts = time.UnixMilli(int64(casted.Time.Created))
|
||||
messageStyle = messageStyle.Width(width - 6)
|
||||
}
|
||||
|
||||
timestamp := ts.
|
||||
Local().
|
||||
Format("02 Jan 2006 03:04 PM")
|
||||
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
|
||||
|
@ -238,30 +254,12 @@ func renderText(
|
|||
info := fmt.Sprintf("%s (%s)", author, timestamp)
|
||||
info = styles.NewStyle().Foreground(t.TextMuted()).Render(info)
|
||||
|
||||
backgroundColor := t.BackgroundPanel()
|
||||
if highlight {
|
||||
backgroundColor = t.BackgroundElement()
|
||||
}
|
||||
messageStyle := styles.NewStyle().Background(backgroundColor)
|
||||
if message.Role == opencode.MessageRoleUser {
|
||||
messageStyle = messageStyle.Width(width - 6)
|
||||
}
|
||||
|
||||
content := messageStyle.Render(text)
|
||||
if message.Role == opencode.MessageRoleAssistant {
|
||||
content = util.ToMarkdown(text, width, backgroundColor)
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
title := renderToolTitle(toolCall, width)
|
||||
style := styles.NewStyle()
|
||||
if _, ok := metadata.ExtraFields["error"]; ok {
|
||||
if toolCall.State.Status == opencode.ToolPartStateStatusError {
|
||||
style = style.Foreground(t.Error())
|
||||
}
|
||||
title = style.Render(title)
|
||||
|
@ -276,8 +274,8 @@ func renderText(
|
|||
}
|
||||
content = strings.Join(sections, "\n")
|
||||
|
||||
switch message.Role {
|
||||
case opencode.MessageRoleUser:
|
||||
switch message.(type) {
|
||||
case opencode.UserMessage:
|
||||
return renderContentBlock(
|
||||
app,
|
||||
content,
|
||||
|
@ -286,7 +284,7 @@ func renderText(
|
|||
WithTextColor(t.Text()),
|
||||
WithBorderColorRight(t.Secondary()),
|
||||
)
|
||||
case opencode.MessageRoleAssistant:
|
||||
case opencode.AssistantMessage:
|
||||
return renderContentBlock(
|
||||
app,
|
||||
content,
|
||||
|
@ -300,39 +298,32 @@ func renderText(
|
|||
|
||||
func renderToolDetails(
|
||||
app *app.App,
|
||||
toolCall opencode.ToolInvocationPart,
|
||||
messageMetadata opencode.MessageMetadata,
|
||||
toolCall opencode.ToolPart,
|
||||
highlight bool,
|
||||
width int,
|
||||
) string {
|
||||
ignoredTools := []string{"todoread"}
|
||||
if slices.Contains(ignoredTools, toolCall.ToolInvocation.ToolName) {
|
||||
if slices.Contains(ignoredTools, toolCall.Tool) {
|
||||
return ""
|
||||
}
|
||||
|
||||
toolCallID := toolCall.ToolInvocation.ToolCallID
|
||||
metadata := opencode.MessageMetadataTool{}
|
||||
if _, ok := messageMetadata.Tool[toolCallID]; ok {
|
||||
metadata = messageMetadata.Tool[toolCallID]
|
||||
}
|
||||
|
||||
var result *string
|
||||
if toolCall.ToolInvocation.Result != "" {
|
||||
result = &toolCall.ToolInvocation.Result
|
||||
}
|
||||
|
||||
if toolCall.ToolInvocation.State == "partial-call" {
|
||||
title := renderToolTitle(toolCall, messageMetadata, width)
|
||||
if toolCall.State.Status == opencode.ToolPartStateStatusPending || toolCall.State.Status == opencode.ToolPartStateStatusRunning {
|
||||
title := renderToolTitle(toolCall, width)
|
||||
return renderContentBlock(app, title, highlight, width)
|
||||
}
|
||||
|
||||
toolArgsMap := make(map[string]any)
|
||||
if toolCall.ToolInvocation.Args != nil {
|
||||
value := toolCall.ToolInvocation.Args
|
||||
var result *string
|
||||
if toolCall.State.Output != "" {
|
||||
result = &toolCall.State.Output
|
||||
}
|
||||
|
||||
toolInputMap := make(map[string]any)
|
||||
if toolCall.State.Input != nil {
|
||||
value := toolCall.State.Input
|
||||
if m, ok := value.(map[string]any); ok {
|
||||
toolArgsMap = m
|
||||
keys := make([]string, 0, len(toolArgsMap))
|
||||
for key := range toolArgsMap {
|
||||
toolInputMap = m
|
||||
keys := make([]string, 0, len(toolInputMap))
|
||||
for key := range toolInputMap {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
|
@ -340,7 +331,6 @@ func renderToolDetails(
|
|||
}
|
||||
|
||||
body := ""
|
||||
finished := result != nil && *result != ""
|
||||
t := theme.CurrentTheme()
|
||||
backgroundColor := t.BackgroundPanel()
|
||||
borderColor := t.BackgroundPanel()
|
||||
|
@ -349,137 +339,128 @@ func renderToolDetails(
|
|||
borderColor = t.BorderActive()
|
||||
}
|
||||
|
||||
switch toolCall.ToolInvocation.ToolName {
|
||||
case "read":
|
||||
preview := metadata.ExtraFields["preview"]
|
||||
if preview != nil && toolArgsMap["filePath"] != nil {
|
||||
filename := toolArgsMap["filePath"].(string)
|
||||
body = preview.(string)
|
||||
body = util.RenderFile(filename, body, width, util.WithTruncate(6))
|
||||
}
|
||||
case "edit":
|
||||
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
||||
diffField := metadata.ExtraFields["diff"]
|
||||
if diffField != nil {
|
||||
patch := diffField.(string)
|
||||
var formattedDiff string
|
||||
formattedDiff, _ = diff.FormatUnifiedDiff(
|
||||
filename,
|
||||
patch,
|
||||
diff.WithWidth(width-2),
|
||||
)
|
||||
body = strings.TrimSpace(formattedDiff)
|
||||
style := styles.NewStyle().
|
||||
Background(backgroundColor).
|
||||
Foreground(t.TextMuted()).
|
||||
Padding(1, 2).
|
||||
Width(width - 4)
|
||||
if highlight {
|
||||
style = style.Foreground(t.Text()).Bold(true)
|
||||
}
|
||||
|
||||
if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
|
||||
diagnostics = style.Render(diagnostics)
|
||||
body += "\n" + diagnostics
|
||||
}
|
||||
|
||||
title := renderToolTitle(toolCall, messageMetadata, width)
|
||||
title = style.Render(title)
|
||||
content := title + "\n" + body
|
||||
content = renderContentBlock(
|
||||
app,
|
||||
content,
|
||||
highlight,
|
||||
width,
|
||||
WithPadding(0),
|
||||
WithBorderColor(borderColor),
|
||||
)
|
||||
return content
|
||||
if toolCall.State.Status == opencode.ToolPartStateStatusCompleted {
|
||||
metadata := toolCall.State.Metadata.(map[string]any)
|
||||
switch toolCall.Tool {
|
||||
case "read":
|
||||
preview := metadata["preview"]
|
||||
if preview != nil && toolInputMap["filePath"] != nil {
|
||||
filename := toolInputMap["filePath"].(string)
|
||||
body = preview.(string)
|
||||
body = util.RenderFile(filename, body, width, util.WithTruncate(6))
|
||||
}
|
||||
}
|
||||
case "write":
|
||||
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
||||
if content, ok := toolArgsMap["content"].(string); ok {
|
||||
body = util.RenderFile(filename, content, width)
|
||||
if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
|
||||
body += "\n\n" + diagnostics
|
||||
case "edit":
|
||||
if filename, ok := toolInputMap["filePath"].(string); ok {
|
||||
diffField := metadata["diff"]
|
||||
if diffField != nil {
|
||||
patch := diffField.(string)
|
||||
var formattedDiff string
|
||||
formattedDiff, _ = diff.FormatUnifiedDiff(
|
||||
filename,
|
||||
patch,
|
||||
diff.WithWidth(width-2),
|
||||
)
|
||||
body = strings.TrimSpace(formattedDiff)
|
||||
style := styles.NewStyle().
|
||||
Background(backgroundColor).
|
||||
Foreground(t.TextMuted()).
|
||||
Padding(1, 2).
|
||||
Width(width - 4)
|
||||
if highlight {
|
||||
style = style.Foreground(t.Text()).Bold(true)
|
||||
}
|
||||
|
||||
if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
|
||||
diagnostics = style.Render(diagnostics)
|
||||
body += "\n" + diagnostics
|
||||
}
|
||||
|
||||
title := renderToolTitle(toolCall, width)
|
||||
title = style.Render(title)
|
||||
content := title + "\n" + body
|
||||
content = renderContentBlock(
|
||||
app,
|
||||
content,
|
||||
highlight,
|
||||
width,
|
||||
WithPadding(0),
|
||||
WithBorderColor(borderColor),
|
||||
)
|
||||
return content
|
||||
}
|
||||
}
|
||||
}
|
||||
case "bash":
|
||||
stdout := metadata.ExtraFields["stdout"]
|
||||
if stdout != nil {
|
||||
command := toolArgsMap["command"].(string)
|
||||
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
|
||||
body = util.ToMarkdown(body, width, backgroundColor)
|
||||
}
|
||||
case "webfetch":
|
||||
if format, ok := toolArgsMap["format"].(string); ok && result != nil {
|
||||
body = *result
|
||||
body = util.TruncateHeight(body, 10)
|
||||
if format == "html" || format == "markdown" {
|
||||
case "write":
|
||||
if filename, ok := toolInputMap["filePath"].(string); ok {
|
||||
if content, ok := toolInputMap["content"].(string); ok {
|
||||
body = util.RenderFile(filename, content, width)
|
||||
if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
|
||||
body += "\n\n" + diagnostics
|
||||
}
|
||||
}
|
||||
}
|
||||
case "bash":
|
||||
stdout := metadata["stdout"]
|
||||
if stdout != nil {
|
||||
command := toolInputMap["command"].(string)
|
||||
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
|
||||
body = util.ToMarkdown(body, width, backgroundColor)
|
||||
}
|
||||
}
|
||||
case "todowrite":
|
||||
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":
|
||||
// body += fmt.Sprintf("- [ ] %s\n", content)
|
||||
default:
|
||||
body += fmt.Sprintf("- [ ] %s\n", content)
|
||||
case "webfetch":
|
||||
if format, ok := toolInputMap["format"].(string); ok && result != nil {
|
||||
body = *result
|
||||
body = util.TruncateHeight(body, 10)
|
||||
if format == "html" || format == "markdown" {
|
||||
body = util.ToMarkdown(body, width, backgroundColor)
|
||||
}
|
||||
}
|
||||
body = util.ToMarkdown(body, width, backgroundColor)
|
||||
}
|
||||
case "task":
|
||||
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)
|
||||
case "todowrite":
|
||||
todos := metadata["todos"]
|
||||
if todos != nil {
|
||||
for _, item := range todos.([]any) {
|
||||
todo := item.(map[string]any)
|
||||
content := todo["content"].(string)
|
||||
switch todo["status"] {
|
||||
case "completed":
|
||||
body += fmt.Sprintf("- [x] %s\n", content)
|
||||
// case "in-progress":
|
||||
// body += fmt.Sprintf("- [ ] %s\n", content)
|
||||
default:
|
||||
body += fmt.Sprintf("- [ ] %s\n", content)
|
||||
}
|
||||
}
|
||||
body = util.ToMarkdown(body, width, backgroundColor)
|
||||
}
|
||||
case "task":
|
||||
summary := metadata["summary"]
|
||||
if summary != nil {
|
||||
toolcalls := summary.([]any)
|
||||
steps := []string{}
|
||||
for _, toolcall := range toolcalls {
|
||||
call := toolcall.(map[string]any)
|
||||
if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok {
|
||||
data, _ := json.Marshal(toolInvocation)
|
||||
var toolCall opencode.ToolPart
|
||||
_ = json.Unmarshal(data, &toolCall)
|
||||
step := renderToolTitle(toolCall, width)
|
||||
step = "∟ " + step
|
||||
steps = append(steps, step)
|
||||
}
|
||||
}
|
||||
body = strings.Join(steps, "\n")
|
||||
}
|
||||
body = strings.Join(steps, "\n")
|
||||
default:
|
||||
if result == nil {
|
||||
empty := ""
|
||||
result = &empty
|
||||
}
|
||||
body = *result
|
||||
body = util.TruncateHeight(body, 10)
|
||||
}
|
||||
default:
|
||||
if result == nil {
|
||||
empty := ""
|
||||
result = &empty
|
||||
}
|
||||
body = *result
|
||||
body = util.TruncateHeight(body, 10)
|
||||
}
|
||||
|
||||
error := ""
|
||||
if err, ok := metadata.ExtraFields["error"].(bool); ok && err {
|
||||
if message, ok := metadata.ExtraFields["message"].(string); ok {
|
||||
error = message
|
||||
}
|
||||
if toolCall.State.Status == opencode.ToolPartStateStatusError {
|
||||
error = toolCall.State.Error
|
||||
}
|
||||
|
||||
if error != "" {
|
||||
|
@ -494,7 +475,7 @@ func renderToolDetails(
|
|||
body = util.TruncateHeight(body, 10)
|
||||
}
|
||||
|
||||
title := renderToolTitle(toolCall, messageMetadata, width)
|
||||
title := renderToolTitle(toolCall, width)
|
||||
content := title + "\n\n" + body
|
||||
return renderContentBlock(app, content, highlight, width, WithBorderColor(borderColor))
|
||||
}
|
||||
|
@ -515,20 +496,19 @@ func renderToolName(name string) string {
|
|||
}
|
||||
|
||||
func renderToolTitle(
|
||||
toolCall opencode.ToolInvocationPart,
|
||||
messageMetadata opencode.MessageMetadata,
|
||||
toolCall opencode.ToolPart,
|
||||
width int,
|
||||
) string {
|
||||
// TODO: handle truncate to width
|
||||
|
||||
if toolCall.ToolInvocation.State == "partial-call" {
|
||||
return renderToolAction(toolCall.ToolInvocation.ToolName)
|
||||
if toolCall.State.Status == opencode.ToolPartStateStatusPending {
|
||||
return renderToolAction(toolCall.Tool)
|
||||
}
|
||||
|
||||
toolArgs := ""
|
||||
toolArgsMap := make(map[string]any)
|
||||
if toolCall.ToolInvocation.Args != nil {
|
||||
value := toolCall.ToolInvocation.Args
|
||||
if toolCall.State.Input != nil {
|
||||
value := toolCall.State.Input
|
||||
if m, ok := value.(map[string]any); ok {
|
||||
toolArgsMap = m
|
||||
|
||||
|
@ -546,8 +526,8 @@ func renderToolTitle(
|
|||
}
|
||||
}
|
||||
|
||||
title := renderToolName(toolCall.ToolInvocation.ToolName)
|
||||
switch toolCall.ToolInvocation.ToolName {
|
||||
title := renderToolName(toolCall.Tool)
|
||||
switch toolCall.Tool {
|
||||
case "read":
|
||||
toolArgs = renderArgs(&toolArgsMap, "filePath")
|
||||
title = fmt.Sprintf("%s %s", title, toolArgs)
|
||||
|
@ -565,7 +545,7 @@ func renderToolTitle(
|
|||
case "todowrite", "todoread":
|
||||
// title is just the tool name
|
||||
default:
|
||||
toolName := renderToolName(toolCall.ToolInvocation.ToolName)
|
||||
toolName := renderToolName(toolCall.Tool)
|
||||
title = fmt.Sprintf("%s %s", toolName, toolArgs)
|
||||
}
|
||||
return title
|
||||
|
@ -645,8 +625,8 @@ type Diagnostic struct {
|
|||
}
|
||||
|
||||
// renderDiagnostics formats LSP diagnostics for display in the TUI
|
||||
func renderDiagnostics(metadata opencode.MessageMetadataTool, filePath string) string {
|
||||
if diagnosticsData, ok := metadata.ExtraFields["diagnostics"].(map[string]any); ok {
|
||||
func renderDiagnostics(metadata map[string]any, filePath string) string {
|
||||
if diagnosticsData, ok := metadata["diagnostics"].(map[string]any); ok {
|
||||
if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok {
|
||||
var errorDiagnostics []string
|
||||
for _, diagInterface := range fileDiagnostics {
|
||||
|
|
|
@ -99,7 +99,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
}
|
||||
case opencode.EventListResponseEventMessageUpdated:
|
||||
if msg.Properties.Info.Metadata.SessionID == m.app.Session.ID {
|
||||
if msg.Properties.Info.SessionID == m.app.Session.ID {
|
||||
m.renderView(m.width)
|
||||
if m.tail {
|
||||
m.viewport.GotoBottom()
|
||||
|
@ -124,19 +124,19 @@ func (m *messagesComponent) renderView(width int) {
|
|||
m.partCount = 0
|
||||
m.lineCount = 0
|
||||
|
||||
orphanedToolCalls := make([]opencode.ToolInvocationPart, 0)
|
||||
orphanedToolCalls := make([]opencode.ToolPart, 0)
|
||||
|
||||
for _, message := range m.app.Messages {
|
||||
var content string
|
||||
var cached bool
|
||||
|
||||
switch message.Role {
|
||||
case opencode.MessageRoleUser:
|
||||
switch casted := message.(type) {
|
||||
case opencode.UserMessage:
|
||||
userLoop:
|
||||
for partIndex, part := range message.Parts {
|
||||
for partIndex, part := range casted.Parts {
|
||||
switch part := part.AsUnion().(type) {
|
||||
case opencode.TextPart:
|
||||
remainingParts := message.Parts[partIndex+1:]
|
||||
remainingParts := casted.Parts[partIndex+1:]
|
||||
fileParts := make([]opencode.FilePart, 0)
|
||||
for _, part := range remainingParts {
|
||||
switch part := part.AsUnion().(type) {
|
||||
|
@ -150,7 +150,7 @@ func (m *messagesComponent) renderView(width int) {
|
|||
mediaTypeStyle := styles.NewStyle().Background(t.Secondary()).Foreground(t.BackgroundPanel()).Padding(0, 1)
|
||||
for _, filePart := range fileParts {
|
||||
mediaType := ""
|
||||
switch filePart.MediaType {
|
||||
switch filePart.Mime {
|
||||
case "text/plain":
|
||||
mediaType = "txt"
|
||||
case "image/png", "image/jpeg", "image/gif", "image/webp":
|
||||
|
@ -175,7 +175,7 @@ func (m *messagesComponent) renderView(width int) {
|
|||
flexItems...,
|
||||
)
|
||||
|
||||
key := m.cache.GenerateKey(message.ID, part.Text, width, m.selectedPart == m.partCount, files)
|
||||
key := m.cache.GenerateKey(casted.ID, part.Text, width, m.selectedPart == m.partCount, files)
|
||||
content, cached = m.cache.Get(key)
|
||||
if !cached {
|
||||
content = renderText(
|
||||
|
@ -199,21 +199,21 @@ func (m *messagesComponent) renderView(width int) {
|
|||
}
|
||||
}
|
||||
|
||||
case opencode.MessageRoleAssistant:
|
||||
case opencode.AssistantMessage:
|
||||
hasTextPart := false
|
||||
for partIndex, p := range message.Parts {
|
||||
for partIndex, p := range casted.Parts {
|
||||
switch part := p.AsUnion().(type) {
|
||||
case opencode.TextPart:
|
||||
hasTextPart = true
|
||||
finished := message.Metadata.Time.Completed > 0
|
||||
remainingParts := message.Parts[partIndex+1:]
|
||||
toolCallParts := make([]opencode.ToolInvocationPart, 0)
|
||||
finished := casted.Time.Completed > 0
|
||||
remainingParts := casted.Parts[partIndex+1:]
|
||||
toolCallParts := make([]opencode.ToolPart, 0)
|
||||
|
||||
// sometimes tool calls happen without an assistant message
|
||||
// these should be included in this assistant message as well
|
||||
if len(orphanedToolCalls) > 0 {
|
||||
toolCallParts = append(toolCallParts, orphanedToolCalls...)
|
||||
orphanedToolCalls = make([]opencode.ToolInvocationPart, 0)
|
||||
orphanedToolCalls = make([]opencode.ToolPart, 0)
|
||||
}
|
||||
|
||||
remaining := true
|
||||
|
@ -226,9 +226,9 @@ func (m *messagesComponent) renderView(width int) {
|
|||
// we only want tool calls associated with the current text part.
|
||||
// if we hit another text part, we're done.
|
||||
remaining = false
|
||||
case opencode.ToolInvocationPart:
|
||||
case opencode.ToolPart:
|
||||
toolCallParts = append(toolCallParts, part)
|
||||
if part.ToolInvocation.State != "result" {
|
||||
if part.State.Status != opencode.ToolPartStateStatusCompleted || part.State.Status != opencode.ToolPartStateStatusError {
|
||||
// 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
|
||||
|
@ -237,14 +237,14 @@ func (m *messagesComponent) renderView(width int) {
|
|||
}
|
||||
|
||||
if finished {
|
||||
key := m.cache.GenerateKey(message.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount)
|
||||
key := m.cache.GenerateKey(casted.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount)
|
||||
content, cached = m.cache.Get(key)
|
||||
if !cached {
|
||||
content = renderText(
|
||||
m.app,
|
||||
message,
|
||||
p.Text,
|
||||
message.Metadata.Assistant.ModelID,
|
||||
casted.ModelID,
|
||||
m.showToolDetails,
|
||||
m.partCount == m.selectedPart,
|
||||
width,
|
||||
|
@ -258,7 +258,7 @@ func (m *messagesComponent) renderView(width int) {
|
|||
m.app,
|
||||
message,
|
||||
p.Text,
|
||||
message.Metadata.Assistant.ModelID,
|
||||
casted.ModelID,
|
||||
m.showToolDetails,
|
||||
m.partCount == m.selectedPart,
|
||||
width,
|
||||
|
@ -270,7 +270,7 @@ func (m *messagesComponent) renderView(width int) {
|
|||
m = m.updateSelected(content, p.Text)
|
||||
blocks = append(blocks, content)
|
||||
}
|
||||
case opencode.ToolInvocationPart:
|
||||
case opencode.ToolPart:
|
||||
if !m.showToolDetails {
|
||||
if !hasTextPart {
|
||||
orphanedToolCalls = append(orphanedToolCalls, part)
|
||||
|
@ -278,9 +278,9 @@ func (m *messagesComponent) renderView(width int) {
|
|||
continue
|
||||
}
|
||||
|
||||
if part.ToolInvocation.State == "result" {
|
||||
key := m.cache.GenerateKey(message.ID,
|
||||
part.ToolInvocation.ToolCallID,
|
||||
if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError {
|
||||
key := m.cache.GenerateKey(casted.ID,
|
||||
part.ID,
|
||||
m.showToolDetails,
|
||||
width,
|
||||
m.partCount == m.selectedPart,
|
||||
|
@ -290,7 +290,6 @@ func (m *messagesComponent) renderView(width int) {
|
|||
content = renderToolDetails(
|
||||
m.app,
|
||||
part,
|
||||
message.Metadata,
|
||||
m.partCount == m.selectedPart,
|
||||
width,
|
||||
)
|
||||
|
@ -301,7 +300,6 @@ func (m *messagesComponent) renderView(width int) {
|
|||
content = renderToolDetails(
|
||||
m.app,
|
||||
part,
|
||||
message.Metadata,
|
||||
m.partCount == m.selectedPart,
|
||||
width,
|
||||
)
|
||||
|
@ -315,14 +313,16 @@ func (m *messagesComponent) renderView(width int) {
|
|||
}
|
||||
|
||||
error := ""
|
||||
switch err := message.Metadata.Error.AsUnion().(type) {
|
||||
case nil:
|
||||
case opencode.MessageMetadataErrorMessageOutputLengthError:
|
||||
error = "Message output length exceeded"
|
||||
case opencode.ProviderAuthError:
|
||||
error = err.Data.Message
|
||||
case opencode.UnknownError:
|
||||
error = err.Data.Message
|
||||
if assistant, ok := message.(opencode.AssistantMessage); ok {
|
||||
switch err := assistant.Error.AsUnion().(type) {
|
||||
case nil:
|
||||
case opencode.AssistantMessageErrorMessageOutputLengthError:
|
||||
error = "Message output length exceeded"
|
||||
case opencode.ProviderAuthError:
|
||||
error = err.Data.Message
|
||||
case opencode.UnknownError:
|
||||
error = err.Data.Message
|
||||
}
|
||||
}
|
||||
|
||||
if error != "" {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
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/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
|
@ -101,18 +102,20 @@ func (m statusComponent) View() string {
|
|||
contextWindow := m.app.Model.Limit.Context
|
||||
|
||||
for _, message := range m.app.Messages {
|
||||
cost += message.Metadata.Assistant.Cost
|
||||
usage := message.Metadata.Assistant.Tokens
|
||||
if usage.Output > 0 {
|
||||
if message.Metadata.Assistant.Summary {
|
||||
tokens = usage.Output
|
||||
continue
|
||||
if assistant, ok := message.(opencode.AssistantMessage); ok {
|
||||
cost += assistant.Cost
|
||||
usage := assistant.Tokens
|
||||
if usage.Output > 0 {
|
||||
if assistant.Summary {
|
||||
tokens = usage.Output
|
||||
continue
|
||||
}
|
||||
tokens = (usage.Input +
|
||||
usage.Cache.Write +
|
||||
usage.Cache.Read +
|
||||
usage.Output +
|
||||
usage.Reasoning)
|
||||
}
|
||||
tokens = (usage.Input +
|
||||
usage.Cache.Write +
|
||||
usage.Cache.Read +
|
||||
usage.Output +
|
||||
usage.Reasoning)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -363,7 +363,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
case opencode.EventListResponseEventSessionDeleted:
|
||||
if a.app.Session != nil && msg.Properties.Info.ID == a.app.Session.ID {
|
||||
a.app.Session = &opencode.Session{}
|
||||
a.app.Messages = []opencode.Message{}
|
||||
a.app.Messages = []opencode.MessageUnion{}
|
||||
}
|
||||
return a, toast.NewSuccessToast("Session deleted successfully")
|
||||
case opencode.EventListResponseEventSessionUpdated:
|
||||
|
@ -371,7 +371,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
a.app.Session = &msg.Properties.Info
|
||||
}
|
||||
case opencode.EventListResponseEventMessageUpdated:
|
||||
if msg.Properties.Info.Metadata.SessionID == a.app.Session.ID {
|
||||
if msg.Properties.Info.SessionID == a.app.Session.ID {
|
||||
exists := false
|
||||
optimisticReplaced := false
|
||||
|
||||
|
@ -379,12 +379,15 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
if msg.Properties.Info.Role == opencode.MessageRoleUser {
|
||||
// Look for optimistic messages to replace
|
||||
for i, m := range a.app.Messages {
|
||||
if strings.HasPrefix(m.ID, "optimistic-") && m.Role == opencode.MessageRoleUser {
|
||||
// Replace the optimistic message with the real one
|
||||
a.app.Messages[i] = msg.Properties.Info
|
||||
exists = true
|
||||
optimisticReplaced = true
|
||||
break
|
||||
switch m := m.(type) {
|
||||
case opencode.UserMessage:
|
||||
if strings.HasPrefix(m.ID, "optimistic-") && m.Role == opencode.UserMessageRoleUser {
|
||||
// Replace the optimistic message with the real one
|
||||
a.app.Messages[i] = msg.Properties.Info.AsUnion()
|
||||
exists = true
|
||||
optimisticReplaced = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -392,8 +395,15 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
// If not replacing optimistic, check for existing message with same ID
|
||||
if !optimisticReplaced {
|
||||
for i, m := range a.app.Messages {
|
||||
if m.ID == msg.Properties.Info.ID {
|
||||
a.app.Messages[i] = msg.Properties.Info
|
||||
var id string
|
||||
switch m := m.(type) {
|
||||
case opencode.UserMessage:
|
||||
id = m.ID
|
||||
case opencode.AssistantMessage:
|
||||
id = m.ID
|
||||
}
|
||||
if id == msg.Properties.Info.ID {
|
||||
a.app.Messages[i] = msg.Properties.Info.AsUnion()
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
|
@ -401,7 +411,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
|
||||
if !exists {
|
||||
a.app.Messages = append(a.app.Messages, msg.Properties.Info)
|
||||
a.app.Messages = append(a.app.Messages, msg.Properties.Info.AsUnion())
|
||||
}
|
||||
}
|
||||
case opencode.EventListResponseEventSessionError:
|
||||
|
@ -462,7 +472,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return a, toast.NewErrorToast("Failed to open session")
|
||||
}
|
||||
a.app.Session = msg
|
||||
a.app.Messages = messages
|
||||
a.app.Messages = make([]opencode.MessageUnion, 0)
|
||||
for _, message := range messages {
|
||||
a.app.Messages = append(a.app.Messages, message.AsUnion())
|
||||
}
|
||||
return a, util.CmdHandler(app.SessionLoadedMsg{})
|
||||
case app.ModelSelectedMsg:
|
||||
a.app.Provider = &msg.Provider
|
||||
|
@ -813,7 +826,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
|||
return a, nil
|
||||
}
|
||||
a.app.Session = &opencode.Session{}
|
||||
a.app.Messages = []opencode.Message{}
|
||||
a.app.Messages = []opencode.MessageUnion{}
|
||||
cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{}))
|
||||
case commands.SessionListCommand:
|
||||
sessionDialog := dialog.NewSessionDialog(a.app)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
configured_endpoints: 20
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-15eeb028f79b9a065b4e54a6ea6a58631e9bd5004f97820f0c79d18e3f8bac84.yml
|
||||
openapi_spec_hash: 38c8bacb6c8e4c46852a3e81e3fb9fda
|
||||
config_hash: 348a85e725de595ca05a61f4333794ac
|
||||
config_hash: e03e9d1aad76081fa1163086e89f201b
|
||||
|
|
|
@ -88,6 +88,7 @@ Response Types:
|
|||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateError">ToolStateError</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStatePending">ToolStatePending</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunning">ToolStateRunning</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessage">UserMessage</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessagePart">UserMessagePart</a>
|
||||
|
||||
Methods:
|
||||
|
|
|
@ -577,7 +577,7 @@ type Message struct {
|
|||
Parts interface{} `json:"parts,required"`
|
||||
Role MessageRole `json:"role,required"`
|
||||
SessionID string `json:"sessionID,required"`
|
||||
// This field can have the runtime type of [MessageUserMessageTime],
|
||||
// This field can have the runtime type of [UserMessageTime],
|
||||
// [AssistantMessageTime].
|
||||
Time interface{} `json:"time,required"`
|
||||
Cost float64 `json:"cost"`
|
||||
|
@ -631,13 +631,12 @@ func (r *Message) UnmarshalJSON(data []byte) (err error) {
|
|||
// AsUnion returns a [MessageUnion] interface which you can cast to the specific
|
||||
// types for more type safety.
|
||||
//
|
||||
// Possible runtime types of the union are [MessageUserMessage],
|
||||
// [AssistantMessage].
|
||||
// Possible runtime types of the union are [UserMessage], [AssistantMessage].
|
||||
func (r Message) AsUnion() MessageUnion {
|
||||
return r.union
|
||||
}
|
||||
|
||||
// Union satisfied by [MessageUserMessage] or [AssistantMessage].
|
||||
// Union satisfied by [UserMessage] or [AssistantMessage].
|
||||
type MessageUnion interface {
|
||||
implementsMessage()
|
||||
}
|
||||
|
@ -648,7 +647,7 @@ func init() {
|
|||
"role",
|
||||
apijson.UnionVariant{
|
||||
TypeFilter: gjson.JSON,
|
||||
Type: reflect.TypeOf(MessageUserMessage{}),
|
||||
Type: reflect.TypeOf(UserMessage{}),
|
||||
DiscriminatorValue: "user",
|
||||
},
|
||||
apijson.UnionVariant{
|
||||
|
@ -659,72 +658,6 @@ func init() {
|
|||
)
|
||||
}
|
||||
|
||||
type MessageUserMessage struct {
|
||||
ID string `json:"id,required"`
|
||||
Parts []UserMessagePart `json:"parts,required"`
|
||||
Role MessageUserMessageRole `json:"role,required"`
|
||||
SessionID string `json:"sessionID,required"`
|
||||
Time MessageUserMessageTime `json:"time,required"`
|
||||
JSON messageUserMessageJSON `json:"-"`
|
||||
}
|
||||
|
||||
// messageUserMessageJSON contains the JSON metadata for the struct
|
||||
// [MessageUserMessage]
|
||||
type messageUserMessageJSON struct {
|
||||
ID apijson.Field
|
||||
Parts apijson.Field
|
||||
Role apijson.Field
|
||||
SessionID apijson.Field
|
||||
Time apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *MessageUserMessage) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r messageUserMessageJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
func (r MessageUserMessage) implementsMessage() {}
|
||||
|
||||
type MessageUserMessageRole string
|
||||
|
||||
const (
|
||||
MessageUserMessageRoleUser MessageUserMessageRole = "user"
|
||||
)
|
||||
|
||||
func (r MessageUserMessageRole) IsKnown() bool {
|
||||
switch r {
|
||||
case MessageUserMessageRoleUser:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type MessageUserMessageTime struct {
|
||||
Created float64 `json:"created,required"`
|
||||
JSON messageUserMessageTimeJSON `json:"-"`
|
||||
}
|
||||
|
||||
// messageUserMessageTimeJSON contains the JSON metadata for the struct
|
||||
// [MessageUserMessageTime]
|
||||
type messageUserMessageTimeJSON struct {
|
||||
Created apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *MessageUserMessageTime) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r messageUserMessageTimeJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type MessageRole string
|
||||
|
||||
const (
|
||||
|
@ -1306,6 +1239,70 @@ func (r toolStateRunningTimeJSON) RawJSON() string {
|
|||
return r.raw
|
||||
}
|
||||
|
||||
type UserMessage struct {
|
||||
ID string `json:"id,required"`
|
||||
Parts []UserMessagePart `json:"parts,required"`
|
||||
Role UserMessageRole `json:"role,required"`
|
||||
SessionID string `json:"sessionID,required"`
|
||||
Time UserMessageTime `json:"time,required"`
|
||||
JSON userMessageJSON `json:"-"`
|
||||
}
|
||||
|
||||
// userMessageJSON contains the JSON metadata for the struct [UserMessage]
|
||||
type userMessageJSON struct {
|
||||
ID apijson.Field
|
||||
Parts apijson.Field
|
||||
Role apijson.Field
|
||||
SessionID apijson.Field
|
||||
Time apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *UserMessage) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r userMessageJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
func (r UserMessage) implementsMessage() {}
|
||||
|
||||
type UserMessageRole string
|
||||
|
||||
const (
|
||||
UserMessageRoleUser UserMessageRole = "user"
|
||||
)
|
||||
|
||||
func (r UserMessageRole) IsKnown() bool {
|
||||
switch r {
|
||||
case UserMessageRoleUser:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type UserMessageTime struct {
|
||||
Created float64 `json:"created,required"`
|
||||
JSON userMessageTimeJSON `json:"-"`
|
||||
}
|
||||
|
||||
// userMessageTimeJSON contains the JSON metadata for the struct [UserMessageTime]
|
||||
type userMessageTimeJSON struct {
|
||||
Created apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *UserMessageTime) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r userMessageTimeJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type UserMessagePart struct {
|
||||
Type UserMessagePartType `json:"type,required"`
|
||||
Filename string `json:"filename"`
|
||||
|
|
|
@ -83,8 +83,9 @@ resources:
|
|||
toolPart: ToolPart
|
||||
stepStartPart: StepStartPart
|
||||
assistantMessage: AssistantMessage
|
||||
userMessagePart: UserMessagePart
|
||||
assistantMessagePart: AssistantMessagePart
|
||||
userMessage: UserMessage
|
||||
userMessagePart: UserMessagePart
|
||||
toolStatePending: ToolStatePending
|
||||
toolStateRunning: ToolStateRunning
|
||||
toolStateCompleted: ToolStateCompleted
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue