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) 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)