diff options
| author | adamdottv <[email protected]> | 2025-06-23 12:30:20 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-06-23 12:30:20 -0500 |
| commit | 8d3b2fb8215b6b6b7b6a433cfba7e74bf0de93d1 (patch) | |
| tree | d63fc40e2cc9b970f5f889914a68cf624e4253e0 /packages/tui/internal | |
| parent | fa991920bc721ce15ce6921dd91e83abab2fa23e (diff) | |
| download | opencode-8d3b2fb8215b6b6b7b6a433cfba7e74bf0de93d1.tar.gz opencode-8d3b2fb8215b6b6b7b6a433cfba7e74bf0de93d1.zip | |
feat(tui): optimistically render user messages
Diffstat (limited to 'packages/tui/internal')
| -rw-r--r-- | packages/tui/internal/app/app.go | 48 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/message.go | 4 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/messages.go | 8 | ||||
| -rw-r--r-- | packages/tui/internal/tui/tui.go | 29 |
4 files changed, 68 insertions, 21 deletions
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index f20620790..63c398c43 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -6,11 +6,13 @@ import ( "path/filepath" "sort" "strings" + "time" "log/slog" tea "github.com/charmbracelet/bubbletea/v2" "github.com/sst/opencode/internal/commands" + "github.com/sst/opencode/internal/components/toast" "github.com/sst/opencode/internal/config" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" @@ -47,6 +49,9 @@ type SendMsg struct { type CompletionDialogTriggeredMsg struct { InitialValue string } +type OptimisticMessageAddedMsg struct { + Message client.MessageInfo +} func New( ctx context.Context, @@ -297,19 +302,12 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At if a.Session.Id == "" { session, err := a.CreateSession(ctx) if err != nil { - // status.Error(err.Error()) - return nil + return toast.NewErrorToast(err.Error()) } a.Session = session cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session))) } - // TODO: Handle attachments when API supports them - if len(attachments) > 0 { - // For now, ignore attachments - // return "", fmt.Errorf("attachments not supported yet") - } - part := client.MessagePart{} part.FromMessagePartText(client.MessagePartText{ Type: "text", @@ -317,7 +315,26 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At }) parts := []client.MessagePart{part} - go func() { + optimisticMessage := client.MessageInfo{ + Id: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()), + Role: client.User, + Parts: parts, + Metadata: client.MessageMetadata{ + SessionID: a.Session.Id, + Time: struct { + Completed *float32 `json:"completed,omitempty"` + Created float32 `json:"created"` + }{ + Created: float32(time.Now().Unix()), + }, + Tool: make(map[string]client.MessageMetadata_Tool_AdditionalProperties), + }, + } + + a.Messages = append(a.Messages, optimisticMessage) + cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage})) + + cmds = append(cmds, func() tea.Msg { response, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{ SessionID: a.Session.Id, Parts: parts, @@ -325,14 +342,17 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At ModelID: a.Model.Id, }) if err != nil { - slog.Error("Failed to send message", "error", err) - // status.Error(err.Error()) + errormsg := fmt.Sprintf("failed to send message: %v", err) + slog.Error(errormsg) + return toast.NewErrorToast(errormsg)() } if response != nil && response.StatusCode != 200 { - slog.Error("Failed to send message", "error", fmt.Sprintf("failed to send message: %d", response.StatusCode)) - // status.Error(fmt.Sprintf("failed to send message: %d", response.StatusCode)) + errormsg := fmt.Sprintf("failed to send message: %d", response.StatusCode) + slog.Error(errormsg) + return toast.NewErrorToast(errormsg)() } - }() + return nil + }) // The actual response will come through SSE // For now, just return success diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go index e4b3fbb0e..c20bf6447 100644 --- a/packages/tui/internal/components/chat/message.go +++ b/packages/tui/internal/components/chat/message.go @@ -253,7 +253,7 @@ func renderText(message client.MessageInfo, text string, author string) string { func renderToolInvocation( toolCall client.MessageToolInvocationToolCall, result *string, - metadata client.MessageInfo_Metadata_Tool_AdditionalProperties, + metadata client.MessageMetadata_Tool_AdditionalProperties, showDetails bool, isLast bool, contentOnly bool, @@ -466,7 +466,7 @@ func renderToolInvocation( if metadata, ok := call["metadata"].(map[string]any); ok { data, _ = json.Marshal(metadata) - var toolMetadata client.MessageInfo_Metadata_Tool_AdditionalProperties + var toolMetadata client.MessageMetadata_Tool_AdditionalProperties _ = json.Unmarshal(data, &toolMetadata) step := renderToolInvocation( diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 3c5e88c20..bdb6a6f82 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -58,6 +58,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.GotoBottom() m.tail = true return m, nil + case app.OptimisticMessageAddedMsg: + m.renderView() + if m.tail { + m.viewport.GotoBottom() + } + return m, nil case dialog.ThemeSelectedMsg: m.cache.Clear() return m, m.Reload() @@ -171,7 +177,7 @@ func (m *messagesComponent) renderView() { isLastToolInvocation := slices.Contains(lastToolIndices, i) toolInvocationPart := part.(client.MessagePartToolInvocation) toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall() - metadata := client.MessageInfo_Metadata_Tool_AdditionalProperties{} + metadata := client.MessageMetadata_Tool_AdditionalProperties{} if _, ok := message.Metadata.Tool[toolCall.ToolCallId]; ok { metadata = message.Metadata.Tool[toolCall.ToolCallId] } diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index d2b401551..654cd2a52 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -230,12 +230,33 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case client.EventMessageUpdated: if msg.Properties.Info.Metadata.SessionID == a.app.Session.Id { exists := false - for i, m := range a.app.Messages { - if m.Id == msg.Properties.Info.Id { - a.app.Messages[i] = msg.Properties.Info - exists = true + optimisticReplaced := false + + // First check if this is replacing an optimistic message + if msg.Properties.Info.Role == client.User { + // Look for optimistic messages to replace + for i, m := range a.app.Messages { + if strings.HasPrefix(m.Id, "optimistic-") && m.Role == client.User { + // Replace the optimistic message with the real one + a.app.Messages[i] = msg.Properties.Info + exists = true + optimisticReplaced = true + break + } } } + + // 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 + exists = true + break + } + } + } + if !exists { a.app.Messages = append(a.app.Messages, msg.Properties.Info) } |
