summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-05-28 10:12:08 -0500
committeradamdottv <[email protected]>2025-05-28 15:36:35 -0500
commit5e738ce7d3b66b88a407a1bcc53a5169d0f4a904 (patch)
tree5c7a6ac13ebb60946e7c88f7d0e04df766edf94a
parent641e9ff6642ef7b55928ef235f54d53822045baf (diff)
downloadopencode-5e738ce7d3b66b88a407a1bcc53a5169d0f4a904.tar.gz
opencode-5e738ce7d3b66b88a407a1bcc53a5169d0f4a904.zip
wip: refactoring tui
-rw-r--r--MISSING_API_FEATURES.md102
-rw-r--r--cmd/non_interactive_mode.go292
-rw-r--r--cmd/root.go65
-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
12 files changed, 938 insertions, 467 deletions
diff --git a/MISSING_API_FEATURES.md b/MISSING_API_FEATURES.md
new file mode 100644
index 000000000..fcdccf32c
--- /dev/null
+++ b/MISSING_API_FEATURES.md
@@ -0,0 +1,102 @@
+# Missing API Features for TypeScript Backend
+
+This document tracks features that need to be implemented in the TypeScript backend to support the existing Go TUI functionality.
+
+## Current API Endpoints Available
+- `/session_create` - Create a new session
+- `/session_share` - Share a session
+- `/session_messages` - Get messages for a session
+- `/session_list` - List all sessions
+- `/session_chat` - Send a chat message (with SSE streaming response)
+- `/event` - SSE event stream (currently only supports `storage.write` events)
+
+## Missing Features
+
+### Session Management
+- [ ] Session deletion
+- [ ] Session renaming/updating title
+- [ ] Session compaction/summarization
+- [ ] Session export/import
+
+### Message Management
+- [ ] Message editing
+- [ ] Message deletion
+- [ ] Message retrieval by ID
+- [ ] Message search/filtering
+- [ ] System messages support
+
+### Agent/LLM Features
+- [ ] Model selection/switching
+- [ ] Tool invocation support
+- [ ] Agent state management (busy/idle)
+- [ ] Cancel ongoing generation
+- [ ] Token usage tracking per message
+- [ ] Custom prompts/system messages
+
+### File/Attachment Support
+- [ ] File attachments in messages
+- [ ] Image attachments
+- [ ] Code snippet attachments
+- [ ] Attachment storage/retrieval
+
+### LSP Integration
+- [ ] LSP server discovery
+- [ ] LSP diagnostics
+- [ ] LSP code actions
+- [ ] LSP hover information
+- [ ] LSP references
+- [ ] LSP workspace symbols
+
+### Configuration
+- [ ] Model configuration
+- [ ] API key management
+- [ ] Theme preferences
+- [ ] User preferences storage
+
+### Permissions
+- [ ] File system access permissions
+- [ ] Command execution permissions
+- [ ] Network access permissions
+
+### Status/Notifications
+- [ ] Status message broadcasting
+- [ ] Error notifications
+- [ ] Progress indicators
+
+### History
+- [ ] Command history
+- [ ] Search history
+- [ ] Recent files/folders
+
+### Events (SSE)
+Currently only `storage.write` is supported. Missing events:
+- [ ] `session.created`
+- [ ] `session.updated`
+- [ ] `session.deleted`
+- [ ] `message.created`
+- [ ] `message.updated`
+- [ ] `message.deleted`
+- [ ] `agent.status` (busy/idle)
+- [ ] `tool.invoked`
+- [ ] `tool.result`
+- [ ] `error`
+- [ ] `status` (info/warning/error messages)
+- [ ] `lsp.diagnostics`
+- [ ] `permission.requested`
+- [ ] `permission.granted`
+- [ ] `permission.denied`
+
+### Database/Storage
+- [ ] Message persistence
+- [ ] Session persistence
+- [ ] File tracking
+- [ ] Log storage
+
+### Pubsub/Real-time Updates
+- [ ] Publish message events when messages are created/updated via API
+- [ ] Agent busy/idle status updates
+
+### Misc
+- [ ] Health check endpoint
+- [ ] Version endpoint
+- [ ] Metrics/telemetry \ No newline at end of file
diff --git a/cmd/non_interactive_mode.go b/cmd/non_interactive_mode.go
deleted file mode 100644
index 5023839c3..000000000
--- a/cmd/non_interactive_mode.go
+++ /dev/null
@@ -1,292 +0,0 @@
-package cmd
-
-import (
- "context"
- "fmt"
- "io"
- "os"
- "sync"
- "time"
-
- "log/slog"
-
- charmlog "github.com/charmbracelet/log"
- "github.com/sst/opencode/internal/app"
- "github.com/sst/opencode/internal/config"
- "github.com/sst/opencode/internal/db"
- "github.com/sst/opencode/internal/format"
- "github.com/sst/opencode/internal/llm/agent"
- "github.com/sst/opencode/internal/llm/tools"
- "github.com/sst/opencode/internal/message"
- "github.com/sst/opencode/internal/permission"
- "github.com/sst/opencode/internal/tui/components/spinner"
- "github.com/sst/opencode/internal/tui/theme"
-)
-
-// syncWriter is a thread-safe writer that prevents interleaved output
-type syncWriter struct {
- w io.Writer
- mu sync.Mutex
-}
-
-// Write implements io.Writer
-func (sw *syncWriter) Write(p []byte) (n int, err error) {
- sw.mu.Lock()
- defer sw.mu.Unlock()
- return sw.w.Write(p)
-}
-
-// newSyncWriter creates a new synchronized writer
-func newSyncWriter(w io.Writer) io.Writer {
- return &syncWriter{w: w}
-}
-
-// filterTools filters the provided tools based on allowed or excluded tool names
-func filterTools(allTools []tools.BaseTool, allowedTools, excludedTools []string) []tools.BaseTool {
- // If neither allowed nor excluded tools are specified, return all tools
- if len(allowedTools) == 0 && len(excludedTools) == 0 {
- return allTools
- }
-
- // Create a map for faster lookups
- allowedMap := make(map[string]bool)
- for _, name := range allowedTools {
- allowedMap[name] = true
- }
-
- excludedMap := make(map[string]bool)
- for _, name := range excludedTools {
- excludedMap[name] = true
- }
-
- var filteredTools []tools.BaseTool
-
- for _, tool := range allTools {
- toolName := tool.Info().Name
-
- // If we have an allowed list, only include tools in that list
- if len(allowedTools) > 0 {
- if allowedMap[toolName] {
- filteredTools = append(filteredTools, tool)
- }
- } else if len(excludedTools) > 0 {
- // If we have an excluded list, include all tools except those in the list
- if !excludedMap[toolName] {
- filteredTools = append(filteredTools, tool)
- }
- }
- }
-
- return filteredTools
-}
-
-// handleNonInteractiveMode processes a single prompt in non-interactive mode
-func handleNonInteractiveMode(ctx context.Context, prompt string, outputFormat format.OutputFormat, quiet bool, verbose bool, allowedTools, excludedTools []string) error {
- // Initial log message using standard slog
- slog.Info("Running in non-interactive mode", "prompt", prompt, "format", outputFormat, "quiet", quiet, "verbose", verbose,
- "allowedTools", allowedTools, "excludedTools", excludedTools)
-
- // Sanity check for mutually exclusive flags
- if quiet && verbose {
- return fmt.Errorf("--quiet and --verbose flags cannot be used together")
- }
-
- // Set up logging to stderr if verbose mode is enabled
- if verbose {
- // Create a synchronized writer to prevent interleaved output
- syncWriter := newSyncWriter(os.Stderr)
-
- // Create a charmbracelet/log logger that writes to the synchronized writer
- charmLogger := charmlog.NewWithOptions(syncWriter, charmlog.Options{
- Level: charmlog.DebugLevel,
- ReportCaller: true,
- ReportTimestamp: true,
- TimeFormat: time.RFC3339,
- Prefix: "OpenCode",
- })
-
- // Set the global logger for charmbracelet/log
- charmlog.SetDefault(charmLogger)
-
- // Create a slog handler that uses charmbracelet/log
- // This will forward all slog logs to charmbracelet/log
- slog.SetDefault(slog.New(charmLogger))
-
- // Log a message to confirm verbose logging is enabled
- charmLogger.Info("Verbose logging enabled")
- }
-
- // Start spinner if not in quiet mode
- var s *spinner.Spinner
- if !quiet {
- // Get the current theme to style the spinner
- currentTheme := theme.CurrentTheme()
-
- // Create a themed spinner
- if currentTheme != nil {
- // Use the primary color from the theme
- s = spinner.NewThemedSpinner("Thinking...", currentTheme.Primary())
- } else {
- // Fallback to default spinner if no theme is available
- s = spinner.NewSpinner("Thinking...")
- }
-
- s.Start()
- defer s.Stop()
- }
-
- // Connect DB, this will also run migrations
- conn, err := db.Connect()
- if err != nil {
- return err
- }
-
- // Create a context with cancellation
- ctx, cancel := context.WithCancel(ctx)
- defer cancel()
-
- // Create the app
- app, err := app.New(ctx, conn)
- if err != nil {
- slog.Error("Failed to create app", "error", err)
- return err
- }
-
- // Create a new session for this prompt
- session, err := app.Sessions.Create(ctx, "Non-interactive prompt")
- if err != nil {
- return fmt.Errorf("failed to create session: %w", err)
- }
-
- // Set the session as current
- app.CurrentSession = &session
-
- // Auto-approve all permissions for this session
- permission.AutoApproveSession(ctx, session.ID)
-
- // Create the user message
- _, err = app.Messages.Create(ctx, session.ID, message.CreateMessageParams{
- Role: message.User,
- Parts: []message.ContentPart{message.TextContent{Text: prompt}},
- })
- if err != nil {
- return fmt.Errorf("failed to create message: %w", err)
- }
-
- // If tool restrictions are specified, create a new agent with filtered tools
- if len(allowedTools) > 0 || len(excludedTools) > 0 {
- // Initialize MCP tools synchronously to ensure they're included in filtering
- mcpCtx, mcpCancel := context.WithTimeout(ctx, 10*time.Second)
- agent.GetMcpTools(mcpCtx, app.Permissions)
- mcpCancel()
-
- // Get all available tools including MCP tools
- allTools := agent.PrimaryAgentTools(
- app.Permissions,
- app.Sessions,
- app.Messages,
- app.History,
- app.LSPClients,
- )
-
- // Filter tools based on allowed/excluded lists
- filteredTools := filterTools(allTools, allowedTools, excludedTools)
-
- // Log the filtered tools for debugging
- var toolNames []string
- for _, tool := range filteredTools {
- toolNames = append(toolNames, tool.Info().Name)
- }
- slog.Debug("Using filtered tools", "count", len(filteredTools), "tools", toolNames)
-
- // Create a new agent with the filtered tools
- restrictedAgent, err := agent.NewAgent(
- config.AgentPrimary,
- app.Sessions,
- app.Messages,
- filteredTools,
- )
- if err != nil {
- return fmt.Errorf("failed to create restricted agent: %w", err)
- }
-
- // Use the restricted agent for this request
- eventCh, err := restrictedAgent.Run(ctx, session.ID, prompt)
- if err != nil {
- return fmt.Errorf("failed to run restricted agent: %w", err)
- }
-
- // Wait for the response
- var response message.Message
- for event := range eventCh {
- if event.Err() != nil {
- return fmt.Errorf("agent error: %w", event.Err())
- }
- response = event.Response()
- }
-
- // Format and print the output
- content := ""
- if textContent := response.Content(); textContent != nil {
- content = textContent.Text
- }
-
- formattedOutput, err := format.FormatOutput(content, outputFormat)
- if err != nil {
- return fmt.Errorf("failed to format output: %w", err)
- }
-
- // Stop spinner before printing output
- if !quiet && s != nil {
- s.Stop()
- }
-
- // Print the formatted output to stdout
- fmt.Println(formattedOutput)
-
- // Shutdown the app
- app.Shutdown()
-
- return nil
- }
-
- // Run the default agent if no tool restrictions
- eventCh, err := app.PrimaryAgent.Run(ctx, session.ID, prompt)
- if err != nil {
- return fmt.Errorf("failed to run agent: %w", err)
- }
-
- // Wait for the response
- var response message.Message
- for event := range eventCh {
- if event.Err() != nil {
- return fmt.Errorf("agent error: %w", event.Err())
- }
- response = event.Response()
- }
-
- // Get the text content from the response
- content := ""
- if textContent := response.Content(); textContent != nil {
- content = textContent.Text
- }
-
- // Format the output according to the specified format
- formattedOutput, err := format.FormatOutput(content, outputFormat)
- if err != nil {
- return fmt.Errorf("failed to format output: %w", err)
- }
-
- // Stop spinner before printing output
- if !quiet && s != nil {
- s.Stop()
- }
-
- // Print the formatted output to stdout
- fmt.Println(formattedOutput)
-
- // Shutdown the app
- app.Shutdown()
-
- return nil
-}
diff --git a/cmd/root.go b/cmd/root.go
index a6b82c409..7c7795552 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -15,9 +15,6 @@ import (
"github.com/spf13/cobra"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/config"
- "github.com/sst/opencode/internal/db"
- "github.com/sst/opencode/internal/format"
- "github.com/sst/opencode/internal/llm/agent"
"github.com/sst/opencode/internal/logging"
"github.com/sst/opencode/internal/lsp/discovery"
"github.com/sst/opencode/internal/pubsub"
@@ -90,52 +87,17 @@ to assist developers in writing, debugging, and understanding code directly from
return err
}
- // Check if we're in non-interactive mode
- prompt, _ := cmd.Flags().GetString("prompt")
-
- // Check for piped input if no prompt was provided via flag
- if prompt == "" {
- pipedInput, hasPipedInput := checkStdinPipe()
- if hasPipedInput {
- prompt = pipedInput
- }
- }
-
- // If we have a prompt (either from flag or piped input), run in non-interactive mode
- if prompt != "" {
- outputFormatStr, _ := cmd.Flags().GetString("output-format")
- outputFormat := format.OutputFormat(outputFormatStr)
- if !outputFormat.IsValid() {
- return fmt.Errorf("invalid output format: %s", outputFormatStr)
- }
-
- quiet, _ := cmd.Flags().GetBool("quiet")
- verbose, _ := cmd.Flags().GetBool("verbose")
-
- // Get tool restriction flags
- allowedTools, _ := cmd.Flags().GetStringSlice("allowedTools")
- excludedTools, _ := cmd.Flags().GetStringSlice("excludedTools")
-
- return handleNonInteractiveMode(cmd.Context(), prompt, outputFormat, quiet, verbose, allowedTools, excludedTools)
- }
-
// Run LSP auto-discovery
if err := discovery.IntegrateLSPServers(cwd); err != nil {
slog.Warn("Failed to auto-discover LSP servers", "error", err)
// Continue anyway, this is not a fatal error
}
- // Connect DB, this will also run migrations
- conn, err := db.Connect()
- if err != nil {
- return err
- }
-
// Create main context for the application
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- app, err := app.New(ctx, conn)
+ app, err := app.New(ctx)
if err != nil {
slog.Error("Failed to create app", "error", err)
return err
@@ -149,9 +111,6 @@ to assist developers in writing, debugging, and understanding code directly from
tea.WithAltScreen(),
)
- // Initialize MCP tools in the background
- initMCPTools(ctx, app)
-
// Setup the subscriptions, this will send services events to the TUI
ch, cancelSubs := setupSubscriptions(app, ctx)
@@ -234,20 +193,6 @@ func attemptTUIRecovery(program *tea.Program) {
program.Quit()
}
-func initMCPTools(ctx context.Context, app *app.App) {
- go func() {
- defer logging.RecoverPanic("MCP-goroutine", nil)
-
- // Create a context with timeout for the initial MCP tools fetch
- ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
- defer cancel()
-
- // Set this up once with proper error handling
- agent.GetMcpTools(ctxWithTimeout, app.Permissions)
- slog.Info("MCP message handling goroutine exiting")
- }()
-}
-
func setupSubscriber[T any](
ctx context.Context,
wg *sync.WaitGroup,
@@ -298,10 +243,10 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
wg := sync.WaitGroup{}
ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
- setupSubscriber(ctx, &wg, "logging", app.Logs.Subscribe, ch)
- setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
- setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
- setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
+ // setupSubscriber(ctx, &wg, "logging", app.Logs.Subscribe, ch)
+ // setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
+ // setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
+ // setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
setupSubscriber(ctx, &wg, "status", app.Status.Subscribe, ch)
cleanupFunc := func() {
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 {