Browse Source

wip: refactoring tui

adamdottv 9 months ago
parent
commit
5e738ce7d3

+ 102 - 0
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

+ 0 - 292
cmd/non_interactive_mode.go

@@ -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
-}

+ 5 - 60
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() {

+ 44 - 62
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
 }

+ 203 - 0
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
+}

+ 158 - 0
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
+}

+ 42 - 0
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
+}

+ 250 - 0
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)
+}

+ 50 - 0
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)

+ 33 - 28
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

+ 6 - 7
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)

+ 45 - 18
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 {