@geminilight/mindos 0.6.29 → 0.6.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +10 -4
  2. package/README_zh.md +10 -4
  3. package/app/app/api/acp/config/route.ts +82 -0
  4. package/app/app/api/acp/detect/route.ts +71 -48
  5. package/app/app/api/acp/install/route.ts +51 -0
  6. package/app/app/api/acp/session/route.ts +141 -11
  7. package/app/app/api/ask/route.ts +126 -18
  8. package/app/app/api/export/route.ts +105 -0
  9. package/app/app/api/workflows/route.ts +156 -0
  10. package/app/app/globals.css +2 -2
  11. package/app/app/page.tsx +7 -2
  12. package/app/app/trash/page.tsx +7 -0
  13. package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
  14. package/app/components/ActivityBar.tsx +12 -4
  15. package/app/components/AskModal.tsx +4 -1
  16. package/app/components/ExportModal.tsx +220 -0
  17. package/app/components/FileTree.tsx +42 -11
  18. package/app/components/HomeContent.tsx +92 -20
  19. package/app/components/MarkdownView.tsx +45 -10
  20. package/app/components/Panel.tsx +1 -0
  21. package/app/components/RightAskPanel.tsx +5 -1
  22. package/app/components/Sidebar.tsx +10 -1
  23. package/app/components/SidebarLayout.tsx +6 -0
  24. package/app/components/TrashPageClient.tsx +263 -0
  25. package/app/components/agents/AgentDetailContent.tsx +263 -47
  26. package/app/components/agents/AgentsContentPage.tsx +11 -0
  27. package/app/components/agents/AgentsPanelA2aTab.tsx +285 -46
  28. package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
  29. package/app/components/agents/agents-content-model.ts +2 -2
  30. package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
  31. package/app/components/ask/AskContent.tsx +197 -239
  32. package/app/components/ask/FileChip.tsx +82 -17
  33. package/app/components/ask/MentionPopover.tsx +21 -3
  34. package/app/components/ask/MessageList.tsx +30 -9
  35. package/app/components/ask/SlashCommandPopover.tsx +21 -3
  36. package/app/components/ask/ToolCallBlock.tsx +102 -18
  37. package/app/components/changes/ChangesContentPage.tsx +58 -14
  38. package/app/components/explore/ExploreContent.tsx +4 -7
  39. package/app/components/explore/UseCaseCard.tsx +18 -1
  40. package/app/components/explore/use-cases.generated.ts +76 -0
  41. package/app/components/explore/use-cases.yaml +185 -0
  42. package/app/components/panels/AgentsPanel.tsx +1 -0
  43. package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
  44. package/app/components/panels/DiscoverPanel.tsx +1 -1
  45. package/app/components/panels/WorkflowsPanel.tsx +206 -0
  46. package/app/components/renderers/workflow-yaml/StepEditor.tsx +164 -0
  47. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +211 -0
  48. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +269 -0
  49. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
  50. package/app/components/renderers/workflow-yaml/execution.ts +229 -0
  51. package/app/components/renderers/workflow-yaml/index.ts +6 -0
  52. package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
  53. package/app/components/renderers/workflow-yaml/parser.ts +172 -0
  54. package/app/components/renderers/workflow-yaml/selectors.tsx +574 -0
  55. package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
  56. package/app/components/renderers/workflow-yaml/types.ts +46 -0
  57. package/app/components/settings/AiTab.tsx +191 -174
  58. package/app/components/settings/AppearanceTab.tsx +168 -77
  59. package/app/components/settings/KnowledgeTab.tsx +131 -136
  60. package/app/components/settings/McpTab.tsx +11 -11
  61. package/app/components/settings/Primitives.tsx +60 -0
  62. package/app/components/settings/SettingsContent.tsx +15 -8
  63. package/app/components/settings/SyncTab.tsx +12 -12
  64. package/app/components/settings/UninstallTab.tsx +8 -18
  65. package/app/components/settings/UpdateTab.tsx +82 -82
  66. package/app/components/settings/types.ts +17 -8
  67. package/app/hooks/useAcpConfig.ts +96 -0
  68. package/app/hooks/useAcpDetection.ts +69 -14
  69. package/app/hooks/useAcpRegistry.ts +46 -11
  70. package/app/hooks/useAskModal.ts +12 -5
  71. package/app/hooks/useAskPanel.ts +8 -5
  72. package/app/hooks/useAskSession.ts +19 -2
  73. package/app/hooks/useImageUpload.ts +152 -0
  74. package/app/lib/acp/acp-tools.ts +3 -1
  75. package/app/lib/acp/agent-descriptors.ts +274 -0
  76. package/app/lib/acp/bridge.ts +6 -0
  77. package/app/lib/acp/index.ts +20 -4
  78. package/app/lib/acp/registry.ts +74 -7
  79. package/app/lib/acp/session.ts +490 -28
  80. package/app/lib/acp/subprocess.ts +307 -21
  81. package/app/lib/acp/types.ts +158 -20
  82. package/app/lib/actions.ts +57 -3
  83. package/app/lib/agent/model.ts +18 -3
  84. package/app/lib/agent/stream-consumer.ts +18 -0
  85. package/app/lib/agent/to-agent-messages.ts +25 -2
  86. package/app/lib/agent/tools.ts +56 -9
  87. package/app/lib/core/export.ts +116 -0
  88. package/app/lib/core/trash.ts +241 -0
  89. package/app/lib/fs.ts +47 -0
  90. package/app/lib/hooks/usePinnedFiles.ts +90 -0
  91. package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
  92. package/app/lib/i18n/index.ts +3 -0
  93. package/app/lib/i18n/modules/knowledge.ts +124 -6
  94. package/app/lib/i18n/modules/navigation.ts +2 -0
  95. package/app/lib/i18n/modules/onboarding.ts +2 -134
  96. package/app/lib/i18n/modules/panels.ts +146 -2
  97. package/app/lib/i18n/modules/settings.ts +12 -0
  98. package/app/lib/pi-integration/skills.ts +21 -6
  99. package/app/lib/renderers/index.ts +2 -2
  100. package/app/lib/settings.ts +10 -0
  101. package/app/lib/types.ts +12 -1
  102. package/app/next-env.d.ts +1 -1
  103. package/app/package.json +11 -3
  104. package/app/scripts/generate-explore.ts +145 -0
  105. package/package.json +1 -1
  106. package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
  107. package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
  108. package/app/components/explore/use-cases.ts +0 -58
  109. package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
  110. package/app/components/renderers/workflow/manifest.ts +0 -14
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * ACP Session Manager — High-level session lifecycle for ACP agents.
3
3
  * Manages session creation, prompt turns, cancellation, and cleanup.
