app.go 5.6 KB

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