diff options
Diffstat (limited to 'packages/tui')
| -rw-r--r-- | packages/tui/internal/app/app.go | 109 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/messages.go | 43 | ||||
| -rw-r--r-- | packages/tui/internal/id/id.go | 96 | ||||
| -rw-r--r-- | packages/tui/internal/tui/tui.go | 103 | ||||
| -rw-r--r-- | packages/tui/sdk/.stats.yml | 6 | ||||
| -rw-r--r-- | packages/tui/sdk/README.md | 23 | ||||
| -rw-r--r-- | packages/tui/sdk/api.md | 8 | ||||
| -rw-r--r-- | packages/tui/sdk/client_test.go | 44 | ||||
| -rw-r--r-- | packages/tui/sdk/event.go | 8 | ||||
| -rwxr-xr-x | packages/tui/sdk/scripts/lint | 4 | ||||
| -rw-r--r-- | packages/tui/sdk/session.go | 683 | ||||
| -rw-r--r-- | packages/tui/sdk/session_test.go | 18 | ||||
| -rw-r--r-- | packages/tui/sdk/usage_test.go | 7 |
13 files changed, 690 insertions, 462 deletions
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 03eb361b7..fb8358d8c 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -16,11 +16,17 @@ import ( "github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/components/toast" "github.com/sst/opencode/internal/config" + "github.com/sst/opencode/internal/id" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" ) +type Message struct { + Info opencode.MessageUnion + Parts []opencode.PartUnion +} + type App struct { Info opencode.App Modes []opencode.Mode @@ -35,7 +41,7 @@ type App struct { Provider *opencode.Provider Model *opencode.Model Session *opencode.Session - Messages []opencode.MessageUnion + Messages []Message Commands commands.CommandRegistry InitialModel *string InitialPrompt *string @@ -158,7 +164,7 @@ func New( ModeIndex: modeIndex, Mode: mode, Session: &opencode.Session{}, - Messages: []opencode.MessageUnion{}, + Messages: []Message{}, Commands: commands.LoadFromConfig(configInfo), InitialModel: initialModel, InitialPrompt: initialPrompt, @@ -351,7 +357,7 @@ func (a *App) IsBusy() bool { } lastMessage := a.Messages[len(a.Messages)-1] - if casted, ok := lastMessage.(opencode.AssistantMessage); ok { + if casted, ok := lastMessage.Info.(opencode.AssistantMessage); ok { return casted.Time.Completed == 0 } return false @@ -452,54 +458,67 @@ func (a *App) SendChatMessage( cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session))) } - optimisticParts := []opencode.UserMessagePart{{ - Type: opencode.UserMessagePartTypeText, - Text: text, + message := opencode.UserMessage{ + ID: id.Ascending(id.Message), + SessionID: a.Session.ID, + Role: opencode.UserMessageRoleUser, + Time: opencode.UserMessageTime{ + Created: float64(time.Now().UnixMilli()), + }, + } + + parts := []opencode.PartUnion{opencode.TextPart{ + ID: id.Ascending(id.Part), + MessageID: message.ID, + SessionID: a.Session.ID, + Type: opencode.TextPartTypeText, + Text: text, }} if len(attachments) > 0 { for _, attachment := range attachments { - optimisticParts = append(optimisticParts, opencode.UserMessagePart{ - Type: opencode.UserMessagePartTypeFile, - Filename: attachment.Filename.Value, - Mime: attachment.Mime.Value, - URL: attachment.URL.Value, + parts = append(parts, opencode.FilePart{ + ID: id.Ascending(id.Part), + MessageID: message.ID, + SessionID: a.Session.ID, + Type: opencode.FilePartTypeFile, + Filename: attachment.Filename.Value, + Mime: attachment.Mime.Value, + URL: attachment.URL.Value, }) } } - 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()), - }, - } - - a.Messages = append(a.Messages, optimisticMessage) - cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage})) + a.Messages = append(a.Messages, Message{Info: message, Parts: parts}) + cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: message})) cmds = append(cmds, func() tea.Msg { - parts := []opencode.UserMessagePartUnionParam{ - opencode.TextPartParam{ - Type: opencode.F(opencode.TextPartTypeText), - Text: opencode.F(text), - }, - } - if len(attachments) > 0 { - for _, attachment := range attachments { - parts = append(parts, opencode.FilePartParam{ - Mime: attachment.Mime, - Type: attachment.Type, - URL: attachment.URL, - Filename: attachment.Filename, + partsParam := []opencode.SessionChatParamsPartUnion{} + for _, part := range parts { + switch casted := part.(type) { + case opencode.TextPart: + partsParam = append(partsParam, opencode.TextPartParam{ + ID: opencode.F(casted.ID), + MessageID: opencode.F(casted.MessageID), + SessionID: opencode.F(casted.SessionID), + Type: opencode.F(casted.Type), + Text: opencode.F(casted.Text), + }) + case opencode.FilePart: + partsParam = append(partsParam, opencode.FilePartParam{ + ID: opencode.F(casted.ID), + Mime: opencode.F(casted.Mime), + MessageID: opencode.F(casted.MessageID), + SessionID: opencode.F(casted.SessionID), + Type: opencode.F(casted.Type), + URL: opencode.F(casted.URL), + Filename: opencode.F(casted.Filename), }) } } _, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{ - Parts: opencode.F(parts), + Parts: opencode.F(partsParam), + MessageID: opencode.F(message.ID), ProviderID: opencode.F(a.Provider.ID), ModelID: opencode.F(a.Model.ID), Mode: opencode.F(a.Mode.Name), @@ -557,15 +576,25 @@ func (a *App) DeleteSession(ctx context.Context, sessionID string) error { return nil } -func (a *App) ListMessages(ctx context.Context, sessionId string) ([]opencode.Message, error) { +func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, error) { response, err := a.Client.Session.Messages(ctx, sessionId) if err != nil { return nil, err } if response == nil { - return []opencode.Message{}, nil + return []Message{}, nil + } + messages := []Message{} + for _, message := range *response { + msg := Message{ + Info: message.Info.AsUnion(), + Parts: []opencode.PartUnion{}, + } + for _, part := range message.Parts { + msg.Parts = append(msg.Parts, part.AsUnion()) + } + messages = append(messages, msg) } - messages := *response return messages, nil } diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 7ecd9b21f..191432cc3 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -106,6 +106,13 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.GotoBottom() } } + case opencode.EventListResponseEventMessagePartUpdated: + if msg.Properties.Part.SessionID == m.app.Session.ID { + m.renderView(m.width) + if m.tail { + m.viewport.GotoBottom() + } + } } viewport, cmd := m.viewport.Update(msg) @@ -131,16 +138,16 @@ func (m *messagesComponent) renderView(width int) { var content string var cached bool - switch casted := message.(type) { + switch casted := message.Info.(type) { case opencode.UserMessage: userLoop: - for partIndex, part := range casted.Parts { - switch part := part.AsUnion().(type) { + for partIndex, part := range message.Parts { + switch part := part.(type) { case opencode.TextPart: - remainingParts := casted.Parts[partIndex+1:] + remainingParts := message.Parts[partIndex+1:] fileParts := make([]opencode.FilePart, 0) for _, part := range remainingParts { - switch part := part.AsUnion().(type) { + switch part := part.(type) { case opencode.FilePart: fileParts = append(fileParts, part) } @@ -181,7 +188,7 @@ func (m *messagesComponent) renderView(width int) { if !cached { content = renderText( m.app, - message, + message.Info, part.Text, m.app.Info.User, m.showToolDetails, @@ -202,12 +209,12 @@ func (m *messagesComponent) renderView(width int) { case opencode.AssistantMessage: hasTextPart := false - for partIndex, p := range casted.Parts { - switch part := p.AsUnion().(type) { + for partIndex, p := range message.Parts { + switch part := p.(type) { case opencode.TextPart: hasTextPart = true finished := casted.Time.Completed > 0 - remainingParts := casted.Parts[partIndex+1:] + remainingParts := message.Parts[partIndex+1:] toolCallParts := make([]opencode.ToolPart, 0) // sometimes tool calls happen without an assistant message @@ -222,7 +229,7 @@ func (m *messagesComponent) renderView(width int) { if !remaining { break } - switch part := part.AsUnion().(type) { + switch part := part.(type) { case opencode.TextPart: // we only want tool calls associated with the current text part. // if we hit another text part, we're done. @@ -238,13 +245,13 @@ func (m *messagesComponent) renderView(width int) { } if finished { - key := m.cache.GenerateKey(casted.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount) + key := m.cache.GenerateKey(casted.ID, part.Text, width, m.showToolDetails, m.selectedPart == m.partCount) content, cached = m.cache.Get(key) if !cached { content = renderText( m.app, - message, - p.Text, + message.Info, + part.Text, casted.ModelID, m.showToolDetails, m.partCount == m.selectedPart, @@ -257,8 +264,8 @@ func (m *messagesComponent) renderView(width int) { } else { content = renderText( m.app, - message, - p.Text, + message.Info, + part.Text, casted.ModelID, m.showToolDetails, m.partCount == m.selectedPart, @@ -268,7 +275,7 @@ func (m *messagesComponent) renderView(width int) { ) } if content != "" { - m = m.updateSelected(content, p.Text) + m = m.updateSelected(content, part.Text) blocks = append(blocks, content) } case opencode.ToolPart: @@ -314,7 +321,7 @@ func (m *messagesComponent) renderView(width int) { } error := "" - if assistant, ok := message.(opencode.AssistantMessage); ok { + if assistant, ok := message.Info.(opencode.AssistantMessage); ok { switch err := assistant.Error.AsUnion().(type) { case nil: case opencode.AssistantMessageErrorMessageOutputLengthError: @@ -386,7 +393,7 @@ func (m *messagesComponent) header(width int) string { contextWindow := m.app.Model.Limit.Context for _, message := range m.app.Messages { - if assistant, ok := message.(opencode.AssistantMessage); ok { + if assistant, ok := message.Info.(opencode.AssistantMessage); ok { cost += assistant.Cost usage := assistant.Tokens if usage.Output > 0 { diff --git a/packages/tui/internal/id/id.go b/packages/tui/internal/id/id.go new file mode 100644 index 000000000..0490b8f20 --- /dev/null +++ b/packages/tui/internal/id/id.go @@ -0,0 +1,96 @@ +package id + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "strings" + "sync" + "time" +) + +const ( + PrefixSession = "ses" + PrefixMessage = "msg" + PrefixUser = "usr" + PrefixPart = "prt" +) + +const length = 26 + +var ( + lastTimestamp int64 + counter int64 + mu sync.Mutex +) + +type Prefix string + +const ( + Session Prefix = PrefixSession + Message Prefix = PrefixMessage + User Prefix = PrefixUser + Part Prefix = PrefixPart +) + +func ValidatePrefix(id string, prefix Prefix) bool { + return strings.HasPrefix(id, string(prefix)) +} + +func Ascending(prefix Prefix, given ...string) string { + return generateID(prefix, false, given...) +} + +func Descending(prefix Prefix, given ...string) string { + return generateID(prefix, true, given...) +} + +func generateID(prefix Prefix, descending bool, given ...string) string { + if len(given) > 0 && given[0] != "" { + if !strings.HasPrefix(given[0], string(prefix)) { + panic(fmt.Sprintf("ID %s does not start with %s", given[0], string(prefix))) + } + return given[0] + } + + return generateNewID(prefix, descending) +} + +func randomBase62(length int) string { + const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + result := make([]byte, length) + bytes := make([]byte, length) + rand.Read(bytes) + + for i := 0; i < length; i++ { + result[i] = chars[bytes[i]%62] + } + + return string(result) +} + +func generateNewID(prefix Prefix, descending bool) string { + mu.Lock() + defer mu.Unlock() + + currentTimestamp := time.Now().UnixMilli() + + if currentTimestamp != lastTimestamp { + lastTimestamp = currentTimestamp + counter = 0 + } + counter++ + + now := uint64(currentTimestamp)*0x1000 + uint64(counter) + + if descending { + now = ^now + } + + timeBytes := make([]byte, 6) + for i := 0; i < 6; i++ { + timeBytes[i] = byte((now >> (40 - 8*i)) & 0xff) + } + + return string(prefix) + "_" + hex.EncodeToString(timeBytes) + randomBase62(length-12) +}
\ No newline at end of file diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 389dd64f1..0ebdd35ab 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -5,6 +5,7 @@ import ( "log/slog" "os" "os/exec" + "slices" "strings" "time" @@ -364,55 +365,76 @@ 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.MessageUnion{} + a.app.Messages = []app.Message{} } return a, toast.NewSuccessToast("Session deleted successfully") case opencode.EventListResponseEventSessionUpdated: if msg.Properties.Info.ID == a.app.Session.ID { a.app.Session = &msg.Properties.Info } - case opencode.EventListResponseEventMessageUpdated: - if msg.Properties.Info.SessionID == a.app.Session.ID { - exists := false - optimisticReplaced := false - - // First check if this is replacing an optimistic message - if msg.Properties.Info.Role == opencode.MessageRoleUser { - // Look for optimistic messages to replace - for i, m := range a.app.Messages { - 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 - } + case opencode.EventListResponseEventMessagePartUpdated: + slog.Info("message part updated", "message", msg.Properties.Part.MessageID, "part", msg.Properties.Part.ID) + if msg.Properties.Part.SessionID == a.app.Session.ID { + messageIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool { + switch casted := m.Info.(type) { + case opencode.UserMessage: + return casted.ID == msg.Properties.Part.MessageID + case opencode.AssistantMessage: + return casted.ID == msg.Properties.Part.MessageID + } + return false + }) + if messageIndex > -1 { + message := a.app.Messages[messageIndex] + partIndex := slices.IndexFunc(message.Parts, func(p opencode.PartUnion) bool { + switch casted := p.(type) { + case opencode.TextPart: + return casted.ID == msg.Properties.Part.ID + case opencode.FilePart: + return casted.ID == msg.Properties.Part.ID + case opencode.ToolPart: + return casted.ID == msg.Properties.Part.ID + case opencode.StepStartPart: + return casted.ID == msg.Properties.Part.ID + case opencode.StepFinishPart: + return casted.ID == msg.Properties.Part.ID } + return false + }) + if partIndex > -1 { + message.Parts[partIndex] = msg.Properties.Part.AsUnion() } + if partIndex == -1 { + message.Parts = append(message.Parts, msg.Properties.Part.AsUnion()) + } + a.app.Messages[messageIndex] = message } - - // If not replacing optimistic, check for existing message with same ID - if !optimisticReplaced { - for i, m := range a.app.Messages { - 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 - } + } + case opencode.EventListResponseEventMessageUpdated: + if msg.Properties.Info.SessionID == a.app.Session.ID { + matchIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool { + switch casted := m.Info.(type) { + case opencode.UserMessage: + return casted.ID == msg.Properties.Info.ID + case opencode.AssistantMessage: + return casted.ID == msg.Properties.Info.ID + } + return false + }) + + if matchIndex > -1 { + match := a.app.Messages[matchIndex] + a.app.Messages[matchIndex] = app.Message{ + Info: msg.Properties.Info.AsUnion(), + Parts: match.Parts, } } - if !exists { - a.app.Messages = append(a.app.Messages, msg.Properties.Info.AsUnion()) + if matchIndex == -1 { + a.app.Messages = append(a.app.Messages, app.Message{ + Info: msg.Properties.Info.AsUnion(), + Parts: []opencode.PartUnion{}, + }) } } case opencode.EventListResponseEventSessionError: @@ -473,10 +495,7 @@ 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 = make([]opencode.MessageUnion, 0) - for _, message := range messages { - a.app.Messages = append(a.app.Messages, message.AsUnion()) - } + a.app.Messages = messages return a, util.CmdHandler(app.SessionLoadedMsg{}) case app.ModelSelectedMsg: a.app.Provider = &msg.Provider @@ -837,7 +856,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) return a, nil } a.app.Session = &opencode.Session{} - a.app.Messages = []opencode.MessageUnion{} + a.app.Messages = []app.Message{} cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{})) case commands.SessionListCommand: sessionDialog := dialog.NewSessionDialog(a.app) diff --git a/packages/tui/sdk/.stats.yml b/packages/tui/sdk/.stats.yml index 4449c4d95..61d2fa3b7 100644 --- a/packages/tui/sdk/.stats.yml +++ b/packages/tui/sdk/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 22 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-eb25bb3673f94d0e98a7036e2a2b0ed7ad63d1598665f2d5e091ec0835273798.yml -openapi_spec_hash: 62f6a8a06aaa4f4ae13e85d56652724f -config_hash: 589ec6a935a43a3c49a325ece86cbda2 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-352994eb17f76d9472b0f0176efacf77a200a6fab2db28d1cfcd29451b211d7a.yml +openapi_spec_hash: f01cd3de8c7cf0c9fd513896e81986de +config_hash: 3695cfc829cfaae14490850b4a1ed282 diff --git a/packages/tui/sdk/README.md b/packages/tui/sdk/README.md index 2b5782347..38840b28a 100644 --- a/packages/tui/sdk/README.md +++ b/packages/tui/sdk/README.md @@ -49,11 +49,14 @@ import ( func main() { client := opencode.NewClient() - events, err := client.Event.List(context.TODO()) + stream := client.Event.ListStreaming(context.TODO()) + for stream.Next() { + fmt.Printf("%+v\n", stream.Current()) + } + err := stream.Err() if err != nil { panic(err.Error()) } - fmt.Printf("%+v\n", events) } ``` @@ -171,14 +174,14 @@ When the API returns a non-success status code, we return an error with type To handle errors, we recommend that you use the `errors.As` pattern: ```go -_, err := client.Event.List(context.TODO()) -if err != nil { +stream := client.Event.ListStreaming(context.TODO()) +if stream.Err() != nil { var apierr *opencode.Error - if errors.As(err, &apierr) { + if errors.As(stream.Err(), &apierr) { println(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response } - panic(err.Error()) // GET "/event": 400 Bad Request { ... } + panic(stream.Err().Error()) // GET "/event": 400 Bad Request { ... } } ``` @@ -196,7 +199,7 @@ To set a per-retry timeout, use `option.WithRequestTimeout()`. // This sets the timeout for the request, including all the retries. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() -client.Event.List( +client.Event.ListStreaming( ctx, // This sets the per-retry timeout option.WithRequestTimeout(20*time.Second), @@ -231,7 +234,7 @@ client := opencode.NewClient( ) // Override per-request: -client.Event.List(context.TODO(), option.WithMaxRetries(5)) +client.Event.ListStreaming(context.TODO(), option.WithMaxRetries(5)) ``` ### Accessing raw response data (e.g. response headers) @@ -242,8 +245,8 @@ you need to examine response headers, status codes, or other details. ```go // Create a variable to store the HTTP response var response *http.Response -events, err := client.Event.List(context.TODO(), option.WithResponseInto(&response)) -if err != nil { +stream := client.Event.ListStreaming(context.TODO(), option.WithResponseInto(&response)) +if stream.Err() != nil { // handle error } fmt.Printf("%+v\n", events) diff --git a/packages/tui/sdk/api.md b/packages/tui/sdk/api.md index 15bdcfa93..a48e6d7f1 100644 --- a/packages/tui/sdk/api.md +++ b/packages/tui/sdk/api.md @@ -77,15 +77,15 @@ Params 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#FilePartParam">FilePartParam</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#TextPartParam">TextPartParam</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#UserMessagePartUnionParam">UserMessagePartUnionParam</a> 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#AssistantMessage">AssistantMessage</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#AssistantMessagePart">AssistantMessagePart</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#FilePart">FilePart</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#Message">Message</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#Part">Part</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#Session">Session</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#StepFinishPart">StepFinishPart</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#StepStartPart">StepStartPart</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#TextPart">TextPart</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#ToolPart">ToolPart</a> @@ -94,7 +94,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#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> +- <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#SessionMessagesResponse">SessionMessagesResponse</a> Methods: @@ -104,7 +104,7 @@ Methods: - <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <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#SessionChatParams">SessionChatParams</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#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <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#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> -- <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</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#Message">Message</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> +- <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</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#SessionMessagesResponse">SessionMessagesResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</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#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="post /session/{id}/summarize">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Summarize">Summarize</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <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#SessionSummarizeParams">SessionSummarizeParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="delete /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unshare">Unshare</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</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#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> diff --git a/packages/tui/sdk/client_test.go b/packages/tui/sdk/client_test.go index e75d64925..62222a465 100644 --- a/packages/tui/sdk/client_test.go +++ b/packages/tui/sdk/client_test.go @@ -38,7 +38,7 @@ func TestUserAgentHeader(t *testing.T) { }, }), ) - client.Event.List(context.Background()) + client.Event.ListStreaming(context.Background()) if userAgent != fmt.Sprintf("Opencode/Go %s", internal.PackageVersion) { t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent) } @@ -61,7 +61,11 @@ func TestRetryAfter(t *testing.T) { }, }), ) - _, err := client.Event.List(context.Background()) + stream := client.Event.ListStreaming(context.Background()) + for stream.Next() { + // ... + } + err := stream.Err() if err == nil { t.Error("Expected there to be a cancel error") } @@ -95,7 +99,11 @@ func TestDeleteRetryCountHeader(t *testing.T) { }), option.WithHeaderDel("X-Stainless-Retry-Count"), ) - _, err := client.Event.List(context.Background()) + stream := client.Event.ListStreaming(context.Background()) + for stream.Next() { + // ... + } + err := stream.Err() if err == nil { t.Error("Expected there to be a cancel error") } @@ -124,7 +132,11 @@ func TestOverwriteRetryCountHeader(t *testing.T) { }), option.WithHeader("X-Stainless-Retry-Count", "42"), ) - _, err := client.Event.List(context.Background()) + stream := client.Event.ListStreaming(context.Background()) + for stream.Next() { + // ... + } + err := stream.Err() if err == nil { t.Error("Expected there to be a cancel error") } @@ -152,7 +164,11 @@ func TestRetryAfterMs(t *testing.T) { }, }), ) - _, err := client.Event.List(context.Background()) + stream := client.Event.ListStreaming(context.Background()) + for stream.Next() { + // ... + } + err := stream.Err() if err == nil { t.Error("Expected there to be a cancel error") } @@ -174,7 +190,11 @@ func TestContextCancel(t *testing.T) { ) cancelCtx, cancel := context.WithCancel(context.Background()) cancel() - _, err := client.Event.List(cancelCtx) + stream := client.Event.ListStreaming(cancelCtx) + for stream.Next() { + // ... + } + err := stream.Err() if err == nil { t.Error("Expected there to be a cancel error") } @@ -193,7 +213,11 @@ func TestContextCancelDelay(t *testing.T) { ) cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond) defer cancel() - _, err := client.Event.List(cancelCtx) + stream := client.Event.ListStreaming(cancelCtx) + for stream.Next() { + // ... + } + err := stream.Err() if err == nil { t.Error("expected there to be a cancel error") } @@ -218,7 +242,11 @@ func TestContextDeadline(t *testing.T) { }, }), ) - _, err := client.Event.List(deadlineCtx) + stream := client.Event.ListStreaming(deadlineCtx) + for stream.Next() { + // ... + } + err := stream.Err() if err == nil { t.Error("expected there to be a deadline error") } diff --git a/packages/tui/sdk/event.go b/packages/tui/sdk/event.go index 8bbf636c3..9002d2aac 100644 --- a/packages/tui/sdk/event.go +++ b/packages/tui/sdk/event.go @@ -610,18 +610,14 @@ func (r eventListResponseEventMessagePartUpdatedJSON) RawJSON() string { func (r EventListResponseEventMessagePartUpdated) implementsEventListResponse() {} type EventListResponseEventMessagePartUpdatedProperties struct { - MessageID string `json:"messageID,required"` - Part AssistantMessagePart `json:"part,required"` - SessionID string `json:"sessionID,required"` - JSON eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"` + Part Part `json:"part,required"` + JSON eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"` } // eventListResponseEventMessagePartUpdatedPropertiesJSON contains the JSON // metadata for the struct [EventListResponseEventMessagePartUpdatedProperties] type eventListResponseEventMessagePartUpdatedPropertiesJSON struct { - MessageID apijson.Field Part apijson.Field - SessionID apijson.Field raw string ExtraFields map[string]apijson.Field } diff --git a/packages/tui/sdk/scripts/lint b/packages/tui/sdk/scripts/lint index 9f37abf28..7e03a7beb 100755 --- a/packages/tui/sdk/scripts/lint +++ b/packages/tui/sdk/scripts/lint @@ -5,7 +5,7 @@ set -e cd "$(dirname "$0")/.." echo "==> Running Go build" -go build . +go build ./... echo "==> Checking tests compile" -go test -run=^$ . +go test -run=^$ ./... diff --git a/packages/tui/sdk/session.go b/packages/tui/sdk/session.go index 6321d1ff6..e76ab7f90 100644 --- a/packages/tui/sdk/session.go +++ b/packages/tui/sdk/session.go @@ -101,7 +101,7 @@ func (r *SessionService) Init(ctx context.Context, id string, body SessionInitPa } // List messages for a session -func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]Message, err error) { +func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]SessionMessagesResponse, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") @@ -152,7 +152,6 @@ type AssistantMessage struct { ID string `json:"id,required"` Cost float64 `json:"cost,required"` ModelID string `json:"modelID,required"` - Parts []AssistantMessagePart `json:"parts,required"` Path AssistantMessagePath `json:"path,required"` ProviderID string `json:"providerID,required"` Role AssistantMessageRole `json:"role,required"` @@ -171,7 +170,6 @@ type assistantMessageJSON struct { ID apijson.Field Cost apijson.Field ModelID apijson.Field - Parts apijson.Field Path apijson.Field ProviderID apijson.Field Role apijson.Field @@ -435,211 +433,23 @@ func (r AssistantMessageErrorName) IsKnown() bool { return false } -type AssistantMessagePart struct { - Type AssistantMessagePartType `json:"type,required"` - ID string `json:"id"` - Cost float64 `json:"cost"` - // This field can have the runtime type of [ToolPartState]. - State interface{} `json:"state"` - Synthetic bool `json:"synthetic"` - Text string `json:"text"` - // This field can have the runtime type of - // [AssistantMessagePartStepFinishPartTokens]. - Tokens interface{} `json:"tokens"` - Tool string `json:"tool"` - JSON assistantMessagePartJSON `json:"-"` - union AssistantMessagePartUnion -} - -// assistantMessagePartJSON contains the JSON metadata for the struct -// [AssistantMessagePart] -type assistantMessagePartJSON struct { - Type apijson.Field - ID apijson.Field - Cost apijson.Field - State apijson.Field - Synthetic apijson.Field - Text apijson.Field - Tokens apijson.Field - Tool apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r assistantMessagePartJSON) RawJSON() string { - return r.raw -} - -func (r *AssistantMessagePart) UnmarshalJSON(data []byte) (err error) { - *r = AssistantMessagePart{} - err = apijson.UnmarshalRoot(data, &r.union) - if err != nil { - return err - } - return apijson.Port(r.union, &r) -} - -// AsUnion returns a [AssistantMessagePartUnion] interface which you can cast to -// the specific types for more type safety. -// -// Possible runtime types of the union are [TextPart], [ToolPart], [StepStartPart], -// [AssistantMessagePartStepFinishPart]. -func (r AssistantMessagePart) AsUnion() AssistantMessagePartUnion { - return r.union -} - -// Union satisfied by [TextPart], [ToolPart], [StepStartPart] or -// [AssistantMessagePartStepFinishPart]. -type AssistantMessagePartUnion interface { - implementsAssistantMessagePart() -} - -func init() { - apijson.RegisterUnion( - reflect.TypeOf((*AssistantMessagePartUnion)(nil)).Elem(), - "type", - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(TextPart{}), - DiscriminatorValue: "text", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(ToolPart{}), - DiscriminatorValue: "tool", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(StepStartPart{}), - DiscriminatorValue: "step-start", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(AssistantMessagePartStepFinishPart{}), - DiscriminatorValue: "step-finish", - }, - ) -} - -type AssistantMessagePartStepFinishPart struct { - Cost float64 `json:"cost,required"` - Tokens AssistantMessagePartStepFinishPartTokens `json:"tokens,required"` - Type AssistantMessagePartStepFinishPartType `json:"type,required"` - JSON assistantMessagePartStepFinishPartJSON `json:"-"` -} - -// assistantMessagePartStepFinishPartJSON contains the JSON metadata for the struct -// [AssistantMessagePartStepFinishPart] -type assistantMessagePartStepFinishPartJSON struct { - Cost apijson.Field - Tokens apijson.Field - Type apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AssistantMessagePartStepFinishPart) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r assistantMessagePartStepFinishPartJSON) RawJSON() string { - return r.raw -} - -func (r AssistantMessagePartStepFinishPart) implementsAssistantMessagePart() {} - -type AssistantMessagePartStepFinishPartTokens struct { - Cache AssistantMessagePartStepFinishPartTokensCache `json:"cache,required"` - Input float64 `json:"input,required"` - Output float64 `json:"output,required"` - Reasoning float64 `json:"reasoning,required"` - JSON assistantMessagePartStepFinishPartTokensJSON `json:"-"` -} - -// assistantMessagePartStepFinishPartTokensJSON contains the JSON metadata for the -// struct [AssistantMessagePartStepFinishPartTokens] -type assistantMessagePartStepFinishPartTokensJSON struct { - Cache apijson.Field - Input apijson.Field - Output apijson.Field - Reasoning apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AssistantMessagePartStepFinishPartTokens) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r assistantMessagePartStepFinishPartTokensJSON) RawJSON() string { - return r.raw -} - -type AssistantMessagePartStepFinishPartTokensCache struct { - Read float64 `json:"read,required"` - Write float64 `json:"write,required"` - JSON assistantMessagePartStepFinishPartTokensCacheJSON `json:"-"` -} - -// assistantMessagePartStepFinishPartTokensCacheJSON contains the JSON metadata for -// the struct [AssistantMessagePartStepFinishPartTokensCache] -type assistantMessagePartStepFinishPartTokensCacheJSON struct { - Read apijson.Field - Write apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AssistantMessagePartStepFinishPartTokensCache) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r assistantMessagePartStepFinishPartTokensCacheJSON) RawJSON() string { - return r.raw -} - -type AssistantMessagePartStepFinishPartType string - -const ( - AssistantMessagePartStepFinishPartTypeStepFinish AssistantMessagePartStepFinishPartType = "step-finish" -) - -func (r AssistantMessagePartStepFinishPartType) IsKnown() bool { - switch r { - case AssistantMessagePartStepFinishPartTypeStepFinish: - return true - } - return false -} - -type AssistantMessagePartType string - -const ( - AssistantMessagePartTypeText AssistantMessagePartType = "text" - AssistantMessagePartTypeTool AssistantMessagePartType = "tool" - AssistantMessagePartTypeStepStart AssistantMessagePartType = "step-start" - AssistantMessagePartTypeStepFinish AssistantMessagePartType = "step-finish" -) - -func (r AssistantMessagePartType) IsKnown() bool { - switch r { - case AssistantMessagePartTypeText, AssistantMessagePartTypeTool, AssistantMessagePartTypeStepStart, AssistantMessagePartTypeStepFinish: - return true - } - return false -} - type FilePart struct { - Mime string `json:"mime,required"` - Type FilePartType `json:"type,required"` - URL string `json:"url,required"` - Filename string `json:"filename"` - JSON filePartJSON `json:"-"` + ID string `json:"id,required"` + MessageID string `json:"messageID,required"` + Mime string `json:"mime,required"` + SessionID string `json:"sessionID,required"` + Type FilePartType `json:"type,required"` + URL string `json:"url,required"` + Filename string `json:"filename"` + JSON filePartJSON `json:"-"` } // filePartJSON contains the JSON metadata for the struct [FilePart] type filePartJSON struct { + ID apijson.Field + MessageID apijson.Field Mime apijson.Field + SessionID apijson.Field Type apijson.Field URL apijson.Field Filename apijson.Field @@ -655,7 +465,7 @@ func (r filePartJSON) RawJSON() string { return r.raw } -func (r FilePart) implementsUserMessagePart() {} +func (r FilePart) implementsPart() {} type FilePartType string @@ -672,23 +482,23 @@ func (r FilePartType) IsKnown() bool { } type FilePartParam struct { - Mime param.Field[string] `json:"mime,required"` - Type param.Field[FilePartType] `json:"type,required"` - URL param.Field[string] `json:"url,required"` - Filename param.Field[string] `json:"filename"` + ID param.Field[string] `json:"id,required"` + MessageID param.Field[string] `json:"messageID,required"` + Mime param.Field[string] `json:"mime,required"` + SessionID param.Field[string] `json:"sessionID,required"` + Type param.Field[FilePartType] `json:"type,required"` + URL param.Field[string] `json:"url,required"` + Filename param.Field[string] `json:"filename"` } func (r FilePartParam) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } -func (r FilePartParam) implementsUserMessagePartUnionParam() {} +func (r FilePartParam) implementsSessionChatParamsPartUnion() {} type Message struct { - ID string `json:"id,required"` - // This field can have the runtime type of [[]UserMessagePart], - // [[]AssistantMessagePart]. - Parts interface{} `json:"parts,required"` + ID string `json:"id,required"` Role MessageRole `json:"role,required"` SessionID string `json:"sessionID,required"` // This field can have the runtime type of [UserMessageTime], @@ -713,7 +523,6 @@ type Message struct { // messageJSON contains the JSON metadata for the struct [Message] type messageJSON struct { ID apijson.Field - Parts apijson.Field Role apijson.Field SessionID apijson.Field Time apijson.Field @@ -787,6 +596,128 @@ func (r MessageRole) IsKnown() bool { return false } +type Part struct { + ID string `json:"id,required"` + MessageID string `json:"messageID,required"` + SessionID string `json:"sessionID,required"` + Type PartType `json:"type,required"` + CallID string `json:"callID"` + Cost float64 `json:"cost"` + Filename string `json:"filename"` + Mime string `json:"mime"` + // This field can have the runtime type of [ToolPartState]. + State interface{} `json:"state"` + Synthetic bool `json:"synthetic"` + Text string `json:"text"` + // This field can have the runtime type of [TextPartTime]. + Time interface{} `json:"time"` + // This field can have the runtime type of [StepFinishPartTokens]. + Tokens interface{} `json:"tokens"` + Tool string `json:"tool"` + URL string `json:"url"` + JSON partJSON `json:"-"` + union PartUnion +} + +// partJSON contains the JSON metadata for the struct [Part] +type partJSON struct { + ID apijson.Field + MessageID apijson.Field + SessionID apijson.Field + Type apijson.Field + CallID apijson.Field + Cost apijson.Field + Filename apijson.Field + Mime apijson.Field + State apijson.Field + Synthetic apijson.Field + Text apijson.Field + Time apijson.Field + Tokens apijson.Field + Tool apijson.Field + URL apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r partJSON) RawJSON() string { + return r.raw +} + +func (r *Part) UnmarshalJSON(data []byte) (err error) { + *r = Part{} + err = apijson.UnmarshalRoot(data, &r.union) + if err != nil { + return err + } + return apijson.Port(r.union, &r) +} + +// AsUnion returns a [PartUnion] interface which you can cast to the specific types +// for more type safety. +// +// Possible runtime types of the union are [TextPart], [FilePart], [ToolPart], +// [StepStartPart], [StepFinishPart]. +func (r Part) AsUnion() PartUnion { + return r.union +} + +// Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart] or +// [StepFinishPart]. +type PartUnion interface { + implementsPart() +} + +func init() { + apijson.RegisterUnion( + reflect.TypeOf((*PartUnion)(nil)).Elem(), + "type", + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(TextPart{}), + DiscriminatorValue: "text", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(FilePart{}), + DiscriminatorValue: "file", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ToolPart{}), + DiscriminatorValue: "tool", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(StepStartPart{}), + DiscriminatorValue: "step-start", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(StepFinishPart{}), + DiscriminatorValue: "step-finish", + }, + ) +} + +type PartType string + +const ( + PartTypeText PartType = "text" + PartTypeFile PartType = "file" + PartTypeTool PartType = "tool" + PartTypeStepStart PartType = "step-start" + PartTypeStepFinish PartType = "step-finish" +) + +func (r PartType) IsKnown() bool { + switch r { + case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish: + return true + } + return false +} + type Session struct { ID string `json:"id,required"` Time SessionTime `json:"time,required"` @@ -885,13 +816,115 @@ func (r sessionShareJSON) RawJSON() string { return r.raw } +type StepFinishPart struct { + ID string `json:"id,required"` + Cost float64 `json:"cost,required"` + MessageID string `json:"messageID,required"` + SessionID string `json:"sessionID,required"` + Tokens StepFinishPartTokens `json:"tokens,required"` + Type StepFinishPartType `json:"type,required"` + JSON stepFinishPartJSON `json:"-"` +} + +// stepFinishPartJSON contains the JSON metadata for the struct [StepFinishPart] +type stepFinishPartJSON struct { + ID apijson.Field + Cost apijson.Field + MessageID apijson.Field + SessionID apijson.Field + Tokens apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *StepFinishPart) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r stepFinishPartJSON) RawJSON() string { + return r.raw +} + +func (r StepFinishPart) implementsPart() {} + +type StepFinishPartTokens struct { + Cache StepFinishPartTokensCache `json:"cache,required"` + Input float64 `json:"input,required"` + Output float64 `json:"output,required"` + Reasoning float64 `json:"reasoning,required"` + JSON stepFinishPartTokensJSON `json:"-"` +} + +// stepFinishPartTokensJSON contains the JSON metadata for the struct +// [StepFinishPartTokens] +type stepFinishPartTokensJSON struct { + Cache apijson.Field + Input apijson.Field + Output apijson.Field + Reasoning apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *StepFinishPartTokens) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r stepFinishPartTokensJSON) RawJSON() string { + return r.raw +} + +type StepFinishPartTokensCache struct { + Read float64 `json:"read,required"` + Write float64 `json:"write,required"` + JSON stepFinishPartTokensCacheJSON `json:"-"` +} + +// stepFinishPartTokensCacheJSON contains the JSON metadata for the struct +// [StepFinishPartTokensCache] +type stepFinishPartTokensCacheJSON struct { + Read apijson.Field + Write apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *StepFinishPartTokensCache) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r stepFinishPartTokensCacheJSON) RawJSON() string { + return r.raw +} + +type StepFinishPartType string + +const ( + StepFinishPartTypeStepFinish StepFinishPartType = "step-finish" +) + +func (r StepFinishPartType) IsKnown() bool { + switch r { + case StepFinishPartTypeStepFinish: + return true + } + return false +} + type StepStartPart struct { - Type StepStartPartType `json:"type,required"` - JSON stepStartPartJSON `json:"-"` + ID string `json:"id,required"` + MessageID string `json:"messageID,required"` + SessionID string `json:"sessionID,required"` + Type StepStartPartType `json:"type,required"` + JSON stepStartPartJSON `json:"-"` } // stepStartPartJSON contains the JSON metadata for the struct [StepStartPart] type stepStartPartJSON struct { + ID apijson.Field + MessageID apijson.Field + SessionID apijson.Field Type apijson.Field raw string ExtraFields map[string]apijson.Field @@ -905,7 +938,7 @@ func (r stepStartPartJSON) RawJSON() string { return r.raw } -func (r StepStartPart) implementsAssistantMessagePart() {} +func (r StepStartPart) implementsPart() {} type StepStartPartType string @@ -922,17 +955,25 @@ func (r StepStartPartType) IsKnown() bool { } type TextPart struct { + ID string `json:"id,required"` + MessageID string `json:"messageID,required"` + SessionID string `json:"sessionID,required"` Text string `json:"text,required"` Type TextPartType `json:"type,required"` Synthetic bool `json:"synthetic"` + Time TextPartTime `json:"time"` JSON textPartJSON `json:"-"` } // textPartJSON contains the JSON metadata for the struct [TextPart] type textPartJSON struct { + ID apijson.Field + MessageID apijson.Field + SessionID apijson.Field Text apijson.Field Type apijson.Field Synthetic apijson.Field + Time apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -945,9 +986,7 @@ func (r textPartJSON) RawJSON() string { return r.raw } -func (r TextPart) implementsAssistantMessagePart() {} - -func (r TextPart) implementsUserMessagePart() {} +func (r TextPart) implementsPart() {} type TextPartType string @@ -963,29 +1002,70 @@ func (r TextPartType) IsKnown() bool { return false } +type TextPartTime struct { + Start float64 `json:"start,required"` + End float64 `json:"end"` + JSON textPartTimeJSON `json:"-"` +} + +// textPartTimeJSON contains the JSON metadata for the struct [TextPartTime] +type textPartTimeJSON struct { + Start apijson.Field + End apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *TextPartTime) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r textPartTimeJSON) RawJSON() string { + return r.raw +} + type TextPartParam struct { - Text param.Field[string] `json:"text,required"` - Type param.Field[TextPartType] `json:"type,required"` - Synthetic param.Field[bool] `json:"synthetic"` + ID param.Field[string] `json:"id,required"` + MessageID param.Field[string] `json:"messageID,required"` + SessionID param.Field[string] `json:"sessionID,required"` + Text param.Field[string] `json:"text,required"` + Type param.Field[TextPartType] `json:"type,required"` + Synthetic param.Field[bool] `json:"synthetic"` + Time param.Field[TextPartTimeParam] `json:"time"` } func (r TextPartParam) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } -func (r TextPartParam) implementsUserMessagePartUnionParam() {} +func (r TextPartParam) implementsSessionChatParamsPartUnion() {} + +type TextPartTimeParam struct { + Start param.Field[float64] `json:"start,required"` + End param.Field[float64] `json:"end"` +} + +func (r TextPartTimeParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} type ToolPart struct { - ID string `json:"id,required"` - State ToolPartState `json:"state,required"` - Tool string `json:"tool,required"` - Type ToolPartType `json:"type,required"` - JSON toolPartJSON `json:"-"` + ID string `json:"id,required"` + CallID string `json:"callID,required"` + MessageID string `json:"messageID,required"` + SessionID string `json:"sessionID,required"` + State ToolPartState `json:"state,required"` + Tool string `json:"tool,required"` + Type ToolPartType `json:"type,required"` + JSON toolPartJSON `json:"-"` } // toolPartJSON contains the JSON metadata for the struct [ToolPart] type toolPartJSON struct { ID apijson.Field + CallID apijson.Field + MessageID apijson.Field + SessionID apijson.Field State apijson.Field Tool apijson.Field Type apijson.Field @@ -1001,7 +1081,7 @@ func (r toolPartJSON) RawJSON() string { return r.raw } -func (r ToolPart) implementsAssistantMessagePart() {} +func (r ToolPart) implementsPart() {} type ToolPartState struct { Status ToolPartStateStatus `json:"status,required"` @@ -1357,18 +1437,16 @@ func (r toolStateRunningTimeJSON) RawJSON() string { } 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:"-"` + ID string `json:"id,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 @@ -1420,119 +1498,82 @@ func (r userMessageTimeJSON) RawJSON() string { return r.raw } -type UserMessagePart struct { - Type UserMessagePartType `json:"type,required"` - Filename string `json:"filename"` - Mime string `json:"mime"` - Synthetic bool `json:"synthetic"` - Text string `json:"text"` - URL string `json:"url"` - JSON userMessagePartJSON `json:"-"` - union UserMessagePartUnion +type SessionMessagesResponse struct { + Info Message `json:"info,required"` + Parts []Part `json:"parts,required"` + JSON sessionMessagesResponseJSON `json:"-"` } -// userMessagePartJSON contains the JSON metadata for the struct [UserMessagePart] -type userMessagePartJSON struct { - Type apijson.Field - Filename apijson.Field - Mime apijson.Field - Synthetic apijson.Field - Text apijson.Field - URL apijson.Field +// sessionMessagesResponseJSON contains the JSON metadata for the struct +// [SessionMessagesResponse] +type sessionMessagesResponseJSON struct { + Info apijson.Field + Parts apijson.Field raw string ExtraFields map[string]apijson.Field } -func (r userMessagePartJSON) RawJSON() string { +func (r *SessionMessagesResponse) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sessionMessagesResponseJSON) RawJSON() string { return r.raw } -func (r *UserMessagePart) UnmarshalJSON(data []byte) (err error) { - *r = UserMessagePart{} - err = apijson.UnmarshalRoot(data, &r.union) - if err != nil { - return err - } - return apijson.Port(r.union, &r) +type SessionChatParams struct { + MessageID param.Field[string] `json:"messageID,required"` + Mode param.Field[string] `json:"mode,required"` + ModelID param.Field[string] `json:"modelID,required"` + Parts param.Field[[]SessionChatParamsPartUnion] `json:"parts,required"` + ProviderID param.Field[string] `json:"providerID,required"` } -// AsUnion returns a [UserMessagePartUnion] interface which you can cast to the -// specific types for more type safety. -// -// Possible runtime types of the union are [TextPart], [FilePart]. -func (r UserMessagePart) AsUnion() UserMessagePartUnion { - return r.union +func (r SessionChatParams) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) } -// Union satisfied by [TextPart] or [FilePart]. -type UserMessagePartUnion interface { - implementsUserMessagePart() +type SessionChatParamsPart struct { + ID param.Field[string] `json:"id,required"` + MessageID param.Field[string] `json:"messageID,required"` + SessionID param.Field[string] `json:"sessionID,required"` + Type param.Field[SessionChatParamsPartsType] `json:"type,required"` + Filename param.Field[string] `json:"filename"` + Mime param.Field[string] `json:"mime"` + Synthetic param.Field[bool] `json:"synthetic"` + Text param.Field[string] `json:"text"` + Time param.Field[interface{}] `json:"time"` + URL param.Field[string] `json:"url"` } -func init() { - apijson.RegisterUnion( - reflect.TypeOf((*UserMessagePartUnion)(nil)).Elem(), - "type", - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(TextPart{}), - DiscriminatorValue: "text", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(FilePart{}), - DiscriminatorValue: "file", - }, - ) +func (r SessionChatParamsPart) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) } -type UserMessagePartType string +func (r SessionChatParamsPart) implementsSessionChatParamsPartUnion() {} + +// Satisfied by [FilePartParam], [TextPartParam], [SessionChatParamsPart]. +type SessionChatParamsPartUnion interface { + implementsSessionChatParamsPartUnion() +} + +type SessionChatParamsPartsType string const ( - UserMessagePartTypeText UserMessagePartType = "text" - UserMessagePartTypeFile UserMessagePartType = "file" + SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file" + SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text" ) -func (r UserMessagePartType) IsKnown() bool { +func (r SessionChatParamsPartsType) IsKnown() bool { switch r { - case UserMessagePartTypeText, UserMessagePartTypeFile: + case SessionChatParamsPartsTypeFile, SessionChatParamsPartsTypeText: return true } return false } -type UserMessagePartParam struct { - Type param.Field[UserMessagePartType] `json:"type,required"` - Filename param.Field[string] `json:"filename"` - Mime param.Field[string] `json:"mime"` - Synthetic param.Field[bool] `json:"synthetic"` - Text param.Field[string] `json:"text"` - URL param.Field[string] `json:"url"` -} - -func (r UserMessagePartParam) MarshalJSON() (data []byte, err error) { - return apijson.MarshalRoot(r) -} - -func (r UserMessagePartParam) implementsUserMessagePartUnionParam() {} - -// Satisfied by [TextPartParam], [FilePartParam], [UserMessagePartParam]. -type UserMessagePartUnionParam interface { - implementsUserMessagePartUnionParam() -} - -type SessionChatParams struct { - Mode param.Field[string] `json:"mode,required"` - ModelID param.Field[string] `json:"modelID,required"` - Parts param.Field[[]UserMessagePartUnionParam] `json:"parts,required"` - ProviderID param.Field[string] `json:"providerID,required"` -} - -func (r SessionChatParams) MarshalJSON() (data []byte, err error) { - return apijson.MarshalRoot(r) -} - type SessionInitParams struct { + MessageID param.Field[string] `json:"messageID,required"` ModelID param.Field[string] `json:"modelID,required"` ProviderID param.Field[string] `json:"providerID,required"` } diff --git a/packages/tui/sdk/session_test.go b/packages/tui/sdk/session_test.go index 4ff2818c5..c74a4a385 100644 --- a/packages/tui/sdk/session_test.go +++ b/packages/tui/sdk/session_test.go @@ -117,12 +117,17 @@ func TestSessionChat(t *testing.T) { context.TODO(), "id", opencode.SessionChatParams{ - Mode: opencode.F("mode"), - ModelID: opencode.F("modelID"), - Parts: opencode.F([]opencode.UserMessagePartUnionParam{opencode.TextPartParam{ - Text: opencode.F("text"), - Type: opencode.F(opencode.TextPartTypeText), - Synthetic: opencode.F(true), + MessageID: opencode.F("messageID"), + Mode: opencode.F("mode"), + ModelID: opencode.F("modelID"), + Parts: opencode.F([]opencode.SessionChatParamsPartUnion{opencode.FilePartParam{ + ID: opencode.F("id"), + MessageID: opencode.F("messageID"), + Mime: opencode.F("mime"), + SessionID: opencode.F("sessionID"), + Type: opencode.F(opencode.FilePartTypeFile), + URL: opencode.F("url"), + Filename: opencode.F("filename"), }}), ProviderID: opencode.F("providerID"), }, @@ -152,6 +157,7 @@ func TestSessionInit(t *testing.T) { context.TODO(), "id", opencode.SessionInitParams{ + MessageID: opencode.F("messageID"), ModelID: opencode.F("modelID"), ProviderID: opencode.F("providerID"), }, diff --git a/packages/tui/sdk/usage_test.go b/packages/tui/sdk/usage_test.go index 0e261a7aa..5e8f44c7c 100644 --- a/packages/tui/sdk/usage_test.go +++ b/packages/tui/sdk/usage_test.go @@ -23,10 +23,13 @@ func TestUsage(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - events, err := client.Event.List(context.TODO()) + stream := client.Event.ListStreaming(context.TODO()) + for stream.Next() { + t.Logf("%+v\n", stream.Current()) + } + err := stream.Err() if err != nil { t.Error(err) return } - t.Logf("%+v\n", events) } |
