app.go 5.4 KB

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