app.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. package app
  2. import (
  3. "context"
  4. "fmt"
  5. "maps"
  6. "sync"
  7. "time"
  8. "log/slog"
  9. tea "github.com/charmbracelet/bubbletea"
  10. "github.com/sst/opencode/internal/config"
  11. "github.com/sst/opencode/internal/fileutil"
  12. "github.com/sst/opencode/internal/lsp"
  13. "github.com/sst/opencode/internal/message"
  14. "github.com/sst/opencode/internal/session"
  15. "github.com/sst/opencode/internal/status"
  16. "github.com/sst/opencode/internal/tui/state"
  17. "github.com/sst/opencode/internal/tui/theme"
  18. "github.com/sst/opencode/internal/tui/util"
  19. "github.com/sst/opencode/pkg/client"
  20. )
  21. type App struct {
  22. State map[string]any
  23. CurrentSession *session.Session
  24. Logs any // TODO: Define LogService interface when needed
  25. Sessions SessionService
  26. Messages MessageService
  27. History any // TODO: Define HistoryService interface when needed
  28. Permissions any // TODO: Define PermissionService interface when needed
  29. Status status.Service
  30. Client *client.ClientWithResponses
  31. Events *client.Client
  32. PrimaryAgent AgentService
  33. LSPClients map[string]*lsp.Client
  34. clientsMutex sync.RWMutex
  35. watcherCancelFuncs []context.CancelFunc
  36. cancelFuncsMutex sync.Mutex
  37. watcherWG sync.WaitGroup
  38. // UI state
  39. filepickerOpen bool
  40. completionDialogOpen bool
  41. }
  42. func New(ctx context.Context) (*App, error) {
  43. // Initialize status service (still needed for UI notifications)
  44. err := status.InitService()
  45. if err != nil {
  46. slog.Error("Failed to initialize status service", "error", err)
  47. return nil, err
  48. }
  49. // Initialize file utilities
  50. fileutil.Init()
  51. // Create HTTP client
  52. url := "http://localhost:16713"
  53. httpClient, err := client.NewClientWithResponses(url)
  54. if err != nil {
  55. slog.Error("Failed to create client", "error", err)
  56. return nil, err
  57. }
  58. eventClient, err := client.NewClient(url)
  59. if err != nil {
  60. slog.Error("Failed to create event client", "error", err)
  61. return nil, err
  62. }
  63. // Create service bridges
  64. sessionBridge := NewSessionServiceBridge(httpClient)
  65. messageBridge := NewMessageServiceBridge(httpClient)
  66. agentBridge := NewAgentServiceBridge(httpClient)
  67. app := &App{
  68. State: make(map[string]any),
  69. Client: httpClient,
  70. Events: eventClient,
  71. CurrentSession: &session.Session{},
  72. Sessions: sessionBridge,
  73. Messages: messageBridge,
  74. PrimaryAgent: agentBridge,
  75. Status: status.GetService(),
  76. LSPClients: make(map[string]*lsp.Client),
  77. // TODO: These services need API endpoints:
  78. Logs: nil, // logging.GetService(),
  79. History: nil, // history.GetService(),
  80. Permissions: nil, // permission.GetService(),
  81. }
  82. // Initialize theme based on configuration
  83. app.initTheme()
  84. return app, nil
  85. }
  86. // Create creates a new session
  87. func (a *App) SendChatMessage(ctx context.Context, text string, attachments []message.Attachment) tea.Cmd {
  88. var cmds []tea.Cmd
  89. if a.CurrentSession.ID == "" {
  90. resp, err := a.Client.PostSessionCreateWithResponse(ctx)
  91. if err != nil {
  92. // return session.Session{}, err
  93. }
  94. if resp.StatusCode() != 200 {
  95. // return session.Session{}, fmt.Errorf("failed to create session: %d", resp.StatusCode())
  96. }
  97. info := resp.JSON200
  98. // Convert to old session type
  99. newSession := session.Session{
  100. ID: info.Id,
  101. Title: info.Title,
  102. CreatedAt: time.Now(), // API doesn't provide this yet
  103. UpdatedAt: time.Now(), // API doesn't provide this yet
  104. }
  105. if err != nil {
  106. status.Error(err.Error())
  107. return nil
  108. }
  109. a.CurrentSession = &newSession
  110. cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(&newSession)))
  111. }
  112. // TODO: Handle attachments when API supports them
  113. if len(attachments) > 0 {
  114. // For now, ignore attachments
  115. // return "", fmt.Errorf("attachments not supported yet")
  116. }
  117. part := client.SessionMessagePart{}
  118. part.FromSessionMessagePartText(client.SessionMessagePartText{
  119. Type: "text",
  120. Text: text,
  121. })
  122. parts := []client.SessionMessagePart{part}
  123. go a.Client.PostSessionChatWithResponse(ctx, client.PostSessionChatJSONRequestBody{
  124. SessionID: a.CurrentSession.ID,
  125. Parts: parts,
  126. ProviderID: "anthropic",
  127. ModelID: "claude-sonnet-4-20250514",
  128. })
  129. // The actual response will come through SSE
  130. // For now, just return success
  131. return tea.Batch(cmds...)
  132. }
  133. func (a *App) ListSessions(ctx context.Context) ([]session.Session, error) {
  134. resp, err := a.Client.PostSessionListWithResponse(ctx)
  135. if err != nil {
  136. return nil, err
  137. }
  138. if resp.StatusCode() != 200 {
  139. return nil, fmt.Errorf("failed to list sessions: %d", resp.StatusCode())
  140. }
  141. if resp.JSON200 == nil {
  142. return []session.Session{}, nil
  143. }
  144. infos := *resp.JSON200
  145. // Convert to old session type
  146. sessions := make([]session.Session, len(infos))
  147. for i, info := range infos {
  148. sessions[i] = session.Session{
  149. ID: info.Id,
  150. Title: info.Title,
  151. CreatedAt: time.Now(), // API doesn't provide this yet
  152. UpdatedAt: time.Now(), // API doesn't provide this yet
  153. }
  154. }
  155. return sessions, nil
  156. }
  157. // initTheme sets the application theme based on the configuration
  158. func (app *App) initTheme() {
  159. cfg := config.Get()
  160. if cfg == nil || cfg.TUI.Theme == "" {
  161. return // Use default theme
  162. }
  163. // Try to set the theme from config
  164. err := theme.SetTheme(cfg.TUI.Theme)
  165. if err != nil {
  166. slog.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err)
  167. } else {
  168. slog.Debug("Set theme from config", "theme", cfg.TUI.Theme)
  169. }
  170. }
  171. // IsFilepickerOpen returns whether the filepicker is currently open
  172. func (app *App) IsFilepickerOpen() bool {
  173. return app.filepickerOpen
  174. }
  175. // SetFilepickerOpen sets the state of the filepicker
  176. func (app *App) SetFilepickerOpen(open bool) {
  177. app.filepickerOpen = open
  178. }
  179. // IsCompletionDialogOpen returns whether the completion dialog is currently open
  180. func (app *App) IsCompletionDialogOpen() bool {
  181. return app.completionDialogOpen
  182. }
  183. // SetCompletionDialogOpen sets the state of the completion dialog
  184. func (app *App) SetCompletionDialogOpen(open bool) {
  185. app.completionDialogOpen = open
  186. }
  187. // Shutdown performs a clean shutdown of the application
  188. func (app *App) Shutdown() {
  189. // Cancel all watcher goroutines
  190. app.cancelFuncsMutex.Lock()
  191. for _, cancel := range app.watcherCancelFuncs {
  192. cancel()
  193. }
  194. app.cancelFuncsMutex.Unlock()
  195. app.watcherWG.Wait()
  196. // Perform additional cleanup for LSP clients
  197. app.clientsMutex.RLock()
  198. clients := make(map[string]*lsp.Client, len(app.LSPClients))
  199. maps.Copy(clients, app.LSPClients)
  200. app.clientsMutex.RUnlock()
  201. for name, client := range clients {
  202. shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  203. if err := client.Shutdown(shutdownCtx); err != nil {
  204. slog.Error("Failed to shutdown LSP client", "name", name, "error", err)
  205. }
  206. cancel()
  207. }
  208. }