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 "log/slog"
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 // TODO: remove the concept of agent config most likely
61 coderAgentCfg := cfg.Agents["coder"]
62 if coderAgentCfg.ID == "" {
63 return nil, fmt.Errorf("coder agent configuration is missing")
64 }
65
66 var err error
67 app.CoderAgent, err = agent.NewAgent(
68 coderAgentCfg,
69 app.Permissions,
70 app.Sessions,
71 app.Messages,
72 app.History,
73 app.LSPClients,
74 )
75 if err != nil {
76 slog.Error("Failed to create coder agent", err)
77 return nil, err
78 }
79
80 return app, nil
81}
82
83// RunNonInteractive handles the execution flow when a prompt is provided via CLI flag.
84func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat string, quiet bool) error {
85 slog.Info("Running in non-interactive mode")
86
87 // Start spinner if not in quiet mode
88 var spinner *format.Spinner
89 if !quiet {
90 spinner = format.NewSpinner("Thinking...")
91 spinner.Start()
92 defer spinner.Stop()
93 }
94
95 const maxPromptLengthForTitle = 100
96 titlePrefix := "Non-interactive: "
97 var titleSuffix string
98
99 if len(prompt) > maxPromptLengthForTitle {
100 titleSuffix = prompt[:maxPromptLengthForTitle] + "..."
101 } else {
102 titleSuffix = prompt
103 }
104 title := titlePrefix + titleSuffix
105
106 sess, err := a.Sessions.Create(ctx, title)
107 if err != nil {
108 return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
109 }
110 slog.Info("Created session for non-interactive run", "session_id", sess.ID)
111
112 // Automatically approve all permission requests for this non-interactive session
113 a.Permissions.AutoApproveSession(sess.ID)
114
115 done, err := a.CoderAgent.Run(ctx, sess.ID, prompt)
116 if err != nil {
117 return fmt.Errorf("failed to start agent processing stream: %w", err)
118 }
119
120 result := <-done
121 if result.Error != nil {
122 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
123 slog.Info("Agent processing cancelled", "session_id", sess.ID)
124 return nil
125 }
126 return fmt.Errorf("agent processing failed: %w", result.Error)
127 }
128
129 // Stop spinner before printing output
130 if !quiet && spinner != nil {
131 spinner.Stop()
132 }
133
134 // Get the text content from the response
135 content := "No content available"
136 if result.Message.Content().String() != "" {
137 content = result.Message.Content().String()
138 }
139
140 fmt.Println(format.FormatOutput(content, outputFormat))
141
142 slog.Info("Non-interactive run completed", "session_id", sess.ID)
143
144 return nil
145}
146
147// Shutdown performs a clean shutdown of the application
148func (app *App) Shutdown() {
149 // Cancel all watcher goroutines
150 app.cancelFuncsMutex.Lock()
151 for _, cancel := range app.watcherCancelFuncs {
152 cancel()
153 }
154 app.cancelFuncsMutex.Unlock()
155 app.watcherWG.Wait()
156
157 // Perform additional cleanup for LSP clients
158 app.clientsMutex.RLock()
159 clients := make(map[string]*lsp.Client, len(app.LSPClients))
160 maps.Copy(clients, app.LSPClients)
161 app.clientsMutex.RUnlock()
162
163 for name, client := range clients {
164 shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
165 if err := client.Shutdown(shutdownCtx); err != nil {
166 slog.Error("Failed to shutdown LSP client", "name", name, "error", err)
167 }
168 cancel()
169 }
170 app.CoderAgent.CancelAll()
171}
172
173func (app *App) UpdateAgentModel() error {
174 return app.CoderAgent.UpdateModel()
175}