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