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