app.go

  1package app
  2
  3import (
  4	"context"
  5	"database/sql"
  6	"errors"
  7	"fmt"
  8	"maps"
  9	"sync"
 10	"time"
 11
 12	"github.com/charmbracelet/crush/internal/config"
 13	"github.com/charmbracelet/crush/internal/db"
 14	"github.com/charmbracelet/crush/internal/format"
 15	"github.com/charmbracelet/crush/internal/history"
 16	"github.com/charmbracelet/crush/internal/llm/agent"
 17	"github.com/charmbracelet/crush/internal/logging"
 18	"github.com/charmbracelet/crush/internal/lsp"
 19	"github.com/charmbracelet/crush/internal/message"
 20	"github.com/charmbracelet/crush/internal/permission"
 21	"github.com/charmbracelet/crush/internal/session"
 22)
 23
 24type App struct {
 25	Sessions    session.Service
 26	Messages    message.Service
 27	History     history.Service
 28	Permissions permission.Service
 29
 30	CoderAgent agent.Service
 31
 32	LSPClients map[string]*lsp.Client
 33
 34	clientsMutex sync.RWMutex
 35
 36	watcherCancelFuncs []context.CancelFunc
 37	cancelFuncsMutex   sync.Mutex
 38	watcherWG          sync.WaitGroup
 39}
 40
 41func New(ctx context.Context, conn *sql.DB) (*App, error) {
 42	q := db.New(conn)
 43	sessions := session.NewService(q)
 44	messages := message.NewService(q)
 45	files := history.NewService(q, conn)
 46
 47	app := &App{
 48		Sessions:    sessions,
 49		Messages:    messages,
 50		History:     files,
 51		Permissions: permission.NewPermissionService(),
 52		LSPClients:  make(map[string]*lsp.Client),
 53	}
 54
 55	// Initialize LSP clients in the background
 56	go app.initLSPClients(ctx)
 57
 58	cfg := config.Get()
 59
 60	coderAgentCfg := cfg.Agents[config.AgentCoder]
 61	if coderAgentCfg.ID == "" {
 62		return nil, fmt.Errorf("coder agent configuration is missing")
 63	}
 64
 65	var err error
 66	app.CoderAgent, err = agent.NewAgent(
 67		coderAgentCfg,
 68		app.Permissions,
 69		app.Sessions,
 70		app.Messages,
 71		app.History,
 72		app.LSPClients,
 73	)
 74	if err != nil {
 75		logging.Error("Failed to create coder agent", err)
 76		return nil, err
 77	}
 78
 79	return app, nil
 80}
 81
 82// RunNonInteractive handles the execution flow when a prompt is provided via CLI flag.
 83func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat string, quiet bool) error {
 84	logging.Info("Running in non-interactive mode")
 85
 86	// Start spinner if not in quiet mode
 87	var spinner *format.Spinner
 88	if !quiet {
 89		spinner = format.NewSpinner("Thinking...")
 90		spinner.Start()
 91		defer spinner.Stop()
 92	}
 93
 94	const maxPromptLengthForTitle = 100
 95	titlePrefix := "Non-interactive: "
 96	var titleSuffix string
 97
 98	if len(prompt) > maxPromptLengthForTitle {
 99		titleSuffix = prompt[:maxPromptLengthForTitle] + "..."
100	} else {
101		titleSuffix = prompt
102	}
103	title := titlePrefix + titleSuffix
104
105	sess, err := a.Sessions.Create(ctx, title)
106	if err != nil {
107		return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
108	}
109	logging.Info("Created session for non-interactive run", "session_id", sess.ID)
110
111	// Automatically approve all permission requests for this non-interactive session
112	a.Permissions.AutoApproveSession(sess.ID)
113
114	done, err := a.CoderAgent.Run(ctx, sess.ID, prompt)
115	if err != nil {
116		return fmt.Errorf("failed to start agent processing stream: %w", err)
117	}
118
119	result := <-done
120	if result.Error != nil {
121		if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
122			logging.Info("Agent processing cancelled", "session_id", sess.ID)
123			return nil
124		}
125		return fmt.Errorf("agent processing failed: %w", result.Error)
126	}
127
128	// Stop spinner before printing output
129	if !quiet && spinner != nil {
130		spinner.Stop()
131	}
132
133	// Get the text content from the response
134	content := "No content available"
135	if result.Message.Content().String() != "" {
136		content = result.Message.Content().String()
137	}
138
139	fmt.Println(format.FormatOutput(content, outputFormat))
140
141	logging.Info("Non-interactive run completed", "session_id", sess.ID)
142
143	return nil
144}
145
146// Shutdown performs a clean shutdown of the application
147func (app *App) Shutdown() {
148	// Cancel all watcher goroutines
149	app.cancelFuncsMutex.Lock()
150	for _, cancel := range app.watcherCancelFuncs {
151		cancel()
152	}
153	app.cancelFuncsMutex.Unlock()
154	app.watcherWG.Wait()
155
156	// Perform additional cleanup for LSP clients
157	app.clientsMutex.RLock()
158	clients := make(map[string]*lsp.Client, len(app.LSPClients))
159	maps.Copy(clients, app.LSPClients)
160	app.clientsMutex.RUnlock()
161
162	for name, client := range clients {
163		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
164		if err := client.Shutdown(shutdownCtx); err != nil {
165			logging.Error("Failed to shutdown LSP client", "name", name, "error", err)
166		}
167		cancel()
168	}
169	app.CoderAgent.CancelAll()
170}