app_workspace.go

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