app.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. package app
  2. import (
  3. "context"
  4. "database/sql"
  5. "maps"
  6. "sync"
  7. "time"
  8. "log/slog"
  9. "github.com/sst/opencode/internal/config"
  10. "github.com/sst/opencode/internal/fileutil"
  11. "github.com/sst/opencode/internal/history"
  12. "github.com/sst/opencode/internal/llm/agent"
  13. "github.com/sst/opencode/internal/logging"
  14. "github.com/sst/opencode/internal/lsp"
  15. "github.com/sst/opencode/internal/message"
  16. "github.com/sst/opencode/internal/permission"
  17. "github.com/sst/opencode/internal/session"
  18. "github.com/sst/opencode/internal/status"
  19. "github.com/sst/opencode/internal/tui/theme"
  20. "github.com/sst/opencode/pkg/client"
  21. )
  22. type App struct {
  23. CurrentSession *session.Session
  24. Logs logging.Service
  25. Sessions session.Service
  26. Messages message.Service
  27. History history.Service
  28. Permissions permission.Service
  29. Status status.Service
  30. Client *client.Client
  31. PrimaryAgent agent.Service
  32. LSPClients map[string]*lsp.Client
  33. clientsMutex sync.RWMutex
  34. watcherCancelFuncs []context.CancelFunc
  35. cancelFuncsMutex sync.Mutex
  36. watcherWG sync.WaitGroup
  37. // UI state
  38. filepickerOpen bool
  39. completionDialogOpen bool
  40. }
  41. func New(ctx context.Context, conn *sql.DB) (*App, error) {
  42. err := logging.InitService(conn)
  43. if err != nil {
  44. slog.Error("Failed to initialize logging service", "error", err)
  45. return nil, err
  46. }
  47. err = session.InitService(conn)
  48. if err != nil {
  49. slog.Error("Failed to initialize session service", "error", err)
  50. return nil, err
  51. }
  52. err = message.InitService(conn)
  53. if err != nil {
  54. slog.Error("Failed to initialize message service", "error", err)
  55. return nil, err
  56. }
  57. err = history.InitService(conn)
  58. if err != nil {
  59. slog.Error("Failed to initialize history service", "error", err)
  60. return nil, err
  61. }
  62. err = permission.InitService()
  63. if err != nil {
  64. slog.Error("Failed to initialize permission service", "error", err)
  65. return nil, err
  66. }
  67. err = status.InitService()
  68. if err != nil {
  69. slog.Error("Failed to initialize status service", "error", err)
  70. return nil, err
  71. }
  72. fileutil.Init()
  73. client, err := client.NewClient("http://localhost:16713")
  74. if err != nil {
  75. slog.Error("Failed to create client", "error", err)
  76. return nil, err
  77. }
  78. app := &App{
  79. Client: client,
  80. CurrentSession: &session.Session{},
  81. Logs: logging.GetService(),
  82. Sessions: session.GetService(),
  83. Messages: message.GetService(),
  84. History: history.GetService(),
  85. Permissions: permission.GetService(),
  86. Status: status.GetService(),
  87. LSPClients: make(map[string]*lsp.Client),
  88. }
  89. // Initialize theme based on configuration
  90. app.initTheme()
  91. // Initialize LSP clients in the background
  92. go app.initLSPClients(ctx)
  93. app.PrimaryAgent, err = agent.NewAgent(
  94. config.AgentPrimary,
  95. app.Sessions,
  96. app.Messages,
  97. agent.PrimaryAgentTools(
  98. app.Permissions,
  99. app.Sessions,
  100. app.Messages,
  101. app.History,
  102. app.LSPClients,
  103. ),
  104. )
  105. if err != nil {
  106. slog.Error("Failed to create primary agent", "error", err)
  107. return nil, err
  108. }
  109. return app, nil
  110. }
  111. // initTheme sets the application theme based on the configuration
  112. func (app *App) initTheme() {
  113. cfg := config.Get()
  114. if cfg == nil || cfg.TUI.Theme == "" {
  115. return // Use default theme
  116. }
  117. // Try to set the theme from config
  118. err := theme.SetTheme(cfg.TUI.Theme)
  119. if err != nil {
  120. slog.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err)
  121. } else {
  122. slog.Debug("Set theme from config", "theme", cfg.TUI.Theme)
  123. }
  124. }
  125. // IsFilepickerOpen returns whether the filepicker is currently open
  126. func (app *App) IsFilepickerOpen() bool {
  127. return app.filepickerOpen
  128. }
  129. // SetFilepickerOpen sets the state of the filepicker
  130. func (app *App) SetFilepickerOpen(open bool) {
  131. app.filepickerOpen = open
  132. }
  133. // IsCompletionDialogOpen returns whether the completion dialog is currently open
  134. func (app *App) IsCompletionDialogOpen() bool {
  135. return app.completionDialogOpen
  136. }
  137. // SetCompletionDialogOpen sets the state of the completion dialog
  138. func (app *App) SetCompletionDialogOpen(open bool) {
  139. app.completionDialogOpen = open
  140. }
  141. // Shutdown performs a clean shutdown of the application
  142. func (app *App) Shutdown() {
  143. // Cancel all watcher goroutines
  144. app.cancelFuncsMutex.Lock()
  145. for _, cancel := range app.watcherCancelFuncs {
  146. cancel()
  147. }
  148. app.cancelFuncsMutex.Unlock()
  149. app.watcherWG.Wait()
  150. // Perform additional cleanup for LSP clients
  151. app.clientsMutex.RLock()
  152. clients := make(map[string]*lsp.Client, len(app.LSPClients))
  153. maps.Copy(clients, app.LSPClients)
  154. app.clientsMutex.RUnlock()
  155. for name, client := range clients {
  156. shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  157. if err := client.Shutdown(shutdownCtx); err != nil {
  158. slog.Error("Failed to shutdown LSP client", "name", name, "error", err)
  159. }
  160. cancel()
  161. }
  162. }