4
+ * Implements full ACP spec: initialize → session/new → session/prompt → session/cancel → close.
4
5
  */
5
6
 
6
7
  import type {
@@ -9,6 +10,13 @@ import type {
9
10
  AcpSessionUpdate,
10
11
  AcpPromptResponse,
11
12
  AcpRegistryEntry,
13
+ AcpAgentCapabilities,
14
+ AcpMode,
15
+ AcpConfigOption,
16
+ AcpSessionInfo,
17
+ AcpStopReason,
18
+ AcpAuthMethod,
19
+ AcpContentBlock,
12
20
  } from './types';
13
21
  import {
14
22
  spawnAcpAgent,
@@ -16,6 +24,7 @@ import {
16
24
  sendMessage,
17
25
  onMessage,
18
26
  killAgent,
27
+ installAutoApproval,
19
28
  type AcpProcess,
20
29
  } from './subprocess';
21
30
  import { findAcpAgent } from './registry';
@@ -24,15 +33,17 @@ import { findAcpAgent } from './registry';
24
33
 
25
34
  const sessions = new Map<string, AcpSession>();
26
35
  const sessionProcesses = new Map<string, AcpProcess>();
36
+ const autoApprovalCleanups = new Map<string, () => void>();
27
37
 
28
- /* ── Public API ────────────────────────────────────────────────────────── */
38
+ /* ── Public API Session Lifecycle ───────────────────────────────────── */
29
39
 
30
40
  /**
31
41
  * Create a new ACP session by spawning an agent process.
42
+ * Full ACP lifecycle: spawn → initialize → authenticate (if needed) → session/new.
32
43
  */
33
44
  export async function createSession(
34
45
  agentId: string,
35
- options?: { env?: Record<string, string> },
46
+ options?: { env?: Record<string, string>; cwd?: string },
36
47
  ): Promise<AcpSession> {
37
48
  const entry = await findAcpAgent(agentId);
38
49
  if (!entry) {
@@ -47,37 +58,257 @@ export async function createSession(
47
58
  */
48
59
  export async function createSessionFromEntry(
49
60
  entry: AcpRegistryEntry,
50
- options?: { env?: Record<string, string> },
61
+ options?: { env?: Record<string, string>; cwd?: string },
51
62
  ): Promise<AcpSession> {
52
63
  const proc = spawnAcpAgent(entry, options);
53
64
 
54
- // Send session/new and wait for ack
65
+ // Install auto-approval BEFORE initialize so any early permission requests
66
+ // from the agent don't cause a hang waiting for TTY input.
67
+ const unsubApproval = installAutoApproval(proc);
68
+
69
+ let agentCapabilities: AcpAgentCapabilities | undefined;
70
+ let authMethods: AcpAuthMethod[] | undefined;
71
+
72
+ // Phase 1: Initialize — negotiate protocol and capabilities
55
73
  try {
56
- const response = await sendAndWait(proc, 'session/new', {}, 15_000);
74
+ const response = await sendAndWait(proc, 'initialize', {
75
+ protocolVersion: 1,
76
+ capabilities: {
77
+ fs: { readTextFile: true, writeTextFile: true },
78
+ terminal: true,
79
+ },
80
+ clientInfo: { name: 'mindos', version: '0.6.29' },
81
+ }, 30_000);
57
82
 
58
83
  if (response.error) {
84
+ unsubApproval();
59
85
  killAgent(proc);
60
- throw new Error(`session/new failed: ${response.error.message}`);
86
+ throw new Error(`initialize failed: ${response.error.message}`);
87
+ }
88
+
89
+ // Parse agent capabilities from response
90
+ const initResult = response.result as Record<string, unknown> | undefined;
91
+ if (initResult) {
92
+ agentCapabilities = parseAgentCapabilities(initResult.agentCapabilities);
93
+ authMethods = parseAuthMethods(initResult.authMethods);
61
94
  }
62
95
  } catch (err) {
96
+ unsubApproval();
63
97
  killAgent(proc);
64
98
  throw err;
65
99
  }
66
100
 
101
+ // Phase 2: Authenticate (if agent declares auth methods)
102
+ if (authMethods && authMethods.length > 0) {
103
+ try {
104
+ const authResponse = await sendAndWait(proc, 'authenticate', {
105
+ methodId: authMethods[0].id,
106
+ }, 15_000);
107
+
108
+ if (authResponse.error) {
109
+ // Authentication failed — non-fatal, log and continue
110
+ console.warn(`ACP authenticate warning for ${entry.id}: ${authResponse.error.message}`);
111
+ }
112
+ } catch {
113
+ // Best-effort auth — agent may not require it
114
+ }
115
+ }
116
+
117
+ // Phase 3: session/new — create the conversation session
118
+ let modes: AcpMode[] | undefined;
119
+ let configOptions: AcpConfigOption[] | undefined;
120
+
121
+ try {
122
+ const newResponse = await sendAndWait(proc, 'session/new', {
123
+ cwd: options?.cwd ?? process.cwd(),
124
+ mcpServers: [],
125
+ }, 15_000);
126
+
127
+ if (newResponse.error) {
128
+ const errMsg = newResponse.error.message ?? 'session/new failed';
129
+ // Authentication errors are fatal — agent cannot proceed without auth
130
+ if (/auth/i.test(errMsg)) {
131
+ unsubApproval();
132
+ killAgent(proc);
133
+ throw new Error(`${entry.id}: ${errMsg}`);
134
+ }
135
+ // Other errors are non-fatal: some agents may not support explicit session/new
136
+ console.warn(`ACP session/new warning for ${entry.id}: ${errMsg}`);
137
+ } else {
138
+ const newResult = newResponse.result as Record<string, unknown> | undefined;
139
+ if (newResult) {
140
+ modes = parseModes(newResult.modes);
141
+ configOptions = parseConfigOptions(newResult.configOptions);
142
+ }
143
+ }
144
+ } catch (sessionErr) {
145
+ // Re-throw auth errors — they are fatal
146
+ if (sessionErr instanceof Error && /auth/i.test(sessionErr.message)) throw sessionErr;
147
+ // Non-fatal: agent may not support explicit session/new (backwards compat)
148
+ }
149
+
67
150
  const sessionId = `ses-${entry.id}-${Date.now()}`;
68
151
  const session: AcpSession = {
69
152
  id: sessionId,
70
153
  agentId: entry.id,
71
154
  state: 'idle',
155
+ cwd: options?.cwd,
72
156
  createdAt: new Date().toISOString(),
73
157
  lastActivityAt: new Date().toISOString(),
158
+ agentCapabilities,
159
+ modes,
160
+ configOptions,
161
+ authMethods,
74
162
  };
75
163
 
76
164
  sessions.set(sessionId, session);
77
165
  sessionProcesses.set(sessionId, proc);
166
+ autoApprovalCleanups.set(sessionId, unsubApproval);
167
+ return session;
168
+ }
169
+
170
+ /**
171
+ * Load/resume an existing session on an agent.
172
+ * Requires agent to declare `loadSession` capability.
173
+ */
174
+ export async function loadSession(
175
+ agentId: string,
176
+ existingSessionId: string,
177
+ options?: { env?: Record<string, string>; cwd?: string },
178
+ ): Promise<AcpSession> {
179
+ const entry = await findAcpAgent(agentId);
180
+ if (!entry) {
181
+ throw new Error(`ACP agent not found in registry: ${agentId}`);
182
+ }
183
+
184
+ const proc = spawnAcpAgent(entry, options);
185
+ const unsubApproval = installAutoApproval(proc);
186
+
187
+ let agentCapabilities: AcpAgentCapabilities | undefined;
188
+
189
+ // Initialize
190
+ try {
191
+ const initResponse = await sendAndWait(proc, 'initialize', {
192
+ protocolVersion: 1,
193
+ capabilities: {
194
+ fs: { readTextFile: true, writeTextFile: true },
195
+ terminal: true,
196
+ },
197
+ clientInfo: { name: 'mindos', version: '0.6.29' },
198
+ }, 30_000);
199
+
200
+ if (initResponse.error) {
201
+ unsubApproval();
202
+ killAgent(proc);
203
+ throw new Error(`initialize failed: ${initResponse.error.message}`);
204
+ }
205
+
206
+ const initResult = initResponse.result as Record<string, unknown> | undefined;
207
+ if (initResult) {
208
+ agentCapabilities = parseAgentCapabilities(initResult.agentCapabilities);
209
+ }
210
+ } catch (err) {
211
+ unsubApproval();
212
+ killAgent(proc);
213
+ throw err;
214
+ }
215
+
216
+ // Check if agent supports loadSession
217
+ if (!agentCapabilities?.loadSession) {
218
+ unsubApproval();
219
+ killAgent(proc);
220
+ throw new Error(`Agent ${agentId} does not support session/load (loadSession capability not declared)`);
221
+ }
222
+
223
+ // session/load — resume the existing session
224
+ let modes: AcpMode[] | undefined;
225
+ let configOptions: AcpConfigOption[] | undefined;
226
+
227
+ try {
228
+ const loadResponse = await sendAndWait(proc, 'session/load', {
229
+ sessionId: existingSessionId,
230
+ cwd: options?.cwd ?? process.cwd(),
231
+ mcpServers: [],
232
+ }, 15_000);
233
+
234
+ if (loadResponse.error) {
235
+ unsubApproval();
236
+ killAgent(proc);
237
+ throw new Error(`session/load failed: ${loadResponse.error.message}`);
238
+ }
239
+
240
+ const loadResult = loadResponse.result as Record<string, unknown> | undefined;
241
+ if (loadResult) {
242
+ modes = parseModes(loadResult.modes);
243
+ configOptions = parseConfigOptions(loadResult.configOptions);
244
+ }
245
+ } catch (err) {
246
+ unsubApproval();
247
+ killAgent(proc);
248
+ throw err;
249
+ }
250
+
251
+ // Use the original sessionId since we're resuming
252
+ const session: AcpSession = {
253
+ id: existingSessionId,
254
+ agentId: entry.id,
255
+ state: 'idle',
256
+ cwd: options?.cwd,
257
+ createdAt: new Date().toISOString(),
258
+ lastActivityAt: new Date().toISOString(),
259
+ agentCapabilities,
260
+ modes,
261
+ configOptions,
262
+ };
263
+
264
+ sessions.set(existingSessionId, session);
265
+ sessionProcesses.set(existingSessionId, proc);
266
+ autoApprovalCleanups.set(existingSessionId, unsubApproval);
78
267
  return session;
79
268
  }
80
269
 
270
+ /**
271
+ * List resumable sessions from the agent.
272
+ * Requires agent to declare `sessionCapabilities.list`.
273
+ */
274
+ export async function listSessions(
275
+ sessionId: string,
276
+ options?: { cursor?: string; cwd?: string },
277
+ ): Promise<{ sessions: AcpSessionInfo[]; nextCursor?: string }> {
278
+ const { session, proc } = getSessionAndProc(sessionId);
279
+
280
+ if (!session.agentCapabilities?.sessionCapabilities?.list) {
281
+ throw new Error('Agent does not support session/list');
282
+ }
283
+
284
+ const response = await sendAndWait(proc, 'session/list', {
285
+ ...(options?.cursor ? { cursor: options.cursor } : {}),
286
+ ...(options?.cwd ? { cwd: options.cwd } : {}),
287
+ }, 10_000);
288
+
289
+ if (response.error) {
290
+ throw new Error(`session/list failed: ${response.error.message}`);
291
+ }
292
+
293
+ const result = response.result as Record<string, unknown> | undefined;
294
+ const rawSessions = Array.isArray(result?.sessions) ? result.sessions : [];
295
+
296
+ return {
297
+ sessions: rawSessions.map((s: unknown) => {
298
+ const obj = s as Record<string, unknown>;
299
+ return {
300
+ sessionId: String(obj.sessionId ?? ''),
301
+ title: typeof obj.title === 'string' ? obj.title : undefined,
302
+ cwd: typeof obj.cwd === 'string' ? obj.cwd : undefined,
303
+ updatedAt: typeof obj.updatedAt === 'string' ? obj.updatedAt : undefined,
304
+ };
305
+ }),
306
+ nextCursor: typeof result?.nextCursor === 'string' ? result.nextCursor : undefined,
307
+ };
308
+ }
309
+
310
+ /* ── Public API — Prompt ──────────────────────────────────────────────── */
311
+
81
312
  /**
82
313
  * Send a prompt to an active session and collect the full response.
83
314
  * For streaming, use promptStream() instead.
@@ -95,7 +326,11 @@ export async function prompt(
95
326
  updateSessionState(session, 'active');
96
327
 
97
328
  try {
98
- const response = await sendAndWait(proc, 'session/prompt', { text }, 60_000);
329
+ const response = await sendAndWait(proc, 'session/prompt', {
330
+ sessionId,
331
+ prompt: [{ type: 'text', text }] satisfies AcpContentBlock[],
332
+ ...(session.cwd ? { context: { cwd: session.cwd } } : {}),
333
+ }, 60_000);
99
334
 
100
335
  if (response.error) {
101
336
  updateSessionState(session, 'error');
@@ -108,6 +343,7 @@ export async function prompt(
108
343
  sessionId,
109
344
  text: String(result?.text ?? ''),
110
345
  done: true,
346
+ stopReason: parseStopReason(result?.stopReason),
111
347
  toolCalls: result?.toolCalls as AcpPromptResponse['toolCalls'],
112
348
  metadata: result?.metadata as AcpPromptResponse['metadata'],
113
349
  };
@@ -119,6 +355,7 @@ export async function prompt(
119
355
 
120
356
  /**
121
357
  * Send a prompt and receive streaming updates via callback.
358
+ * Handles all 10 ACP session/update types.
122
359
  * Returns the final aggregated response.
123
360
  */
124
361
  export async function promptStream(
@@ -136,47 +373,79 @@ export async function promptStream(
136
373
 
137
374
  return new Promise((resolve, reject) => {
138
375
  let aggregatedText = '';
376
+ let stopReason: AcpStopReason = 'end_turn';
139
377
 
140
378
  const unsub = onMessage(proc, (msg) => {
141
- // Handle streaming notifications (no id field, or notification pattern)
142
379
  if (msg.result && typeof msg.result === 'object') {
143
- const update = msg.result as Record<string, unknown>;
144
- const sessionUpdate: AcpSessionUpdate = {
145
- sessionId,
146
- type: (update.type as AcpSessionUpdate['type']) ?? 'text',
147
- text: update.text as string | undefined,
148
- toolCall: update.toolCall as AcpSessionUpdate['toolCall'],
149
- toolResult: update.toolResult as AcpSessionUpdate['toolResult'],
150
- error: update.error as string | undefined,
151
- };
380
+ const raw = msg.result as Record<string, unknown>;
381
+ const update = parseSessionUpdate(sessionId, raw);
152
382
 
153
- onUpdate(sessionUpdate);
383
+ onUpdate(update);
154
384
 
155
- if (sessionUpdate.type === 'text' && sessionUpdate.text) {
156
- aggregatedText += sessionUpdate.text;
385
+ // Aggregate text from message chunk types
386
+ if ((update.type === 'agent_message_chunk' || update.type === 'text') && update.text) {
387
+ aggregatedText += update.text;
157
388
  }
158
389
 
159
- if (sessionUpdate.type === 'done') {
390
+ // Handle terminal states
391
+ if (update.type === 'done') {
160
392
  unsub();
393
+ if (raw.stopReason) {
394
+ stopReason = parseStopReason(raw.stopReason);
395
+ }
161
396
  updateSessionState(session, 'idle');
162
397
  resolve({
163
398
  sessionId,
164
399
  text: aggregatedText,
165
400
  done: true,
401
+ stopReason,
166
402
  });
167
403
  }
168
404
 
169
- if (sessionUpdate.type === 'error') {
405
+ if (update.type === 'error') {
170
406
  unsub();
171
407
  updateSessionState(session, 'error');
172
- reject(new Error(sessionUpdate.error ?? 'Unknown ACP error'));
408
+ reject(new Error(update.error ?? 'Unknown ACP error'));
409
+ }
410
+
411
+ // Update session metadata from config/mode updates
412
+ if (update.type === 'config_option_update' && update.configOptions) {
413
+ session.configOptions = update.configOptions;
414
+ }
415
+ if (update.type === 'current_mode_update' && update.currentModeId) {
416
+ // Track current mode
417
+ session.lastActivityAt = new Date().toISOString();
173
418
  }
174
419
  }
175
420
  });
176
421
 
177
- // Send the prompt
422
+ // Guard against agent process dying unexpectedly (OOM, SIGKILL, etc.)
423
+ // Without this, the Promise would hang forever if the process exits
424
+ // without sending a done/error notification.
425
+ const onExit = () => {
426
+ unsub();
427
+ updateSessionState(session, 'error');
428
+ reject(new Error(`ACP agent process exited unexpectedly during prompt`));
429
+ };
430
+ proc.proc.once('exit', onExit);
431
+
432
+ // Clean up exit listener when Promise resolves/rejects normally
433
+ const cleanup = () => { proc.proc.removeListener('exit', onExit); };
434
+ // Wrap resolve/reject to include cleanup — but we already unsub in the message handler.
435
+ // The exit listener is a safety net; if done/error fires first, remove the exit listener.
436
+ const origResolve = resolve;
437
+ const origReject = reject;
438
+ resolve = ((val: AcpPromptResponse) => { cleanup(); origResolve(val); }) as typeof resolve;
439
+ reject = ((err: unknown) => { cleanup(); origReject(err); }) as typeof reject;
440
+
441
+ // Send the prompt with ContentBlock format
178
442
  try {
179
- sendMessage(proc, 'session/prompt', { text, stream: true });
443
+ sendMessage(proc, 'session/prompt', {
444
+ sessionId,
445
+ prompt: [{ type: 'text', text }] satisfies AcpContentBlock[],
446
+ stream: true,
447
+ ...(session.cwd ? { context: { cwd: session.cwd } } : {}),
448
+ });
180
449
  } catch (err) {
181
450
  unsub();
182
451
  updateSessionState(session, 'error');
@@ -185,6 +454,8 @@ export async function promptStream(
185
454
  });
186
455
  }
187
456
 
457
+ /* ── Public API — Session Control ─────────────────────────────────────── */
458
+
188
459
  /**
189
460
  * Cancel the current prompt turn on a session.
190
461
  */
@@ -194,7 +465,7 @@ export async function cancelPrompt(sessionId: string): Promise<void> {
194
465
  if (session.state !== 'active') return;
195
466
 
196
467
  try {
197
- await sendAndWait(proc, 'session/cancel', {}, 5_000);
468
+ await sendAndWait(proc, 'session/cancel', { sessionId }, 10_000);
198
469
  } catch {
199
470
  // Best-effort cancel — don't throw if the agent doesn't support it
200
471
  }
@@ -202,6 +473,54 @@ export async function cancelPrompt(sessionId: string): Promise<void> {
202
473
  updateSessionState(session, 'idle');
203
474
  }
204
475
 
476
+ /**
477
+ * Set the operating mode for a session.
478
+ */
479
+ export async function setMode(sessionId: string, modeId: string): Promise<void> {
480
+ const { session, proc } = getSessionAndProc(sessionId);
481
+
482
+ const response = await sendAndWait(proc, 'session/set_mode', {
483
+ sessionId,
484
+ modeId,
485
+ }, 10_000);
486
+
487
+ if (response.error) {
488
+ throw new Error(`session/set_mode failed: ${response.error.message}`);
489
+ }
490
+
491
+ session.lastActivityAt = new Date().toISOString();
492
+ }
493
+
494
+ /**
495
+ * Set a configuration option for a session.
496
+ */
497
+ export async function setConfigOption(
498
+ sessionId: string,
499
+ configId: string,
500
+ value: string,
501
+ ): Promise<AcpConfigOption[]> {
502
+ const { session, proc } = getSessionAndProc(sessionId);
503
+
504
+ const response = await sendAndWait(proc, 'session/set_config_option', {
505
+ sessionId,
506
+ configId,
507
+ value,
508
+ }, 10_000);
509
+
510
+ if (response.error) {
511
+ throw new Error(`session/set_config_option failed: ${response.error.message}`);
512
+ }
513
+
514
+ const result = response.result as Record<string, unknown> | undefined;
515
+ const configOptions = parseConfigOptions(result?.configOptions);
516
+ if (configOptions) {
517
+ session.configOptions = configOptions;
518
+ }
519
+
520
+ session.lastActivityAt = new Date().toISOString();
521
+ return session.configOptions ?? [];
522
+ }
523
+
205
524
  /**
206
525
  * Close a session and terminate the subprocess.
207
526
  */
@@ -210,7 +529,7 @@ export async function closeSession(sessionId: string): Promise<void> {
210
529
 
211
530
  if (proc?.alive) {
212
531
  try {
213
- await sendAndWait(proc, 'session/close', {}, 5_000);
532
+ await sendAndWait(proc, 'session/close', { sessionId }, 5_000);
214
533
  } catch {
215
534
  // Best-effort close
216
535
  }
@@ -219,8 +538,15 @@ export async function closeSession(sessionId: string): Promise<void> {
219
538
 
220
539
  sessions.delete(sessionId);
221
540
  sessionProcesses.delete(sessionId);
541
+ const cleanup = autoApprovalCleanups.get(sessionId);
542
+ if (cleanup) {
543
+ cleanup();
544
+ autoApprovalCleanups.delete(sessionId);
545
+ }
222
546
  }
223
547
 
548
+ /* ── Public API — Queries ─────────────────────────────────────────────── */
549
+
224
550
  /**
225
551
  * Get a session by its ID.
226
552
  */
@@ -243,7 +569,7 @@ export async function closeAllSessions(): Promise<void> {
243
569
  await Promise.allSettled(ids.map(id => closeSession(id)));
244
570
  }
245
571
 
246
- /* ── Internal ──────────────────────────────────────────────────────────── */
572
+ /* ── Internal Session helpers ───────────────────────────────────────── */
247
573
 
248
574
  function getSessionAndProc(sessionId: string): { session: AcpSession; proc: AcpProcess } {
249
575
  const session = sessions.get(sessionId);
@@ -262,3 +588,139 @@ function updateSessionState(session: AcpSession, state: AcpSessionState): void {
262
588
  session.state = state;
263
589
  session.lastActivityAt = new Date().toISOString();
264
590
  }
591
+
592
+ /* ── Internal — Parsers ───────────────────────────────────────────────── */
593
+
594
+ function parseStopReason(raw: unknown): AcpStopReason {
595
+ const valid: AcpStopReason[] = ['end_turn', 'max_tokens', 'max_turn_requests', 'refusal', 'cancelled'];
596
+ return valid.includes(raw as AcpStopReason) ? (raw as AcpStopReason) : 'end_turn';
597
+ }
598
+
599
+ function parseAgentCapabilities(raw: unknown): AcpAgentCapabilities | undefined {
600
+ if (!raw || typeof raw !== 'object') return undefined;
601
+ const obj = raw as Record<string, unknown>;
602
+ return {
603
+ loadSession: obj.loadSession === true,
604
+ mcpCapabilities: typeof obj.mcpCapabilities === 'object' ? obj.mcpCapabilities as AcpAgentCapabilities['mcpCapabilities'] : undefined,
605
+ promptCapabilities: typeof obj.promptCapabilities === 'object' ? obj.promptCapabilities as AcpAgentCapabilities['promptCapabilities'] : undefined,
606
+ sessionCapabilities: typeof obj.sessionCapabilities === 'object' ? obj.sessionCapabilities as AcpAgentCapabilities['sessionCapabilities'] : undefined,
607
+ };
608
+ }
609
+
610
+ function parseAuthMethods(raw: unknown): AcpAuthMethod[] | undefined {
611
+ if (!Array.isArray(raw) || raw.length === 0) return undefined;
612
+ return raw
613
+ .filter((m): m is Record<string, unknown> => !!m && typeof m === 'object')
614
+ .map(m => ({
615
+ id: String(m.id ?? ''),
616
+ name: String(m.name ?? ''),
617
+ description: typeof m.description === 'string' ? m.description : undefined,
618
+ }))
619
+ .filter(m => m.id && m.name);
620
+ }
621
+
622
+ function parseModes(raw: unknown): AcpMode[] | undefined {
623
+ if (!Array.isArray(raw) || raw.length === 0) return undefined;
624
+ return raw
625
+ .filter((m): m is Record<string, unknown> => !!m && typeof m === 'object')
626
+ .map(m => ({
627
+ id: String(m.id ?? ''),
628
+ name: String(m.name ?? ''),
629
+ description: typeof m.description === 'string' ? m.description : undefined,
630
+ }))
631
+ .filter(m => m.id && m.name);
632
+ }
633
+
634
+ function parseConfigOptions(raw: unknown): AcpConfigOption[] | undefined {
635
+ if (!Array.isArray(raw) || raw.length === 0) return undefined;
636
+ return raw
637
+ .filter((o): o is Record<string, unknown> => !!o && typeof o === 'object')
638
+ .map(o => ({
639
+ type: 'select' as const,
640
+ configId: String(o.configId ?? o.id ?? ''),
641
+ category: String(o.category ?? 'other'),
642
+ label: typeof o.label === 'string' ? o.label : undefined,
643
+ currentValue: String(o.currentValue ?? ''),
644
+ options: Array.isArray(o.options) ? o.options.map((opt: unknown) => {
645
+ const optObj = opt as Record<string, unknown>;
646
+ return { id: String(optObj.id ?? ''), label: String(optObj.label ?? '') };
647
+ }) : [],
648
+ }))
649
+ .filter(o => o.configId);
650
+ }
651
+
652
+ /** Parse a raw session/update notification into a typed AcpSessionUpdate. */
653
+ function parseSessionUpdate(sessionId: string, raw: Record<string, unknown>): AcpSessionUpdate {
654
+ const type = raw.type as AcpSessionUpdate['type'] ?? 'text';
655
+
656
+ const base: AcpSessionUpdate = { sessionId, type };
657
+
658
+ switch (type) {
659
+ case 'agent_message_chunk':
660
+ case 'user_message_chunk':
661
+ case 'agent_thought_chunk':
662
+ case 'text':
663
+ base.text = typeof raw.text === 'string' ? raw.text
664
+ : typeof raw.content === 'string' ? raw.content
665
+ : undefined;
666
+ break;
667
+
668
+ case 'tool_call':
669
+ case 'tool_call_update':
670
+ if (raw.toolCall && typeof raw.toolCall === 'object') {
671
+ base.toolCall = raw.toolCall as AcpSessionUpdate['toolCall'];
672
+ } else {
673
+ // Top-level tool call fields
674
+ base.toolCall = {
675
+ toolCallId: String(raw.toolCallId ?? ''),
676
+ title: typeof raw.title === 'string' ? raw.title : undefined,
677
+ kind: raw.kind as AcpSessionUpdate['toolCall'] extends { kind: infer K } ? K : undefined,
678
+ status: (raw.status as 'pending' | 'in_progress' | 'completed' | 'failed') ?? 'pending',
679
+ rawInput: typeof raw.rawInput === 'string' ? raw.rawInput : undefined,
680
+ rawOutput: typeof raw.rawOutput === 'string' ? raw.rawOutput : undefined,
681
+ };
682
+ }
683
+ break;
684
+
685
+ case 'plan':
686
+ if (raw.entries && Array.isArray(raw.entries)) {
687
+ base.plan = { entries: raw.entries as AcpSessionUpdate['plan'] extends { entries: infer E } ? E : never };
688
+ } else if (raw.plan && typeof raw.plan === 'object') {
689
+ base.plan = raw.plan as AcpSessionUpdate['plan'];
690
+ }
691
+ break;
692
+
693
+ case 'available_commands_update':
694
+ base.availableCommands = Array.isArray(raw.availableCommands) ? raw.availableCommands : undefined;
695
+ break;
696
+
697
+ case 'current_mode_update':
698
+ base.currentModeId = typeof raw.currentModeId === 'string' ? raw.currentModeId : undefined;
699
+ break;
700
+
701
+ case 'config_option_update':
702
+ base.configOptions = parseConfigOptions(raw.configOptions);
703
+ break;
704
+
705
+ case 'session_info_update':
706
+ base.sessionInfo = {
707
+ title: typeof raw.title === 'string' ? raw.title : undefined,
708
+ updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : undefined,
709
+ };
710
+ break;
711
+
712
+ case 'error':
713
+ base.error = typeof raw.error === 'string' ? raw.error : String(raw.message ?? 'Unknown error');
714
+ break;
715
+
716
+ case 'done':
717
+ // Terminal state — no extra fields
718
+ break;
719
+
720
+ case 'tool_result':
721
+ base.toolResult = raw.toolResult as AcpSessionUpdate['toolResult'];
722
+ break;
723
+ }
724
+
725
+ return base;
726
+ }