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