mirror of
https://github.com/sst/opencode.git
synced 2025-08-31 02:07:24 +00:00
231 lines
6.1 KiB
Go
231 lines
6.1 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"log/slog"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/sst/opencode/internal/config"
|
|
"github.com/sst/opencode/internal/fileutil"
|
|
"github.com/sst/opencode/internal/message"
|
|
"github.com/sst/opencode/internal/session"
|
|
"github.com/sst/opencode/internal/status"
|
|
"github.com/sst/opencode/internal/tui/state"
|
|
"github.com/sst/opencode/internal/tui/theme"
|
|
"github.com/sst/opencode/internal/tui/util"
|
|
"github.com/sst/opencode/pkg/client"
|
|
)
|
|
|
|
type App struct {
|
|
Client *client.ClientWithResponses
|
|
Events *client.Client
|
|
State map[string]any
|
|
Session *client.SessionInfo
|
|
Messages []client.MessageInfo
|
|
|
|
CurrentSessionOLD *session.Session
|
|
SessionsOLD SessionService
|
|
MessagesOLD MessageService
|
|
LogsOLD any // TODO: Define LogService interface when needed
|
|
HistoryOLD any // TODO: Define HistoryService interface when needed
|
|
PermissionsOLD any // TODO: Define PermissionService interface when needed
|
|
Status status.Service
|
|
|
|
PrimaryAgentOLD AgentService
|
|
|
|
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,
|
|
Session: &client.SessionInfo{},
|
|
CurrentSessionOLD: &session.Session{},
|
|
SessionsOLD: sessionBridge,
|
|
MessagesOLD: messageBridge,
|
|
PrimaryAgentOLD: agentBridge,
|
|
Status: status.GetService(),
|
|
|
|
// TODO: These services need API endpoints:
|
|
LogsOLD: nil, // logging.GetService(),
|
|
HistoryOLD: nil, // history.GetService(),
|
|
PermissionsOLD: nil, // permission.GetService(),
|
|
}
|
|
|
|
// Initialize theme based on configuration
|
|
app.initTheme()
|
|
|
|
return app, nil
|
|
}
|
|
|
|
// Create creates a new session
|
|
func (a *App) SendChatMessage(ctx context.Context, text string, attachments []message.Attachment) tea.Cmd {
|
|
var cmds []tea.Cmd
|
|
if a.Session.Id == "" {
|
|
resp, err := a.Client.PostSessionCreateWithResponse(ctx)
|
|
if err != nil {
|
|
status.Error(err.Error())
|
|
return nil
|
|
}
|
|
if resp.StatusCode() != 200 {
|
|
status.Error(fmt.Sprintf("failed to create session: %d", resp.StatusCode()))
|
|
return nil
|
|
}
|
|
|
|
info := resp.JSON200
|
|
a.Session = info
|
|
|
|
// Convert to old session type for backwards compatibility
|
|
newSession := 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
|
|
}
|
|
a.CurrentSessionOLD = &newSession
|
|
|
|
cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(&newSession)))
|
|
}
|
|
|
|
// TODO: Handle attachments when API supports them
|
|
if len(attachments) > 0 {
|
|
// For now, ignore attachments
|
|
// return "", fmt.Errorf("attachments not supported yet")
|
|
}
|
|
|
|
part := client.MessagePart{}
|
|
part.FromMessagePartText(client.MessagePartText{
|
|
Type: "text",
|
|
Text: text,
|
|
})
|
|
parts := []client.MessagePart{part}
|
|
|
|
go a.Client.PostSessionChatWithResponse(ctx, client.PostSessionChatJSONRequestBody{
|
|
SessionID: a.Session.Id,
|
|
Parts: parts,
|
|
ProviderID: "anthropic",
|
|
ModelID: "claude-sonnet-4-20250514",
|
|
})
|
|
|
|
// The actual response will come through SSE
|
|
// For now, just return success
|
|
|
|
return tea.Batch(cmds...)
|
|
}
|
|
|
|
func (a *App) ListSessions(ctx context.Context) ([]session.Session, error) {
|
|
resp, err := a.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
|
|
}
|
|
|
|
// 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()
|
|
}
|