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