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