wip: refactoring tui

This commit is contained in:
adamdottv
2025-05-28 15:36:31 -05:00
parent 8863a499a9
commit 9d7c5efb9b
21 changed files with 136 additions and 1346 deletions

177
internal/tui/app/app.go Normal file
View File

@@ -0,0 +1,177 @@
package app
import (
"context"
"maps"
"sync"
"time"
"log/slog"
"github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/fileutil"
"github.com/sst/opencode/internal/lsp"
"github.com/sst/opencode/internal/session"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/tui/theme"
"github.com/sst/opencode/pkg/client"
)
type App struct {
State map[string]any
CurrentSession *session.Session
Logs any // TODO: Define LogService interface when needed
Sessions SessionService
Messages MessageService
History any // TODO: Define HistoryService interface when needed
Permissions any // TODO: Define PermissionService interface when needed
Status status.Service
Client *client.ClientWithResponses
Events *client.Client
PrimaryAgent AgentService
LSPClients map[string]*lsp.Client
clientsMutex sync.RWMutex
watcherCancelFuncs []context.CancelFunc
cancelFuncsMutex sync.Mutex
watcherWG sync.WaitGroup
// UI state
filepickerOpen bool
completionDialogOpen bool
}
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()
// Create HTTP client
url := "http://localhost:16713"
httpClient, err := client.NewClientWithResponses(url)
if err != nil {
slog.Error("Failed to create client", "error", err)
return nil, err
}
eventClient, err := client.NewClient(url)
if err != nil {
slog.Error("Failed to create event client", "error", err)
return nil, err
}
// Create service bridges
sessionBridge := NewSessionServiceBridge(httpClient)
messageBridge := NewMessageServiceBridge(httpClient)
agentBridge := NewAgentServiceBridge(httpClient)
app := &App{
State: make(map[string]any),
Client: httpClient,
Events: eventClient,
CurrentSession: &session.Session{},
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
app.initTheme()
// 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
}
// initTheme sets the application theme based on the configuration
func (app *App) initTheme() {
cfg := config.Get()
if cfg == nil || cfg.TUI.Theme == "" {
return // Use default theme
}
// Try to set the theme from config
err := theme.SetTheme(cfg.TUI.Theme)
if err != nil {
slog.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err)
} else {
slog.Debug("Set theme from config", "theme", cfg.TUI.Theme)
}
}
// IsFilepickerOpen returns whether the filepicker is currently open
func (app *App) IsFilepickerOpen() bool {
return app.filepickerOpen
}
// SetFilepickerOpen sets the state of the filepicker
func (app *App) SetFilepickerOpen(open bool) {
app.filepickerOpen = open
}
// IsCompletionDialogOpen returns whether the completion dialog is currently open
func (app *App) IsCompletionDialogOpen() bool {
return app.completionDialogOpen
}
// SetCompletionDialogOpen sets the state of the completion dialog
func (app *App) SetCompletionDialogOpen(open bool) {
app.completionDialogOpen = open
}
// Shutdown performs a clean shutdown of the application
func (app *App) Shutdown() {
// Cancel all watcher goroutines
app.cancelFuncsMutex.Lock()
for _, cancel := range app.watcherCancelFuncs {
cancel()
}
app.cancelFuncsMutex.Unlock()
app.watcherWG.Wait()
// Perform additional cleanup for LSP clients
app.clientsMutex.RLock()
clients := make(map[string]*lsp.Client, len(app.LSPClients))
maps.Copy(clients, app.LSPClients)
app.clientsMutex.RUnlock()
for name, client := range clients {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := client.Shutdown(shutdownCtx); err != nil {
slog.Error("Failed to shutdown LSP client", "name", name, "error", err)
}
cancel()
}
}

246
internal/tui/app/bridge.go Normal file
View File

@@ -0,0 +1,246 @@
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.ClientWithResponses
}
// NewSessionServiceBridge creates a new session service bridge
func NewSessionServiceBridge(client *client.ClientWithResponses) *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.PostSessionCreateWithResponse(ctx)
if err != nil {
return session.Session{}, err
}
if resp.StatusCode() != 200 {
return session.Session{}, fmt.Errorf("failed to create session: %d", resp.StatusCode())
}
info := resp.JSON200
// 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.PostSessionListWithResponse(ctx)
if err != nil {
return nil, err
}
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("failed to list sessions: %d", resp.StatusCode())
}
if resp.JSON200 == nil {
return []session.Session{}, nil
}
infos := *resp.JSON200
// 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.ClientWithResponses
}
// NewAgentServiceBridge creates a new agent service bridge
func NewAgentServiceBridge(client *client.ClientWithResponses) *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")
}
part := client.SessionMessagePart{}
part.FromSessionMessagePartText(client.SessionMessagePartText{
Type: "text",
Text: text,
})
parts := []client.SessionMessagePart{part}
go a.client.PostSessionChatWithResponse(ctx, client.PostSessionChatJSONRequestBody{
SessionID: sessionID,
Parts: parts,
ProviderID: "anthropic",
ModelID: "claude-sonnet-4-20250514",
})
// 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.ClientWithResponses
broker *pubsub.Broker[message.Message]
}
// NewMessageServiceBridge creates a new message service bridge
func NewMessageServiceBridge(client *client.ClientWithResponses) *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 any
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)
}

View File

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