summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-06-23 12:30:20 -0500
committeradamdottv <[email protected]>2025-06-23 12:30:20 -0500
commit8d3b2fb8215b6b6b7b6a433cfba7e74bf0de93d1 (patch)
treed63fc40e2cc9b970f5f889914a68cf624e4253e0 /packages/tui/internal
parentfa991920bc721ce15ce6921dd91e83abab2fa23e (diff)
downloadopencode-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.go48
-rw-r--r--packages/tui/internal/components/chat/message.go4
-rw-r--r--packages/tui/internal/components/chat/messages.go8
-rw-r--r--packages/tui/internal/tui/tui.go29
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)
}