app_workspace.go

  1package workspace
  2
  3import (
  4	"context"
  5	"log/slog"
  6	"time"
  7
  8	tea "charm.land/bubbletea/v2"
  9	"github.com/charmbracelet/crush/internal/agent"
 10	mcptools "github.com/charmbracelet/crush/internal/agent/tools/mcp"
 11	"github.com/charmbracelet/crush/internal/app"
 12	"github.com/charmbracelet/crush/internal/commands"
 13	"github.com/charmbracelet/crush/internal/config"
 14	"github.com/charmbracelet/crush/internal/history"
 15	"github.com/charmbracelet/crush/internal/log"
 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/pubsub"
 21	"github.com/charmbracelet/crush/internal/session"
 22)
 23
 24// AppWorkspace wraps an in-process app.App to satisfy the Workspace
 25// interface. This is the default mode when no server is involved.
 26type AppWorkspace struct {
 27	app *app.App
 28}
 29
 30// NewAppWorkspace creates a Workspace backed by a local app.App.
 31func NewAppWorkspace(a *app.App) *AppWorkspace {
 32	return &AppWorkspace{app: a}
 33}
 34
 35// App returns the underlying app.App for callers that still need
 36// direct access during the migration period.
 37func (w *AppWorkspace) App() *app.App {
 38	return w.app
 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// -- Messages --
 72
 73func (w *AppWorkspace) ListMessages(ctx context.Context, sessionID string) ([]message.Message, error) {
 74	return w.app.Messages.List(ctx, sessionID)
 75}
 76
 77func (w *AppWorkspace) ListUserMessages(ctx context.Context, sessionID string) ([]message.Message, error) {
 78	return w.app.Messages.ListUserMessages(ctx, sessionID)
 79}
 80
 81func (w *AppWorkspace) ListAllUserMessages(ctx context.Context) ([]message.Message, error) {
 82	return w.app.Messages.ListAllUserMessages(ctx)
 83}
 84
 85// -- Agent --
 86
 87func (w *AppWorkspace) AgentRun(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) error {
 88	if w.app.AgentCoordinator == nil {
 89		return nil
 90	}
 91	_, err := w.app.AgentCoordinator.Run(ctx, sessionID, prompt, attachments...)
 92	return err
 93}
 94
 95func (w *AppWorkspace) AgentCancel(sessionID string) {
 96	if w.app.AgentCoordinator != nil {
 97		w.app.AgentCoordinator.Cancel(sessionID)
 98	}
 99}
100
101func (w *AppWorkspace) AgentIsBusy() bool {
102	if w.app.AgentCoordinator == nil {
103		return false
104	}
105	return w.app.AgentCoordinator.IsBusy()
106}
107
108func (w *AppWorkspace) AgentIsSessionBusy(sessionID string) bool {
109	if w.app.AgentCoordinator == nil {
110		return false
111	}
112	return w.app.AgentCoordinator.IsSessionBusy(sessionID)
113}
114
115func (w *AppWorkspace) AgentModel() AgentModel {
116	if w.app.AgentCoordinator == nil {
117		return AgentModel{}
118	}
119	m := w.app.AgentCoordinator.Model()
120	return AgentModel{
121		CatwalkCfg: m.CatwalkCfg,
122		ModelCfg:   m.ModelCfg,
123	}
124}
125
126func (w *AppWorkspace) AgentIsReady() bool {
127	return w.app.AgentCoordinator != nil
128}
129
130func (w *AppWorkspace) AgentQueuedPrompts(sessionID string) int {
131	if w.app.AgentCoordinator == nil {
132		return 0
133	}
134	return w.app.AgentCoordinator.QueuedPrompts(sessionID)
135}
136
137func (w *AppWorkspace) AgentQueuedPromptsList(sessionID string) []string {
138	if w.app.AgentCoordinator == nil {
139		return nil
140	}
141	return w.app.AgentCoordinator.QueuedPromptsList(sessionID)
142}
143
144func (w *AppWorkspace) AgentClearQueue(sessionID string) {
145	if w.app.AgentCoordinator != nil {
146		w.app.AgentCoordinator.ClearQueue(sessionID)
147	}
148}
149
150func (w *AppWorkspace) AgentSummarize(ctx context.Context, sessionID string) error {
151	if w.app.AgentCoordinator == nil {
152		return nil
153	}
154	return w.app.AgentCoordinator.Summarize(ctx, sessionID)
155}
156
157func (w *AppWorkspace) UpdateAgentModel(ctx context.Context) error {
158	return w.app.UpdateAgentModel(ctx)
159}
160
161func (w *AppWorkspace) InitCoderAgent(ctx context.Context) error {
162	return w.app.InitCoderAgent(ctx)
163}
164
165func (w *AppWorkspace) GetDefaultSmallModel(providerID string) config.SelectedModel {
166	return w.app.GetDefaultSmallModel(providerID)
167}
168
169// -- Permissions --
170
171func (w *AppWorkspace) PermissionGrant(perm permission.PermissionRequest) {
172	w.app.Permissions.Grant(perm)
173}
174
175func (w *AppWorkspace) PermissionGrantPersistent(perm permission.PermissionRequest) {
176	w.app.Permissions.GrantPersistent(perm)
177}
178
179func (w *AppWorkspace) PermissionDeny(perm permission.PermissionRequest) {
180	w.app.Permissions.Deny(perm)
181}
182
183func (w *AppWorkspace) PermissionSkipRequests() bool {
184	return w.app.Permissions.SkipRequests()
185}
186
187func (w *AppWorkspace) PermissionSetSkipRequests(skip bool) {
188	w.app.Permissions.SetSkipRequests(skip)
189}
190
191// -- FileTracker --
192
193func (w *AppWorkspace) FileTrackerRecordRead(ctx context.Context, sessionID, path string) {
194	w.app.FileTracker.RecordRead(ctx, sessionID, path)
195}
196
197func (w *AppWorkspace) FileTrackerLastReadTime(ctx context.Context, sessionID, path string) time.Time {
198	return w.app.FileTracker.LastReadTime(ctx, sessionID, path)
199}
200
201func (w *AppWorkspace) FileTrackerListReadFiles(ctx context.Context, sessionID string) ([]string, error) {
202	return w.app.FileTracker.ListReadFiles(ctx, sessionID)
203}
204
205// -- History --
206
207func (w *AppWorkspace) ListSessionHistory(ctx context.Context, sessionID string) ([]history.File, error) {
208	return w.app.History.ListBySession(ctx, sessionID)
209}
210
211// -- LSP --
212
213func (w *AppWorkspace) LSPStart(ctx context.Context, path string) {
214	w.app.LSPManager.Start(ctx, path)
215}
216
217func (w *AppWorkspace) LSPStopAll(ctx context.Context) {
218	w.app.LSPManager.StopAll(ctx)
219}
220
221func (w *AppWorkspace) LSPGetStates() map[string]LSPClientInfo {
222	states := app.GetLSPStates()
223	result := make(map[string]LSPClientInfo, len(states))
224	for k, v := range states {
225		result[k] = LSPClientInfo{
226			Name:            v.Name,
227			State:           v.State,
228			Error:           v.Error,
229			DiagnosticCount: v.DiagnosticCount,
230			ConnectedAt:     v.ConnectedAt,
231		}
232	}
233	return result
234}
235
236func (w *AppWorkspace) LSPGetClient(name string) (*lsp.Client, bool) {
237	info, ok := app.GetLSPState(name)
238	if !ok {
239		return nil, false
240	}
241	return info.Client, true
242}
243
244// -- Config (read-only) --
245
246func (w *AppWorkspace) Config() *config.Config {
247	return w.app.Config()
248}
249
250func (w *AppWorkspace) WorkingDir() string {
251	return w.app.Store().WorkingDir()
252}
253
254func (w *AppWorkspace) Resolver() config.VariableResolver {
255	return w.app.Store().Resolver()
256}
257
258// -- Config mutations --
259
260func (w *AppWorkspace) UpdatePreferredModel(scope config.Scope, modelType config.SelectedModelType, model config.SelectedModel) error {
261	return w.app.Store().UpdatePreferredModel(scope, modelType, model)
262}
263
264func (w *AppWorkspace) SetCompactMode(scope config.Scope, enabled bool) error {
265	return w.app.Store().SetCompactMode(scope, enabled)
266}
267
268func (w *AppWorkspace) SetProviderAPIKey(scope config.Scope, providerID string, apiKey any) error {
269	return w.app.Store().SetProviderAPIKey(scope, providerID, apiKey)
270}
271
272func (w *AppWorkspace) SetConfigField(scope config.Scope, key string, value any) error {
273	return w.app.Store().SetConfigField(scope, key, value)
274}
275
276func (w *AppWorkspace) RemoveConfigField(scope config.Scope, key string) error {
277	return w.app.Store().RemoveConfigField(scope, key)
278}
279
280func (w *AppWorkspace) ImportCopilot() (*oauth.Token, bool) {
281	return w.app.Store().ImportCopilot()
282}
283
284func (w *AppWorkspace) RefreshOAuthToken(ctx context.Context, scope config.Scope, providerID string) error {
285	return w.app.Store().RefreshOAuthToken(ctx, scope, providerID)
286}
287
288// -- Project lifecycle --
289
290func (w *AppWorkspace) ProjectNeedsInitialization() (bool, error) {
291	return config.ProjectNeedsInitialization(w.app.Store())
292}
293
294func (w *AppWorkspace) MarkProjectInitialized() error {
295	return config.MarkProjectInitialized(w.app.Store())
296}
297
298func (w *AppWorkspace) InitializePrompt() (string, error) {
299	return agent.InitializePrompt(w.app.Store())
300}
301
302// -- MCP operations --
303
304func (w *AppWorkspace) MCPGetStates() map[string]mcptools.ClientInfo {
305	return mcptools.GetStates()
306}
307
308func (w *AppWorkspace) MCPRefreshPrompts(ctx context.Context, name string) {
309	mcptools.RefreshPrompts(ctx, name)
310}
311
312func (w *AppWorkspace) MCPRefreshResources(ctx context.Context, name string) {
313	mcptools.RefreshResources(ctx, name)
314}
315
316func (w *AppWorkspace) RefreshMCPTools(ctx context.Context, name string) {
317	mcptools.RefreshTools(ctx, w.app.Store(), name)
318}
319
320func (w *AppWorkspace) ReadMCPResource(ctx context.Context, name, uri string) ([]MCPResourceContents, error) {
321	contents, err := mcptools.ReadResource(ctx, w.app.Store(), name, uri)
322	if err != nil {
323		return nil, err
324	}
325	result := make([]MCPResourceContents, len(contents))
326	for i, c := range contents {
327		result[i] = MCPResourceContents{
328			URI:      c.URI,
329			MIMEType: c.MIMEType,
330			Text:     c.Text,
331			Blob:     c.Blob,
332		}
333	}
334	return result, nil
335}
336
337func (w *AppWorkspace) GetMCPPrompt(clientID, promptID string, args map[string]string) (string, error) {
338	return commands.GetMCPPrompt(w.app.Store(), clientID, promptID, args)
339}
340
341// -- Lifecycle --
342
343func (w *AppWorkspace) Subscribe(program *tea.Program) {
344	defer log.RecoverPanic("AppWorkspace.Subscribe", func() {
345		slog.Info("TUI subscription panic: attempting graceful shutdown")
346		program.Quit()
347	})
348
349	for msg := range w.app.Events() {
350		switch ev := msg.(type) {
351		case pubsub.Event[app.LSPEvent]:
352			program.Send(pubsub.Event[LSPEvent]{
353				Type: ev.Type,
354				Payload: LSPEvent{
355					Type:            LSPEventType(ev.Payload.Type),
356					Name:            ev.Payload.Name,
357					State:           ev.Payload.State,
358					Error:           ev.Payload.Error,
359					DiagnosticCount: ev.Payload.DiagnosticCount,
360				},
361			})
362		default:
363			program.Send(msg)
364		}
365	}
366}
367
368func (w *AppWorkspace) Shutdown() {
369	w.app.Shutdown()
370}