@@ -10,11 +10,13 @@ import (
13+ tea "github.com/charmbracelet/bubbletea/v2"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/db"
15 "github.com/charmbracelet/crush/internal/db"
15 "github.com/charmbracelet/crush/internal/format"
16 "github.com/charmbracelet/crush/internal/format"
16 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/llm/agent"
18 "github.com/charmbracelet/crush/internal/llm/agent"
19+ "github.com/charmbracelet/crush/internal/pubsub"
19 "github.com/charmbracelet/crush/internal/lsp"
21 "github.com/charmbracelet/crush/internal/lsp"
20 "github.com/charmbracelet/crush/internal/message"
22 "github.com/charmbracelet/crush/internal/message"
@@ -36,9 +38,18 @@ type App struct {
37 watcherCancelFuncs []context.CancelFunc
39 watcherCancelFuncs []context.CancelFunc
38 cancelFuncsMutex sync.Mutex
40 cancelFuncsMutex sync.Mutex
39− watcherWG sync.WaitGroup
41+ lspWatcherWG sync.WaitGroup
41 config *config.Config
43 config *config.Config
45+ serviceEventsWG *sync.WaitGroup
46+ eventsCtx context.Context
48+ tuiWG *sync.WaitGroup
50+ // global context and cleanup functions
51+ globalCtx context.Context
52+ cleanupFuncs []func()
44func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
55func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
@@ -53,32 +64,29 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
53 History: files,
64 History: files,
54 Permissions: permission.NewPermissionService(cfg.WorkingDir()),
65 Permissions: permission.NewPermissionService(cfg.WorkingDir()),
55 LSPClients: make(map[string]*lsp.Client),
66 LSPClients: make(map[string]*lsp.Client),
56− config: cfg,
70+ config: cfg,
72+ events: make(chan tea.Msg, 100),
73+ serviceEventsWG: &sync.WaitGroup{},
74+ tuiWG: &sync.WaitGroup{},
59 // Initialize LSP clients in the background
79 // Initialize LSP clients in the background
60 go app.initLSPClients(ctx)
80 go app.initLSPClients(ctx)
62 // TODO: remove the concept of agent config most likely
82 // TODO: remove the concept of agent config most likely
63− coderAgentCfg := cfg.Agents["coder"]
83+ if cfg.IsConfigured() {
64− if coderAgentCfg.ID == "" {
84+ if err := app.InitCoderAgent(); err != nil {
65− return nil, fmt.Errorf("coder agent configuration is missing")
85+ return nil, fmt.Errorf("failed to initialize coder agent: %w", err)
68− var err error
88+ slog.Warn("No agent configuration found")
69− app.CoderAgent, err = agent.NewAgent(
78− slog.Error("Failed to create coder agent", "err", err)
82 return app, nil
90 return app, nil
@@ -146,32 +154,133 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
146 return nil
154 return nil
157+func (app *App) UpdateAgentModel() error {
158+ return app.CoderAgent.UpdateModel()
161+func (app *App) setupEvents() {
162+ ctx, cancel := context.WithCancel(app.globalCtx)
164+ setupSubscriber(ctx, app.serviceEventsWG, "sessions", app.Sessions.Subscribe, app.events)
165+ setupSubscriber(ctx, app.serviceEventsWG, "messages", app.Messages.Subscribe, app.events)
166+ setupSubscriber(ctx, app.serviceEventsWG, "permissions", app.Permissions.Subscribe, app.events)
167+ setupSubscriber(ctx, app.serviceEventsWG, "history", app.History.Subscribe, app.events)
168+ cleanupFunc := func() {
170+ app.serviceEventsWG.Wait()
172+ app.cleanupFuncs = append(app.cleanupFuncs, cleanupFunc)
175+func setupSubscriber[T any](
176+ ctx context.Context,
179+ subscriber func(context.Context) <-chan pubsub.Event[T],
180+ outputCh chan<- tea.Msg,
185+ subCh := subscriber(ctx)
188+ case event, ok := <-subCh:
190+ slog.Debug("subscription channel closed", "name", name)
193+ var msg tea.Msg = event
195+ case outputCh <- msg:
196+ case <-time.After(2 * time.Second):
197+ slog.Warn("message dropped due to slow consumer", "name", name)
199+ slog.Debug("subscription cancelled", "name", name)
203+ slog.Debug("subscription cancelled", "name", name)
210+func (app *App) InitCoderAgent() error {
211+ coderAgentCfg := app.config.Agents["coder"]
212+ if coderAgentCfg.ID == "" {
213+ return fmt.Errorf("coder agent configuration is missing")
216+ app.CoderAgent, err = agent.NewAgent(
225+ slog.Error("Failed to create coder agent", "err", err)
228+ setupSubscriber(app.eventsCtx, app.serviceEventsWG, "coderAgent", app.CoderAgent.Subscribe, app.events)
232+func (app *App) Subscribe(program *tea.Program) {
234+ tuiCtx, tuiCancel := context.WithCancel(app.globalCtx)
235+ app.cleanupFuncs = append(app.cleanupFuncs, func() {
236+ slog.Debug("Cancelling TUI message handler")
240+ defer app.tuiWG.Done()
243+ case <-tuiCtx.Done():
244+ slog.Debug("TUI message handler shutting down")
246+ case msg, ok := <-app.events:
248+ slog.Debug("TUI message channel closed")
149// Shutdown performs a clean shutdown of the application
256// Shutdown performs a clean shutdown of the application
150func (app *App) Shutdown() {
257func (app *App) Shutdown() {
151− // Cancel all watcher goroutines
152 app.cancelFuncsMutex.Lock()
258 app.cancelFuncsMutex.Lock()
153 for _, cancel := range app.watcherCancelFuncs {
259 for _, cancel := range app.watcherCancelFuncs {
156 app.cancelFuncsMutex.Unlock()
262 app.cancelFuncsMutex.Unlock()
157− app.watcherWG.Wait()
263+ app.lspWatcherWG.Wait()
159− // Perform additional cleanup for LSP clients
160 app.clientsMutex.RLock()
265 app.clientsMutex.RLock()
161 clients := make(map[string]*lsp.Client, len(app.LSPClients))
266 clients := make(map[string]*lsp.Client, len(app.LSPClients))
162 maps.Copy(clients, app.LSPClients)
267 maps.Copy(clients, app.LSPClients)
163 app.clientsMutex.RUnlock()
268 app.clientsMutex.RUnlock()
165 for name, client := range clients {
270 for name, client := range clients {
166− shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
271+ shutdownCtx, cancel := context.WithTimeout(app.globalCtx, 5*time.Second)
167 if err := client.Shutdown(shutdownCtx); err != nil {
272 if err := client.Shutdown(shutdownCtx); err != nil {
168 slog.Error("Failed to shutdown LSP client", "name", name, "error", err)
273 slog.Error("Failed to shutdown LSP client", "name", name, "error", err)
277+ if app.CoderAgent != nil {
172− app.CoderAgent.CancelAll()
278+ app.CoderAgent.CancelAll()
175−func (app *App) UpdateAgentModel() error {
281+ for _, cleanup := range app.cleanupFuncs {
176− return app.CoderAgent.UpdateModel()
282+ if cleanup != nil {