1package workspace
2
3import (
4 "context"
5 "log/slog"
6 "time"
7
8 tea "charm.land/bubbletea/v2"
9 "github.com/charmbracelet/crush/internal/agent"
10 mcptools "github.com/charmbracelet/crush/internal/agent/tools/mcp"
11 "github.com/charmbracelet/crush/internal/app"
12 "github.com/charmbracelet/crush/internal/commands"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/history"
15 "github.com/charmbracelet/crush/internal/log"
16 "github.com/charmbracelet/crush/internal/lsp"
17 "github.com/charmbracelet/crush/internal/message"
18 "github.com/charmbracelet/crush/internal/oauth"
19 "github.com/charmbracelet/crush/internal/permission"
20 "github.com/charmbracelet/crush/internal/pubsub"
21 "github.com/charmbracelet/crush/internal/session"
22)
23
24// AppWorkspace wraps an in-process app.App to satisfy the Workspace
25// interface. This is the default mode when no server is involved.
26type AppWorkspace struct {
27 app *app.App
28}
29
30// NewAppWorkspace creates a Workspace backed by a local app.App.
31func NewAppWorkspace(a *app.App) *AppWorkspace {
32 return &AppWorkspace{app: a}
33}
34
35// App returns the underlying app.App for callers that still need
36// direct access during the migration period.
37func (w *AppWorkspace) App() *app.App {
38 return w.app
39}
40
41// -- Sessions --
42
43func (w *AppWorkspace) CreateSession(ctx context.Context, title string) (session.Session, error) {
44 return w.app.Sessions.Create(ctx, title)
45}
46
47func (w *AppWorkspace) GetSession(ctx context.Context, sessionID string) (session.Session, error) {
48 return w.app.Sessions.Get(ctx, sessionID)
49}
50
51func (w *AppWorkspace) ListSessions(ctx context.Context) ([]session.Session, error) {
52 return w.app.Sessions.List(ctx)
53}
54
55func (w *AppWorkspace) SaveSession(ctx context.Context, sess session.Session) (session.Session, error) {
56 return w.app.Sessions.Save(ctx, sess)
57}
58
59func (w *AppWorkspace) DeleteSession(ctx context.Context, sessionID string) error {
60 return w.app.Sessions.Delete(ctx, sessionID)
61}
62
63func (w *AppWorkspace) CreateAgentToolSessionID(messageID, toolCallID string) string {
64 return w.app.Sessions.CreateAgentToolSessionID(messageID, toolCallID)
65}
66
67func (w *AppWorkspace) ParseAgentToolSessionID(sessionID string) (string, string, bool) {
68 return w.app.Sessions.ParseAgentToolSessionID(sessionID)
69}
70
71// -- Messages --
72
73func (w *AppWorkspace) ListMessages(ctx context.Context, sessionID string) ([]message.Message, error) {
74 return w.app.Messages.List(ctx, sessionID)
75}
76
77func (w *AppWorkspace) ListUserMessages(ctx context.Context, sessionID string) ([]message.Message, error) {
78 return w.app.Messages.ListUserMessages(ctx, sessionID)
79}
80
81func (w *AppWorkspace) ListAllUserMessages(ctx context.Context) ([]message.Message, error) {
82 return w.app.Messages.ListAllUserMessages(ctx)
83}
84
85// -- Agent --
86
87func (w *AppWorkspace) AgentRun(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) error {
88 if w.app.AgentCoordinator == nil {
89 return nil
90 }
91 _, err := w.app.AgentCoordinator.Run(ctx, sessionID, prompt, attachments...)
92 return err
93}
94
95func (w *AppWorkspace) AgentCancel(sessionID string) {
96 if w.app.AgentCoordinator != nil {
97 w.app.AgentCoordinator.Cancel(sessionID)
98 }
99}
100
101func (w *AppWorkspace) AgentIsBusy() bool {
102 if w.app.AgentCoordinator == nil {
103 return false
104 }
105 return w.app.AgentCoordinator.IsBusy()
106}
107
108func (w *AppWorkspace) AgentIsSessionBusy(sessionID string) bool {
109 if w.app.AgentCoordinator == nil {
110 return false
111 }
112 return w.app.AgentCoordinator.IsSessionBusy(sessionID)
113}
114
115func (w *AppWorkspace) AgentModel() AgentModel {
116 if w.app.AgentCoordinator == nil {
117 return AgentModel{}
118 }
119 m := w.app.AgentCoordinator.Model()
120 return AgentModel{
121 CatwalkCfg: m.CatwalkCfg,
122 ModelCfg: m.ModelCfg,
123 }
124}
125
126func (w *AppWorkspace) AgentIsReady() bool {
127 return w.app.AgentCoordinator != nil
128}
129
130func (w *AppWorkspace) AgentQueuedPrompts(sessionID string) int {
131 if w.app.AgentCoordinator == nil {
132 return 0
133 }
134 return w.app.AgentCoordinator.QueuedPrompts(sessionID)
135}
136
137func (w *AppWorkspace) AgentQueuedPromptsList(sessionID string) []string {
138 if w.app.AgentCoordinator == nil {
139 return nil
140 }
141 return w.app.AgentCoordinator.QueuedPromptsList(sessionID)
142}
143
144func (w *AppWorkspace) AgentClearQueue(sessionID string) {
145 if w.app.AgentCoordinator != nil {
146 w.app.AgentCoordinator.ClearQueue(sessionID)
147 }
148}
149
150func (w *AppWorkspace) AgentSummarize(ctx context.Context, sessionID string) error {
151 if w.app.AgentCoordinator == nil {
152 return nil
153 }
154 return w.app.AgentCoordinator.Summarize(ctx, sessionID)
155}
156
157func (w *AppWorkspace) UpdateAgentModel(ctx context.Context) error {
158 return w.app.UpdateAgentModel(ctx)
159}
160
161func (w *AppWorkspace) InitCoderAgent(ctx context.Context) error {
162 return w.app.InitCoderAgent(ctx)
163}
164
165func (w *AppWorkspace) GetDefaultSmallModel(providerID string) config.SelectedModel {
166 return w.app.GetDefaultSmallModel(providerID)
167}
168
169// -- Permissions --
170
171func (w *AppWorkspace) PermissionGrant(perm permission.PermissionRequest) {
172 w.app.Permissions.Grant(perm)
173}
174
175func (w *AppWorkspace) PermissionGrantPersistent(perm permission.PermissionRequest) {
176 w.app.Permissions.GrantPersistent(perm)
177}
178
179func (w *AppWorkspace) PermissionDeny(perm permission.PermissionRequest) {
180 w.app.Permissions.Deny(perm)
181}
182
183func (w *AppWorkspace) PermissionSkipRequests() bool {
184 return w.app.Permissions.SkipRequests()
185}
186
187func (w *AppWorkspace) PermissionSetSkipRequests(skip bool) {
188 w.app.Permissions.SetSkipRequests(skip)
189}
190
191// -- FileTracker --
192
193func (w *AppWorkspace) FileTrackerRecordRead(ctx context.Context, sessionID, path string) {
194 w.app.FileTracker.RecordRead(ctx, sessionID, path)
195}
196
197func (w *AppWorkspace) FileTrackerLastReadTime(ctx context.Context, sessionID, path string) time.Time {
198 return w.app.FileTracker.LastReadTime(ctx, sessionID, path)
199}
200
201func (w *AppWorkspace) FileTrackerListReadFiles(ctx context.Context, sessionID string) ([]string, error) {
202 return w.app.FileTracker.ListReadFiles(ctx, sessionID)
203}
204
205// -- History --
206
207func (w *AppWorkspace) ListSessionHistory(ctx context.Context, sessionID string) ([]history.File, error) {
208 return w.app.History.ListBySession(ctx, sessionID)
209}
210
211// -- LSP --
212
213func (w *AppWorkspace) LSPStart(ctx context.Context, path string) {
214 w.app.LSPManager.Start(ctx, path)
215}
216
217func (w *AppWorkspace) LSPStopAll(ctx context.Context) {
218 w.app.LSPManager.StopAll(ctx)
219}
220
221func (w *AppWorkspace) LSPGetStates() map[string]LSPClientInfo {
222 states := app.GetLSPStates()
223 result := make(map[string]LSPClientInfo, len(states))
224 for k, v := range states {
225 result[k] = LSPClientInfo{
226 Name: v.Name,
227 State: v.State,
228 Error: v.Error,
229 DiagnosticCount: v.DiagnosticCount,
230 ConnectedAt: v.ConnectedAt,
231 }
232 }
233 return result
234}
235
236func (w *AppWorkspace) LSPGetClient(name string) (*lsp.Client, bool) {
237 info, ok := app.GetLSPState(name)
238 if !ok {
239 return nil, false
240 }
241 return info.Client, true
242}
243
244// -- Config (read-only) --
245
246func (w *AppWorkspace) Config() *config.Config {
247 return w.app.Config()
248}
249
250func (w *AppWorkspace) WorkingDir() string {
251 return w.app.Store().WorkingDir()
252}
253
254func (w *AppWorkspace) Resolver() config.VariableResolver {
255 return w.app.Store().Resolver()
256}
257
258// -- Config mutations --
259
260func (w *AppWorkspace) UpdatePreferredModel(scope config.Scope, modelType config.SelectedModelType, model config.SelectedModel) error {
261 return w.app.Store().UpdatePreferredModel(scope, modelType, model)
262}
263
264func (w *AppWorkspace) SetCompactMode(scope config.Scope, enabled bool) error {
265 return w.app.Store().SetCompactMode(scope, enabled)
266}
267
268func (w *AppWorkspace) SetProviderAPIKey(scope config.Scope, providerID string, apiKey any) error {
269 return w.app.Store().SetProviderAPIKey(scope, providerID, apiKey)
270}
271
272func (w *AppWorkspace) SetConfigField(scope config.Scope, key string, value any) error {
273 return w.app.Store().SetConfigField(scope, key, value)
274}
275
276func (w *AppWorkspace) RemoveConfigField(scope config.Scope, key string) error {
277 return w.app.Store().RemoveConfigField(scope, key)
278}
279
280func (w *AppWorkspace) ImportCopilot() (*oauth.Token, bool) {
281 return w.app.Store().ImportCopilot()
282}
283
284func (w *AppWorkspace) RefreshOAuthToken(ctx context.Context, scope config.Scope, providerID string) error {
285 return w.app.Store().RefreshOAuthToken(ctx, scope, providerID)
286}
287
288// -- Project lifecycle --
289
290func (w *AppWorkspace) ProjectNeedsInitialization() (bool, error) {
291 return config.ProjectNeedsInitialization(w.app.Store())
292}
293
294func (w *AppWorkspace) MarkProjectInitialized() error {
295 return config.MarkProjectInitialized(w.app.Store())
296}
297
298func (w *AppWorkspace) InitializePrompt() (string, error) {
299 return agent.InitializePrompt(w.app.Store())
300}
301
302// -- MCP operations --
303
304func (w *AppWorkspace) MCPGetStates() map[string]mcptools.ClientInfo {
305 return mcptools.GetStates()
306}
307
308func (w *AppWorkspace) MCPRefreshPrompts(ctx context.Context, name string) {
309 mcptools.RefreshPrompts(ctx, name)
310}
311
312func (w *AppWorkspace) MCPRefreshResources(ctx context.Context, name string) {
313 mcptools.RefreshResources(ctx, name)
314}
315
316func (w *AppWorkspace) RefreshMCPTools(ctx context.Context, name string) {
317 mcptools.RefreshTools(ctx, w.app.Store(), name)
318}
319
320func (w *AppWorkspace) ReadMCPResource(ctx context.Context, name, uri string) ([]MCPResourceContents, error) {
321 contents, err := mcptools.ReadResource(ctx, w.app.Store(), name, uri)
322 if err != nil {
323 return nil, err
324 }
325 result := make([]MCPResourceContents, len(contents))
326 for i, c := range contents {
327 result[i] = MCPResourceContents{
328 URI: c.URI,
329 MIMEType: c.MIMEType,
330 Text: c.Text,
331 Blob: c.Blob,
332 }
333 }
334 return result, nil
335}
336
337func (w *AppWorkspace) GetMCPPrompt(clientID, promptID string, args map[string]string) (string, error) {
338 return commands.GetMCPPrompt(w.app.Store(), clientID, promptID, args)
339}
340
341// -- Lifecycle --
342
343func (w *AppWorkspace) Subscribe(program *tea.Program) {
344 defer log.RecoverPanic("AppWorkspace.Subscribe", func() {
345 slog.Info("TUI subscription panic: attempting graceful shutdown")
346 program.Quit()
347 })
348
349 for msg := range w.app.Events() {
350 switch ev := msg.(type) {
351 case pubsub.Event[app.LSPEvent]:
352 program.Send(pubsub.Event[LSPEvent]{
353 Type: ev.Type,
354 Payload: LSPEvent{
355 Type: LSPEventType(ev.Payload.Type),
356 Name: ev.Payload.Name,
357 State: ev.Payload.State,
358 Error: ev.Payload.Error,
359 DiagnosticCount: ev.Payload.DiagnosticCount,
360 },
361 })
362 default:
363 program.Send(msg)
364 }
365 }
366}
367
368func (w *AppWorkspace) Shutdown() {
369 w.app.Shutdown()
370}