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