summaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/app/app.go106
-rw-r--r--internal/app/app_new.go203
-rw-r--r--internal/app/event_adapter.go158
-rw-r--r--internal/app/interfaces.go42
-rw-r--r--internal/app/services_bridge.go250
-rw-r--r--internal/tui/components/chat/messages.go50
-rw-r--r--internal/tui/components/chat/sidebar.go61
-rw-r--r--internal/tui/components/logs/table.go13
-rw-r--r--internal/tui/tui.go63
9 files changed, 831 insertions, 115 deletions
diff --git a/internal/app/app.go b/internal/app/app.go
index 0ef89d1b9..7d0bdd4cb 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -2,7 +2,6 @@ package app
import (
"context"
- "database/sql"
"maps"
"sync"
"time"
@@ -11,12 +10,7 @@ import (
"github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/fileutil"
- "github.com/sst/opencode/internal/history"
- "github.com/sst/opencode/internal/llm/agent"
- "github.com/sst/opencode/internal/logging"
"github.com/sst/opencode/internal/lsp"
- "github.com/sst/opencode/internal/message"
- "github.com/sst/opencode/internal/permission"
"github.com/sst/opencode/internal/session"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/tui/theme"
@@ -25,15 +19,15 @@ import (
type App struct {
CurrentSession *session.Session
- Logs logging.Service
- Sessions session.Service
- Messages message.Service
- History history.Service
- Permissions permission.Service
+ Logs interface{} // TODO: Define LogService interface when needed
+ Sessions SessionService
+ Messages MessageService
+ History interface{} // TODO: Define HistoryService interface when needed
+ Permissions interface{} // TODO: Define PermissionService interface when needed
Status status.Service
Client *client.Client
- PrimaryAgent agent.Service
+ PrimaryAgent AgentService
LSPClients map[string]*lsp.Client
@@ -48,55 +42,42 @@ type App struct {
completionDialogOpen bool
}
-func New(ctx context.Context, conn *sql.DB) (*App, error) {
- err := logging.InitService(conn)
- if err != nil {
- slog.Error("Failed to initialize logging service", "error", err)
- return nil, err
- }
- err = session.InitService(conn)
- if err != nil {
- slog.Error("Failed to initialize session service", "error", err)
- return nil, err
- }
- err = message.InitService(conn)
- if err != nil {
- slog.Error("Failed to initialize message service", "error", err)
- return nil, err
- }
- err = history.InitService(conn)
- if err != nil {
- slog.Error("Failed to initialize history service", "error", err)
- return nil, err
- }
- err = permission.InitService()
- if err != nil {
- slog.Error("Failed to initialize permission service", "error", err)
- return nil, err
- }
- err = status.InitService()
+func New(ctx context.Context) (*App, error) {
+ // Initialize status service (still needed for UI notifications)
+ err := status.InitService()
if err != nil {
slog.Error("Failed to initialize status service", "error", err)
return nil, err
}
+
+ // Initialize file utilities
fileutil.Init()
- client, err := client.NewClient("http://localhost:16713")
+ // Create HTTP client
+ httpClient, err := client.NewClient("http://localhost:16713")
if err != nil {
slog.Error("Failed to create client", "error", err)
return nil, err
}
+ // Create service bridges
+ sessionBridge := NewSessionServiceBridge(httpClient)
+ messageBridge := NewMessageServiceBridge(httpClient)
+ agentBridge := NewAgentServiceBridge(httpClient)
+
app := &App{
- Client: client,
+ Client: httpClient,
CurrentSession: &session.Session{},
- Logs: logging.GetService(),
- Sessions: session.GetService(),
- Messages: message.GetService(),
- History: history.GetService(),
- Permissions: permission.GetService(),
+ Sessions: sessionBridge,
+ Messages: messageBridge,
+ PrimaryAgent: agentBridge,
Status: status.GetService(),
LSPClients: make(map[string]*lsp.Client),
+
+ // TODO: These services need API endpoints:
+ Logs: nil, // logging.GetService(),
+ History: nil, // history.GetService(),
+ Permissions: nil, // permission.GetService(),
}
// Initialize theme based on configuration
@@ -105,22 +86,23 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
// Initialize LSP clients in the background
go app.initLSPClients(ctx)
- app.PrimaryAgent, err = agent.NewAgent(
- config.AgentPrimary,
- app.Sessions,
- app.Messages,
- agent.PrimaryAgentTools(
- app.Permissions,
- app.Sessions,
- app.Messages,
- app.History,
- app.LSPClients,
- ),
- )
- if err != nil {
- slog.Error("Failed to create primary agent", "error", err)
- return nil, err
- }
+ // TODO: Remove this once agent is fully replaced by API
+ // app.PrimaryAgent, err = agent.NewAgent(
+ // config.AgentPrimary,
+ // app.Sessions,
+ // app.Messages,
+ // agent.PrimaryAgentTools(
+ // app.Permissions,
+ // app.Sessions,
+ // app.Messages,
+ // app.History,
+ // app.LSPClients,
+ // ),
+ // )
+ // if err != nil {
+ // slog.Error("Failed to create primary agent", "error", err)
+ // return nil, err
+ // }
return app, nil
}
diff --git a/internal/app/app_new.go b/internal/app/app_new.go
new file mode 100644
index 000000000..1568b4d57
--- /dev/null
+++ b/internal/app/app_new.go
@@ -0,0 +1,203 @@
+package app
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sync"
+
+ "log/slog"
+
+ "github.com/sst/opencode/pkg/client"
+)
+
+// AppNew is the new app structure that uses the TypeScript backend
+type AppNew struct {
+ Client *client.Client
+ CurrentSession *client.SessionInfo
+
+ // Event handling
+ eventCtx context.Context
+ eventCancel context.CancelFunc
+ eventChan <-chan any
+
+ // UI state
+ filepickerOpen bool
+ completionDialogOpen bool
+
+ // Mutex for thread-safe operations
+ mu sync.RWMutex
+}
+
+// NewApp creates a new app instance connected to the TypeScript backend
+func NewApp(ctx context.Context) (*AppNew, error) {
+ httpClient, err := client.NewClient("http://localhost:16713")
+ if err != nil {
+ slog.Error("Failed to create client", "error", err)
+ return nil, err
+ }
+
+ app := &AppNew{
+ Client: httpClient,
+ }
+
+ // Start event listener
+ if err := app.startEventListener(ctx); err != nil {
+ return nil, err
+ }
+
+ return app, nil
+}
+
+// startEventListener connects to the SSE endpoint and processes events
+func (a *AppNew) startEventListener(ctx context.Context) error {
+ a.eventCtx, a.eventCancel = context.WithCancel(ctx)
+
+ eventChan, err := a.Client.Event(a.eventCtx)
+ if err != nil {
+ return err
+ }
+
+ a.eventChan = eventChan
+
+ // Start processing events in background
+ go a.processEvents()
+
+ return nil
+}
+
+// processEvents handles incoming SSE events
+func (a *AppNew) processEvents() {
+ for event := range a.eventChan {
+ switch e := event.(type) {
+ case *client.EventStorageWrite:
+ // Handle storage write events
+ slog.Debug("Storage write event", "key", e.Key)
+ // TODO: Update local state based on storage events
+ default:
+ slog.Debug("Unknown event type", "event", e)
+ }
+ }
+}
+
+// CreateSession creates a new session via the API
+func (a *AppNew) CreateSession(ctx context.Context) error {
+ resp, err := a.Client.PostSessionCreate(ctx)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ return fmt.Errorf("failed to create session: %d", resp.StatusCode)
+ }
+
+ var session client.SessionInfo
+ if err := json.NewDecoder(resp.Body).Decode(&session); err != nil {
+ return err
+ }
+
+ a.mu.Lock()
+ a.CurrentSession = &session
+ a.mu.Unlock()
+
+ return nil
+}
+
+// SendMessage sends a message to the current session
+func (a *AppNew) SendMessage(ctx context.Context, text string) error {
+ if a.CurrentSession == nil {
+ if err := a.CreateSession(ctx); err != nil {
+ return err
+ }
+ }
+
+ a.mu.RLock()
+ sessionID := a.CurrentSession.Id
+ a.mu.RUnlock()
+
+ parts := interface{}([]map[string]interface{}{
+ {
+ "type": "text",
+ "text": text,
+ },
+ })
+
+ resp, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
+ SessionID: sessionID,
+ Parts: &parts,
+ })
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ // The response will be streamed via SSE
+ return nil
+}
+
+// GetSessions retrieves all sessions
+func (a *AppNew) GetSessions(ctx context.Context) ([]client.SessionInfo, error) {
+ resp, err := a.Client.PostSessionList(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var sessions []client.SessionInfo
+ if err := json.NewDecoder(resp.Body).Decode(&sessions); err != nil {
+ return nil, err
+ }
+
+ return sessions, nil
+}
+
+// GetMessages retrieves messages for a session
+func (a *AppNew) GetMessages(ctx context.Context, sessionID string) (interface{}, error) {
+ resp, err := a.Client.PostSessionMessages(ctx, client.PostSessionMessagesJSONRequestBody{
+ SessionID: sessionID,
+ })
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var messages interface{}
+ if err := json.NewDecoder(resp.Body).Decode(&messages); err != nil {
+ return nil, err
+ }
+
+ return messages, nil
+}
+
+// Close shuts down the app and its connections
+func (a *AppNew) Close() {
+ if a.eventCancel != nil {
+ a.eventCancel()
+ }
+}
+
+// UI state methods
+func (a *AppNew) SetFilepickerOpen(open bool) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ a.filepickerOpen = open
+}
+
+func (a *AppNew) IsFilepickerOpen() bool {
+ a.mu.RLock()
+ defer a.mu.RUnlock()
+ return a.filepickerOpen
+}
+
+func (a *AppNew) SetCompletionDialogOpen(open bool) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ a.completionDialogOpen = open
+}
+
+func (a *AppNew) IsCompletionDialogOpen() bool {
+ a.mu.RLock()
+ defer a.mu.RUnlock()
+ return a.completionDialogOpen
+} \ No newline at end of file
diff --git a/internal/app/event_adapter.go b/internal/app/event_adapter.go
new file mode 100644
index 000000000..4772bde49
--- /dev/null
+++ b/internal/app/event_adapter.go
@@ -0,0 +1,158 @@
+package app
+
+import (
+ "encoding/json"
+ "time"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/sst/opencode/internal/message"
+ "github.com/sst/opencode/pkg/client"
+)
+
+// StorageWriteMsg is sent when a storage.write event is received
+type StorageWriteMsg struct {
+ Key string
+ Content interface{}
+}
+
+// ProcessSSEEvent converts SSE events into TUI messages
+func ProcessSSEEvent(event interface{}) tea.Msg {
+ switch e := event.(type) {
+ case *client.EventStorageWrite:
+ return StorageWriteMsg{
+ Key: e.Key,
+ Content: e.Content,
+ }
+ }
+
+ // Return the raw event if we don't have a specific handler
+ return event
+}
+
+// MessageFromStorage converts storage content to internal message format
+type MessageData struct {
+ ID string `json:"id"`
+ Role string `json:"role"`
+ Parts []interface{} `json:"parts"`
+ Metadata map[string]interface{} `json:"metadata"`
+}
+
+// SessionInfoFromStorage converts storage content to session info
+type SessionInfoData struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ ShareID *string `json:"shareID,omitempty"`
+ Tokens struct {
+ Input float32 `json:"input"`
+ Output float32 `json:"output"`
+ Reasoning float32 `json:"reasoning"`
+ } `json:"tokens"`
+}
+
+// ConvertStorageMessage converts a storage message to internal message format
+func ConvertStorageMessage(data interface{}, sessionID string) (*message.Message, error) {
+ // Convert the interface{} to JSON then back to our struct
+ jsonData, err := json.Marshal(data)
+ if err != nil {
+ return nil, err
+ }
+
+ var msgData MessageData
+ if err := json.Unmarshal(jsonData, &msgData); err != nil {
+ return nil, err
+ }
+
+ // Convert parts
+ var parts []message.ContentPart
+ for _, part := range msgData.Parts {
+ partMap, ok := part.(map[string]interface{})
+ if !ok {
+ continue
+ }
+
+ partType, ok := partMap["type"].(string)
+ if !ok {
+ continue
+ }
+
+ switch partType {
+ case "text":
+ if text, ok := partMap["text"].(string); ok {
+ parts = append(parts, message.TextContent{Text: text})
+ }
+ case "tool-invocation":
+ if toolInv, ok := partMap["toolInvocation"].(map[string]interface{}); ok {
+ // Convert tool invocation to tool call
+ toolCall := message.ToolCall{
+ ID: toolInv["toolCallId"].(string),
+ Name: toolInv["toolName"].(string),
+ Type: "function",
+ }
+
+ if args, ok := toolInv["args"]; ok {
+ argsJSON, _ := json.Marshal(args)
+ toolCall.Input = string(argsJSON)
+ }
+
+ if state, ok := toolInv["state"].(string); ok {
+ toolCall.Finished = state == "result"
+ }
+
+ parts = append(parts, toolCall)
+
+ // If there's a result, add it as a tool result
+ if result, ok := toolInv["result"]; ok && toolCall.Finished {
+ resultStr := ""
+ switch r := result.(type) {
+ case string:
+ resultStr = r
+ default:
+ resultJSON, _ := json.Marshal(r)
+ resultStr = string(resultJSON)
+ }
+
+ parts = append(parts, message.ToolResult{
+ ToolCallID: toolCall.ID,
+ Name: toolCall.Name,
+ Content: resultStr,
+ })
+ }
+ }
+ }
+ }
+
+ // Convert role
+ var role message.MessageRole
+ switch msgData.Role {
+ case "user":
+ role = message.User
+ case "assistant":
+ role = message.Assistant
+ case "system":
+ role = message.System
+ default:
+ role = message.MessageRole(msgData.Role)
+ }
+
+ // Create message
+ msg := &message.Message{
+ ID: msgData.ID,
+ Role: role,
+ SessionID: sessionID,
+ Parts: parts,
+ CreatedAt: time.Now(), // TODO: Get from metadata
+ UpdatedAt: time.Now(), // TODO: Get from metadata
+ }
+
+ // Try to get timestamps from metadata
+ if metadata, ok := msgData.Metadata["time"].(map[string]interface{}); ok {
+ if created, ok := metadata["created"].(float64); ok {
+ msg.CreatedAt = time.Unix(int64(created/1000), 0)
+ }
+ if completed, ok := metadata["completed"].(float64); ok {
+ msg.UpdatedAt = time.Unix(int64(completed/1000), 0)
+ }
+ }
+
+ return msg, nil
+} \ No newline at end of file
diff --git a/internal/app/interfaces.go b/internal/app/interfaces.go
new file mode 100644
index 000000000..2f4287078
--- /dev/null
+++ b/internal/app/interfaces.go
@@ -0,0 +1,42 @@
+package app
+
+import (
+ "context"
+ "time"
+
+ "github.com/sst/opencode/internal/message"
+ "github.com/sst/opencode/internal/pubsub"
+ "github.com/sst/opencode/internal/session"
+)
+
+// SessionService defines the interface for session operations
+type SessionService interface {
+ Create(ctx context.Context, title string) (session.Session, error)
+ Get(ctx context.Context, id string) (session.Session, error)
+ List(ctx context.Context) ([]session.Session, error)
+ Update(ctx context.Context, id, title string) error
+ Delete(ctx context.Context, id string) error
+}
+
+// MessageService defines the interface for message operations
+type MessageService interface {
+ pubsub.Subscriber[message.Message]
+
+ GetBySession(ctx context.Context, sessionID string) ([]message.Message, error)
+ List(ctx context.Context, sessionID string) ([]message.Message, error)
+ Create(ctx context.Context, sessionID string, params message.CreateMessageParams) (message.Message, error)
+ Update(ctx context.Context, msg message.Message) (message.Message, error)
+ Delete(ctx context.Context, id string) error
+ DeleteSessionMessages(ctx context.Context, sessionID string) error
+ Get(ctx context.Context, id string) (message.Message, error)
+ ListAfter(ctx context.Context, sessionID string, timestamp time.Time) ([]message.Message, error)
+}
+
+// AgentService defines the interface for agent operations
+type AgentService interface {
+ Run(ctx context.Context, sessionID string, text string, attachments ...message.Attachment) (string, error)
+ Cancel(sessionID string) error
+ IsBusy() bool
+ IsSessionBusy(sessionID string) bool
+ CompactSession(ctx context.Context, sessionID string, force bool) error
+} \ No newline at end of file
diff --git a/internal/app/services_bridge.go b/internal/app/services_bridge.go
new file mode 100644
index 000000000..e2d2ab5ba
--- /dev/null
+++ b/internal/app/services_bridge.go
@@ -0,0 +1,250 @@
+package app
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/sst/opencode/internal/message"
+ "github.com/sst/opencode/internal/pubsub"
+ "github.com/sst/opencode/internal/session"
+ "github.com/sst/opencode/pkg/client"
+)
+
+// SessionServiceBridge adapts the HTTP API to the old session.Service interface
+type SessionServiceBridge struct {
+ client *client.Client
+}
+
+// NewSessionServiceBridge creates a new session service bridge
+func NewSessionServiceBridge(client *client.Client) *SessionServiceBridge {
+ return &SessionServiceBridge{client: client}
+}
+
+// Create creates a new session
+func (s *SessionServiceBridge) Create(ctx context.Context, title string) (session.Session, error) {
+ resp, err := s.client.PostSessionCreate(ctx)
+ if err != nil {
+ return session.Session{}, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ return session.Session{}, fmt.Errorf("failed to create session: %d", resp.StatusCode)
+ }
+
+ var info client.SessionInfo
+ if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
+ return session.Session{}, err
+ }
+
+ // Convert to old session type
+ return session.Session{
+ ID: info.Id,
+ Title: info.Title,
+ CreatedAt: time.Now(), // API doesn't provide this yet
+ UpdatedAt: time.Now(), // API doesn't provide this yet
+ }, nil
+}
+
+// Get retrieves a session by ID
+func (s *SessionServiceBridge) Get(ctx context.Context, id string) (session.Session, error) {
+ // TODO: API doesn't have a get by ID endpoint yet
+ // For now, list all and find the one we want
+ sessions, err := s.List(ctx)
+ if err != nil {
+ return session.Session{}, err
+ }
+
+ for _, sess := range sessions {
+ if sess.ID == id {
+ return sess, nil
+ }
+ }
+
+ return session.Session{}, fmt.Errorf("session not found: %s", id)
+}
+
+// List retrieves all sessions
+func (s *SessionServiceBridge) List(ctx context.Context) ([]session.Session, error) {
+ resp, err := s.client.PostSessionList(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var infos []client.SessionInfo
+ if err := json.NewDecoder(resp.Body).Decode(&infos); err != nil {
+ return nil, err
+ }
+
+ // Convert to old session type
+ sessions := make([]session.Session, len(infos))
+ for i, info := range infos {
+ sessions[i] = session.Session{
+ ID: info.Id,
+ Title: info.Title,
+ CreatedAt: time.Now(), // API doesn't provide this yet
+ UpdatedAt: time.Now(), // API doesn't provide this yet
+ }
+ }
+
+ return sessions, nil
+}
+
+// Update updates a session - NOT IMPLEMENTED IN API YET
+func (s *SessionServiceBridge) Update(ctx context.Context, id, title string) error {
+ // TODO: Not implemented in TypeScript API yet
+ return fmt.Errorf("session update not implemented in API")
+}
+
+// Delete deletes a session - NOT IMPLEMENTED IN API YET
+func (s *SessionServiceBridge) Delete(ctx context.Context, id string) error {
+ // TODO: Not implemented in TypeScript API yet
+ return fmt.Errorf("session delete not implemented in API")
+}
+
+// AgentServiceBridge provides a minimal agent service that sends messages to the API
+type AgentServiceBridge struct {
+ client *client.Client
+}
+
+// NewAgentServiceBridge creates a new agent service bridge
+func NewAgentServiceBridge(client *client.Client) *AgentServiceBridge {
+ return &AgentServiceBridge{client: client}
+}
+
+// Run sends a message to the chat API
+func (a *AgentServiceBridge) Run(ctx context.Context, sessionID string, text string, attachments ...message.Attachment) (string, error) {
+ // TODO: Handle attachments when API supports them
+ if len(attachments) > 0 {
+ // For now, ignore attachments
+ // return "", fmt.Errorf("attachments not supported yet")
+ }
+
+ parts := interface{}([]map[string]interface{}{
+ {
+ "type": "text",
+ "text": text,
+ },
+ })
+
+ resp, err := a.client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
+ SessionID: sessionID,
+ Parts: &parts,
+ })
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ // The actual response will come through SSE
+ // For now, just return success
+ return "", nil
+}
+
+// Cancel cancels the current generation - NOT IMPLEMENTED IN API YET
+func (a *AgentServiceBridge) Cancel(sessionID string) error {
+ // TODO: Not implemented in TypeScript API yet
+ return nil
+}
+
+// IsBusy checks if the agent is busy - NOT IMPLEMENTED IN API YET
+func (a *AgentServiceBridge) IsBusy() bool {
+ // TODO: Not implemented in TypeScript API yet
+ return false
+}
+
+// IsSessionBusy checks if the agent is busy for a specific session - NOT IMPLEMENTED IN API YET
+func (a *AgentServiceBridge) IsSessionBusy(sessionID string) bool {
+ // TODO: Not implemented in TypeScript API yet
+ return false
+}
+
+// CompactSession compacts a session - NOT IMPLEMENTED IN API YET
+func (a *AgentServiceBridge) CompactSession(ctx context.Context, sessionID string, force bool) error {
+ // TODO: Not implemented in TypeScript API yet
+ return fmt.Errorf("session compaction not implemented in API")
+}
+
+// MessageServiceBridge provides a minimal message service that fetches from the API
+type MessageServiceBridge struct {
+ client *client.Client
+ broker *pubsub.Broker[message.Message]
+}
+
+// NewMessageServiceBridge creates a new message service bridge
+func NewMessageServiceBridge(client *client.Client) *MessageServiceBridge {
+ return &MessageServiceBridge{
+ client: client,
+ broker: pubsub.NewBroker[message.Message](),
+ }
+}
+
+// GetBySession retrieves messages for a session
+func (m *MessageServiceBridge) GetBySession(ctx context.Context, sessionID string) ([]message.Message, error) {
+ return m.List(ctx, sessionID)
+}
+
+// List retrieves messages for a session
+func (m *MessageServiceBridge) List(ctx context.Context, sessionID string) ([]message.Message, error) {
+ resp, err := m.client.PostSessionMessages(ctx, client.PostSessionMessagesJSONRequestBody{
+ SessionID: sessionID,
+ })
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ // The API returns a different format, we'll need to adapt it
+ var rawMessages interface{}
+ if err := json.NewDecoder(resp.Body).Decode(&rawMessages); err != nil {
+ return nil, err
+ }
+
+ // TODO: Convert the API message format to our internal format
+ // For now, return empty to avoid compilation errors
+ return []message.Message{}, nil
+}
+
+// Create creates a new message - NOT NEEDED, handled by chat API
+func (m *MessageServiceBridge) Create(ctx context.Context, sessionID string, params message.CreateMessageParams) (message.Message, error) {
+ // Messages are created through the chat API
+ return message.Message{}, fmt.Errorf("use chat API to send messages")
+}
+
+// Update updates a message - NOT IMPLEMENTED IN API YET
+func (m *MessageServiceBridge) Update(ctx context.Context, msg message.Message) (message.Message, error) {
+ // TODO: Not implemented in TypeScript API yet
+ return message.Message{}, fmt.Errorf("message update not implemented in API")
+}
+
+// Delete deletes a message - NOT IMPLEMENTED IN API YET
+func (m *MessageServiceBridge) Delete(ctx context.Context, id string) error {
+ // TODO: Not implemented in TypeScript API yet
+ return fmt.Errorf("message delete not implemented in API")
+}
+
+// DeleteSessionMessages deletes all messages for a session - NOT IMPLEMENTED IN API YET
+func (m *MessageServiceBridge) DeleteSessionMessages(ctx context.Context, sessionID string) error {
+ // TODO: Not implemented in TypeScript API yet
+ return fmt.Errorf("delete session messages not implemented in API")
+}
+
+// Get retrieves a message by ID - NOT IMPLEMENTED IN API YET
+func (m *MessageServiceBridge) Get(ctx context.Context, id string) (message.Message, error) {
+ // TODO: Not implemented in TypeScript API yet
+ return message.Message{}, fmt.Errorf("get message by ID not implemented in API")
+}
+
+// ListAfter retrieves messages after a timestamp - NOT IMPLEMENTED IN API YET
+func (m *MessageServiceBridge) ListAfter(ctx context.Context, sessionID string, timestamp time.Time) ([]message.Message, error) {
+ // TODO: Not implemented in TypeScript API yet
+ return []message.Message{}, fmt.Errorf("list messages after timestamp not implemented in API")
+}
+
+// Subscribe subscribes to message events
+func (m *MessageServiceBridge) Subscribe(ctx context.Context) <-chan pubsub.Event[message.Message] {
+ return m.broker.Subscribe(ctx)
+} \ No newline at end of file
diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go
index d6f252aad..613c4169a 100644
--- a/internal/tui/components/chat/messages.go
+++ b/internal/tui/components/chat/messages.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math"
+ "strings"
"time"
"github.com/charmbracelet/bubbles/key"
@@ -155,6 +156,55 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
}
+ case app.StorageWriteMsg:
+ // Handle storage write events from the TypeScript backend
+ keyParts := strings.Split(msg.Key, "/")
+ if len(keyParts) >= 4 && keyParts[0] == "session" && keyParts[1] == "message" {
+ sessionID := keyParts[2]
+ if sessionID == m.app.CurrentSession.ID {
+ // Convert storage message to internal format
+ convertedMsg, err := app.ConvertStorageMessage(msg.Content, sessionID)
+ if err != nil {
+ status.Error("Failed to convert message: " + err.Error())
+ return m, nil
+ }
+
+ // Check if message exists
+ messageExists := false
+ messageIndex := -1
+ for i, v := range m.messages {
+ if v.ID == convertedMsg.ID {
+ messageExists = true
+ messageIndex = i
+ break
+ }
+ }
+
+ needsRerender := false
+ if messageExists {
+ // Update existing message
+ m.messages[messageIndex] = *convertedMsg
+ delete(m.cachedContent, convertedMsg.ID)
+ needsRerender = true
+ } else {
+ // Add new message
+ if len(m.messages) > 0 {
+ lastMsgID := m.messages[len(m.messages)-1].ID
+ delete(m.cachedContent, lastMsgID)
+ }
+
+ m.messages = append(m.messages, *convertedMsg)
+ delete(m.cachedContent, m.currentMsgID)
+ m.currentMsgID = convertedMsg.ID
+ needsRerender = true
+ }
+
+ if needsRerender {
+ m.renderView()
+ m.viewport.GotoBottom()
+ }
+ }
+ }
}
spinner, cmd := m.spinner.Update(msg)
diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go
index 973b03ef1..c2b392c24 100644
--- a/internal/tui/components/chat/sidebar.go
+++ b/internal/tui/components/chat/sidebar.go
@@ -10,7 +10,7 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/config"
- "github.com/sst/opencode/internal/diff"
+ // "github.com/sst/opencode/internal/diff"
"github.com/sst/opencode/internal/history"
"github.com/sst/opencode/internal/pubsub"
"github.com/sst/opencode/internal/tui/state"
@@ -28,39 +28,28 @@ type sidebarCmp struct {
}
func (m *sidebarCmp) Init() tea.Cmd {
- if m.app.History != nil {
- ctx := context.Background()
- // Subscribe to file events
- filesCh := m.app.History.Subscribe(ctx)
-
- // Initialize the modified files map
- m.modFiles = make(map[string]struct {
- additions int
- removals int
- })
-
- // Load initial files and calculate diffs
- m.loadModifiedFiles(ctx)
-
- // Return a command that will send file events to the Update method
- return func() tea.Msg {
- return <-filesCh
- }
- }
+ // TODO: History service not implemented in API yet
+ // Initialize the modified files map
+ m.modFiles = make(map[string]struct {
+ additions int
+ removals int
+ })
return nil
}
func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
+ switch msg.(type) {
case state.SessionSelectedMsg:
- ctx := context.Background()
- m.loadModifiedFiles(ctx)
+ // TODO: History service not implemented in API yet
+ // ctx := context.Background()
+ // m.loadModifiedFiles(ctx)
case pubsub.Event[history.File]:
- if msg.Payload.SessionID == m.app.CurrentSession.ID {
- // Process the individual file change instead of reloading all files
- ctx := context.Background()
- m.processFileChanges(ctx, msg.Payload)
- }
+ // TODO: History service not implemented in API yet
+ // if msg.Payload.SessionID == m.app.CurrentSession.ID {
+ // // Process the individual file change instead of reloading all files
+ // ctx := context.Background()
+ // m.processFileChanges(ctx, msg.Payload)
+ // }
}
return m, nil
}
@@ -224,6 +213,9 @@ func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) {
return
}
+ // TODO: History service not implemented in API yet
+ return
+ /*
// Get all latest files for this session
latestFiles, err := m.app.History.ListLatestSessionFiles(ctx, m.app.CurrentSession.ID)
if err != nil {
@@ -235,6 +227,7 @@ func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) {
if err != nil {
return
}
+ */
// Clear the existing map to rebuild it
m.modFiles = make(map[string]struct {
@@ -242,6 +235,7 @@ func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) {
removals int
})
+ /*
// Process each latest file
for _, file := range latestFiles {
// Skip if this is the initial version (no changes to show)
@@ -286,9 +280,13 @@ func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) {
}
}
}
+ */
}
func (m *sidebarCmp) processFileChanges(ctx context.Context, file history.File) {
+ // TODO: History service not implemented in API yet
+ return
+ /*
// Skip if this is the initial version (no changes to show)
if file.Version == history.InitialVersion {
return
@@ -327,16 +325,22 @@ func (m *sidebarCmp) processFileChanges(ctx context.Context, file history.File)
displayPath := getDisplayPath(file.Path)
delete(m.modFiles, displayPath)
}
+ */
}
// Helper function to find the initial version of a file
func (m *sidebarCmp) findInitialVersion(ctx context.Context, path string) (history.File, error) {
+ // TODO: History service not implemented in API yet
+ return history.File{}, fmt.Errorf("history service not implemented")
+ /*
// Get all versions of this file for the session
fileVersions, err := m.app.History.ListBySession(ctx, m.app.CurrentSession.ID)
if err != nil {
return history.File{}, err
}
+ */
+ /*
// Find the initial version
for _, v := range fileVersions {
if v.Path == path && v.Version == history.InitialVersion {
@@ -345,6 +349,7 @@ func (m *sidebarCmp) findInitialVersion(ctx context.Context, path string) (histo
}
return history.File{}, fmt.Errorf("initial version not found")
+ */
}
// Helper function to get the display path for a file
diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go
index e85009205..1fc1daa38 100644
--- a/internal/tui/components/logs/table.go
+++ b/internal/tui/components/logs/table.go
@@ -1,7 +1,8 @@
package logs
import (
- "context"
+ // "context"
+ "fmt"
"log/slog"
"github.com/charmbracelet/bubbles/key"
@@ -41,18 +42,16 @@ func (i *tableCmp) Init() tea.Cmd {
func (i *tableCmp) fetchLogs() tea.Cmd {
return func() tea.Msg {
- ctx := context.Background()
+ // ctx := context.Background()
var logs []logging.Log
var err error
// Limit the number of logs to improve performance
const logLimit = 100
- if i.app.CurrentSession.ID == "" {
- logs, err = i.app.Logs.ListAll(ctx, logLimit)
- } else {
- logs, err = i.app.Logs.ListBySession(ctx, i.app.CurrentSession.ID)
- }
+ // TODO: Logs service not implemented in API yet
+ logs = []logging.Log{}
+ err = fmt.Errorf("logs service not implemented")
if err != nil {
slog.Error("Failed to fetch logs", "error", err)
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index ebed1182a..ed3a94f76 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -2,7 +2,7 @@ package tui
import (
"context"
- "fmt"
+ // "fmt"
"log/slog"
"strings"
@@ -13,7 +13,7 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/config"
- "github.com/sst/opencode/internal/llm/agent"
+ // "github.com/sst/opencode/internal/llm/agent"
"github.com/sst/opencode/internal/logging"
"github.com/sst/opencode/internal/message"
"github.com/sst/opencode/internal/permission"
@@ -28,6 +28,7 @@ import (
"github.com/sst/opencode/internal/tui/page"
"github.com/sst/opencode/internal/tui/state"
"github.com/sst/opencode/internal/tui/util"
+ "github.com/sst/opencode/pkg/client"
)
type keyMap struct {
@@ -251,17 +252,18 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.showPermissions = true
return a, a.permissions.SetPermissions(msg.Payload)
case dialog.PermissionResponseMsg:
- var cmd tea.Cmd
- switch msg.Action {
- case dialog.PermissionAllow:
- a.app.Permissions.Grant(context.Background(), msg.Permission)
- case dialog.PermissionAllowForSession:
- a.app.Permissions.GrantPersistant(context.Background(), msg.Permission)
- case dialog.PermissionDeny:
- a.app.Permissions.Deny(context.Background(), msg.Permission)
- }
+ // TODO: Permissions service not implemented in API yet
+ // var cmd tea.Cmd
+ // switch msg.Action {
+ // case dialog.PermissionAllow:
+ // a.app.Permissions.Grant(context.Background(), msg.Permission)
+ // case dialog.PermissionAllowForSession:
+ // a.app.Permissions.GrantPersistant(context.Background(), msg.Permission)
+ // case dialog.PermissionDeny:
+ // a.app.Permissions.Deny(context.Background(), msg.Permission)
+ // }
a.showPermissions = false
- return a, cmd
+ return a, nil
case page.PageChangeMsg:
return a, a.moveToPage(msg.ID)
@@ -280,6 +282,25 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.app.CurrentSession = &msg.Payload
}
}
+
+ // Handle SSE events from the TypeScript backend
+ case *client.EventStorageWrite:
+ // Process storage write events
+ processedMsg := app.ProcessSSEEvent(msg)
+ if storageMsg, ok := processedMsg.(app.StorageWriteMsg); ok {
+ // Forward to the appropriate page/component based on key
+ keyParts := strings.Split(storageMsg.Key, "/")
+ if len(keyParts) >= 3 && keyParts[0] == "session" {
+ if keyParts[1] == "message" {
+ // This is a message update, forward to the chat page
+ return a.updateAllPages(storageMsg)
+ } else if keyParts[1] == "info" {
+ // This is a session info update
+ return a.updateAllPages(storageMsg)
+ }
+ }
+ }
+ return a, nil
case dialog.CloseQuitMsg:
a.showQuit = false
@@ -321,13 +342,15 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case dialog.ModelSelectedMsg:
a.showModelDialog = false
- model, err := a.app.PrimaryAgent.Update(config.AgentPrimary, msg.Model.ID)
- if err != nil {
- status.Error(err.Error())
- return a, nil
- }
+ // TODO: Agent model update not implemented in API yet
+ // model, err := a.app.PrimaryAgent.Update(config.AgentPrimary, msg.Model.ID)
+ // if err != nil {
+ // status.Error(err.Error())
+ // return a, nil
+ // }
- status.Info(fmt.Sprintf("Model changed to %s", model.Name))
+ // status.Info(fmt.Sprintf("Model changed to %s", model.Name))
+ status.Info("Model selection not implemented in API yet")
return a, nil
case dialog.ShowInitDialogMsg:
@@ -707,6 +730,9 @@ func (a *appModel) RegisterCommand(cmd dialog.Command) {
// getAvailableToolNames returns a list of all available tool names
func getAvailableToolNames(app *app.App) []string {
+ // TODO: Tools not implemented in API yet
+ return []string{"Tools not available in API mode"}
+ /*
// Get primary agent tools (which already include MCP tools)
allTools := agent.PrimaryAgentTools(
app.Permissions,
@@ -723,6 +749,7 @@ func getAvailableToolNames(app *app.App) []string {
}
return toolNames
+ */
}
func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {