@geminilight/mindos 0.6.29 → 0.6.30

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 (71) hide show
  1. package/README.md +10 -4
  2. package/app/app/api/acp/config/route.ts +82 -0
  3. package/app/app/api/acp/detect/route.ts +71 -48
  4. package/app/app/api/acp/install/route.ts +51 -0
  5. package/app/app/api/acp/session/route.ts +141 -11
  6. package/app/app/api/ask/route.ts +116 -13
  7. package/app/app/api/workflows/route.ts +156 -0
  8. package/app/app/page.tsx +7 -2
  9. package/app/components/ActivityBar.tsx +12 -4
  10. package/app/components/AskModal.tsx +4 -1
  11. package/app/components/FileTree.tsx +21 -10
  12. package/app/components/HomeContent.tsx +1 -0
  13. package/app/components/Panel.tsx +1 -0
  14. package/app/components/RightAskPanel.tsx +5 -1
  15. package/app/components/SidebarLayout.tsx +6 -0
  16. package/app/components/agents/AgentDetailContent.tsx +263 -47
  17. package/app/components/agents/AgentsContentPage.tsx +11 -0
  18. package/app/components/agents/AgentsPanelA2aTab.tsx +285 -46
  19. package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
  20. package/app/components/agents/agents-content-model.ts +2 -2
  21. package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
  22. package/app/components/ask/AskContent.tsx +197 -239
  23. package/app/components/ask/FileChip.tsx +82 -17
  24. package/app/components/ask/MentionPopover.tsx +21 -3
  25. package/app/components/ask/MessageList.tsx +30 -9
  26. package/app/components/ask/SlashCommandPopover.tsx +21 -3
  27. package/app/components/panels/AgentsPanel.tsx +1 -0
  28. package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
  29. package/app/components/panels/WorkflowsPanel.tsx +206 -0
  30. package/app/components/renderers/workflow-yaml/StepEditor.tsx +157 -0
  31. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +201 -0
  32. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +226 -0
  33. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
  34. package/app/components/renderers/workflow-yaml/execution.ts +177 -0
  35. package/app/components/renderers/workflow-yaml/index.ts +6 -0
  36. package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
  37. package/app/components/renderers/workflow-yaml/parser.ts +172 -0
  38. package/app/components/renderers/workflow-yaml/selectors.tsx +522 -0
  39. package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
  40. package/app/components/renderers/workflow-yaml/types.ts +46 -0
  41. package/app/hooks/useAcpConfig.ts +96 -0
  42. package/app/hooks/useAcpDetection.ts +69 -14
  43. package/app/hooks/useAcpRegistry.ts +46 -11
  44. package/app/hooks/useAskModal.ts +12 -5
  45. package/app/hooks/useAskPanel.ts +8 -5
  46. package/app/hooks/useAskSession.ts +19 -2
  47. package/app/hooks/useImageUpload.ts +152 -0
  48. package/app/lib/acp/acp-tools.ts +3 -1
  49. package/app/lib/acp/agent-descriptors.ts +274 -0
  50. package/app/lib/acp/bridge.ts +6 -0
  51. package/app/lib/acp/index.ts +20 -4
  52. package/app/lib/acp/registry.ts +74 -7
  53. package/app/lib/acp/session.ts +481 -28
  54. package/app/lib/acp/subprocess.ts +307 -21
  55. package/app/lib/acp/types.ts +158 -20
  56. package/app/lib/agent/model.ts +18 -3
  57. package/app/lib/agent/to-agent-messages.ts +25 -2
  58. package/app/lib/i18n/modules/knowledge.ts +4 -0
  59. package/app/lib/i18n/modules/navigation.ts +2 -0
  60. package/app/lib/i18n/modules/panels.ts +146 -2
  61. package/app/lib/pi-integration/skills.ts +21 -6
  62. package/app/lib/renderers/index.ts +2 -2
  63. package/app/lib/settings.ts +10 -0
  64. package/app/lib/types.ts +12 -1
  65. package/app/next-env.d.ts +1 -1
  66. package/app/package.json +3 -1
  67. package/package.json +1 -1
  68. package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
  69. package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
  70. package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
  71. package/app/components/renderers/workflow/manifest.ts +0 -14
@@ -8,8 +8,17 @@ import type {
8
8
  AcpJsonRpcRequest,
9
9
  AcpJsonRpcResponse,
10
10
  AcpRegistryEntry,
11
- AcpTransportType,
12
11
  } from './types';
12
+ import { resolveAgentCommand } from './agent-descriptors';
13
+ import { readSettings } from '../settings';
14
+
15
+ /** Incoming JSON-RPC request from agent (bidirectional — agent asks US for permission). */
16
+ export interface AcpIncomingRequest {
17
+ jsonrpc: '2.0';
18
+ id: string | number;
19
+ method: string;
20
+ params?: Record<string, unknown>;
21
+ }
13
22
 
14
23
  /* ── Types ─────────────────────────────────────────────────────────────── */
15
24
 
@@ -21,11 +30,13 @@ export interface AcpProcess {
21
30
  }
22
31
 
23
32
  type MessageCallback = (msg: AcpJsonRpcResponse) => void;
33
+ type RequestCallback = (req: AcpIncomingRequest) => void;
24
34
 
25
35
  /* ── State ─────────────────────────────────────────────────────────────── */
26
36
 
27
37
  const processes = new Map<string, AcpProcess>();
28
38
  const messageListeners = new Map<string, Set<MessageCallback>>();
39
+ const requestListeners = new Map<string, Set<RequestCallback>>();
29
40
  let rpcIdCounter = 1;
30
41
 
31
42
  /* ── Public API ────────────────────────────────────────────────────────── */
@@ -35,13 +46,17 @@ let rpcIdCounter = 1;
35
46
  */
36
47
  export function spawnAcpAgent(
37
48
  entry: AcpRegistryEntry,
38
- options?: { env?: Record<string, string> },
49
+ options?: { env?: Record<string, string>; cwd?: string },
39
50
  ): AcpProcess {
40
- const { cmd, args } = buildCommand(entry);
51
+ const settings = readSettings();
52
+ const userOverride = settings.acpAgents?.[entry.id];
53
+ const resolved = resolveAgentCommand(entry.id, entry, userOverride);
54
+ const { cmd, args } = { cmd: resolved.cmd, args: resolved.args };
41
55
 
42
56
  const mergedEnv = {
43
57
  ...process.env,
44
58
  ...(entry.env ?? {}),
59
+ ...(resolved.env ?? {}),
45
60
  ...(options?.env ?? {}),
46
61
  };
47
62
 
@@ -49,6 +64,7 @@ export function spawnAcpAgent(
49
64
  stdio: ['pipe', 'pipe', 'pipe'],
50
65
  env: mergedEnv,
51
66
  shell: false,
67
+ ...(options?.cwd ? { cwd: options.cwd } : {}),
52
68
  });
53
69
 
54
70
  const id = `acp-${entry.id}-${Date.now()}`;
@@ -56,6 +72,7 @@ export function spawnAcpAgent(
56
72
 
57
73
  processes.set(id, acpProc);
58
74
  messageListeners.set(id, new Set());
75
+ requestListeners.set(id, new Set());
59
76
 
60
77
  // Parse newline-delimited JSON from stdout
61
78
  let buffer = '';
@@ -68,10 +85,23 @@ export function spawnAcpAgent(
68
85
  const trimmed = line.trim();
69
86
  if (!trimmed) continue;
70
87
  try {
71
- const msg = JSON.parse(trimmed) as AcpJsonRpcResponse;
72
- const listeners = messageListeners.get(id);
73
- if (listeners) {
74
- for (const cb of listeners) cb(msg);
88
+ const msg = JSON.parse(trimmed);
89
+
90
+ // Distinguish incoming requests (agent → client) from responses (to our requests).
91
+ // Requests have `method` and `id` but no `result`/`error`.
92
+ const isRequest = msg.method && msg.id !== undefined
93
+ && !('result' in msg) && !('error' in msg);
94
+
95
+ if (isRequest) {
96
+ const reqListeners = requestListeners.get(id);
97
+ if (reqListeners) {
98
+ for (const cb of reqListeners) cb(msg as AcpIncomingRequest);
99
+ }
100
+ } else {
101
+ const listeners = messageListeners.get(id);
102
+ if (listeners) {
103
+ for (const cb of listeners) cb(msg as AcpJsonRpcResponse);
104
+ }
75
105
  }
76
106
  } catch {
77
107
  // Not valid JSON — skip (could be agent debug output)
@@ -79,13 +109,26 @@ export function spawnAcpAgent(
79
109
  }
80
110
  });
81
111
 
82
- proc.on('close', () => {
112
+ // Capture stderr for debugging (agents may log startup errors there)
113
+ let stderrBuf = '';
114
+ proc.stderr?.on('data', (chunk: Buffer) => {
115
+ stderrBuf += chunk.toString();
116
+ // Keep only last 4KB to avoid unbounded memory
117
+ if (stderrBuf.length > 4096) stderrBuf = stderrBuf.slice(-4096);
118
+ });
119
+
120
+ proc.on('close', (code) => {
83
121
  acpProc.alive = false;
122
+ if (code && code !== 0 && stderrBuf.trim()) {
123
+ console.error(`[ACP] ${entry.id} exited with code ${code}: ${stderrBuf.trim().slice(0, 500)}`);
124
+ }
84
125
  messageListeners.delete(id);
126
+ requestListeners.delete(id);
85
127
  });
86
128
 
87
- proc.on('error', () => {
129
+ proc.on('error', (err) => {
88
130
  acpProc.alive = false;
131
+ console.error(`[ACP] ${entry.id} spawn error:`, err.message);
89
132
  });
90
133
 
91
134
  return acpProc;
@@ -166,6 +209,15 @@ export function killAgent(acpProc: AcpProcess): void {
166
209
  acpProc.alive = false;
167
210
  processes.delete(acpProc.id);
168
211
  messageListeners.delete(acpProc.id);
212
+ requestListeners.delete(acpProc.id);
213
+ // Clean up any terminals spawned by this process
214
+ const terms = terminalMaps.get(acpProc.id);
215
+ if (terms) {
216
+ for (const entry of terms.values()) {
217
+ if (entry.child.exitCode === null) entry.child.kill('SIGTERM');
218
+ }
219
+ terminalMaps.delete(acpProc.id);
220
+ }
169
221
  }
170
222
 
171
223
  /**
@@ -191,19 +243,253 @@ export function killAllAgents(): void {
191
243
  }
192
244
  }
193
245
 
194
- /* ── Internal ──────────────────────────────────────────────────────────── */
246
+ /**
247
+ * Register a callback for incoming JSON-RPC REQUESTS from the agent
248
+ * (bidirectional: agent asks client for permission / capability).
249
+ * Returns an unsubscribe function.
250
+ */
251
+ export function onRequest(acpProc: AcpProcess, callback: RequestCallback): () => void {
252
+ const listeners = requestListeners.get(acpProc.id);
253
+ if (!listeners) throw new Error(`ACP process ${acpProc.id} not found`);
254
+
255
+ listeners.add(callback);
256
+ return () => { listeners.delete(callback); };
257
+ }
258
+
259
+ /**
260
+ * Send a raw JSON-RPC response back to the agent's stdin.
261
+ * Used for replying to incoming requests (e.g. permission approvals).
262
+ */
263
+ export function sendResponse(
264
+ acpProc: AcpProcess,
265
+ id: string | number,
266
+ result: unknown,
267
+ ): void {
268
+ if (!acpProc.alive || !acpProc.proc.stdin?.writable) {
269
+ throw new Error(`ACP process ${acpProc.id} is not alive`);
270
+ }
271
+
272
+ const response: AcpJsonRpcResponse = {
273
+ jsonrpc: '2.0',
274
+ id,
275
+ result,
276
+ };
277
+ acpProc.proc.stdin.write(JSON.stringify(response) + '\n');
278
+ }
279
+
280
+ /**
281
+ * Install auto-approval for all incoming permission/capability requests.
282
+ * Agents in ACP mode send requests like fs/read, fs/write, terminal/execute etc.
283
+ * Without approval, the agent hangs waiting for TTY input that never comes.
284
+ * Returns an unsubscribe function.
285
+ */
286
+ export function installAutoApproval(acpProc: AcpProcess): () => void {
287
+ return onRequest(acpProc, (req) => {
288
+ const method = req.method;
289
+ const params = (req.params ?? {}) as Record<string, unknown>;
290
+
291
+ switch (method) {
292
+ // ── File system: read ──
293
+ case 'fs/read_text_file': {
294
+ const filePath = String(params.path ?? '');
295
+ if (!filePath) {
296
+ sendResponse(acpProc, req.id, { error: { code: -32602, message: 'path is required' } });
297
+ return;
298
+ }
299
+ try {
300
+ const fs = require('fs');
301
+ const line = typeof params.line === 'number' ? params.line : undefined;
302
+ const limit = typeof params.limit === 'number' ? params.limit : undefined;
303
+ let content = fs.readFileSync(filePath, 'utf-8') as string;
304
+ if (line !== undefined || limit !== undefined) {
305
+ const lines = content.split('\n');
306
+ const start = (line ?? 1) - 1; // 1-based to 0-based
307
+ const end = limit !== undefined ? start + limit : lines.length;
308
+ content = lines.slice(Math.max(0, start), end).join('\n');
309
+ }
310
+ sendResponse(acpProc, req.id, { content });
311
+ } catch (err) {
312
+ sendResponse(acpProc, req.id, { error: { code: -32002, message: (err as Error).message } });
313
+ }
314
+ return;
315
+ }
316
+
317
+ // ── File system: write ──
318
+ case 'fs/write_text_file': {
319
+ const filePath = String(params.path ?? '');
320
+ const content = String(params.content ?? '');
321
+ if (!filePath) {
322
+ sendResponse(acpProc, req.id, { error: { code: -32602, message: 'path is required' } });
323
+ return;
324
+ }
325
+ try {
326
+ const fs = require('fs');
327
+ const path = require('path');
328
+ const dir = path.dirname(filePath);
329
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
330
+ fs.writeFileSync(filePath, content, 'utf-8');
331
+ sendResponse(acpProc, req.id, {});
332
+ } catch (err) {
333
+ sendResponse(acpProc, req.id, { error: { code: -32603, message: (err as Error).message } });
334
+ }
335
+ return;
336
+ }
337
+
338
+ // ── Terminal: create ──
339
+ case 'terminal/create': {
340
+ const command = String(params.command ?? '');
341
+ const args = Array.isArray(params.args) ? params.args.map(String) : [];
342
+ const cwd = typeof params.cwd === 'string' ? params.cwd : undefined;
343
+ const env = (params.env && typeof params.env === 'object') ? params.env as Record<string, string> : undefined;
344
+ const outputByteLimit = typeof params.outputByteLimit === 'number' ? params.outputByteLimit : 1_000_000;
345
+
346
+ if (!command) {
347
+ sendResponse(acpProc, req.id, { error: { code: -32602, message: 'command is required' } });
348
+ return;
349
+ }
350
+
351
+ const terminalId = `term-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
352
+ try {
353
+ const { spawn: spawnChild } = require('child_process');
354
+ const child = spawnChild(command, args, {
355
+ cwd,
356
+ env: { ...process.env, ...(env ?? {}) },
357
+ shell: true,
358
+ stdio: ['pipe', 'pipe', 'pipe'],
359
+ });
360
+
361
+ let output = '';
362
+ let truncated = false;
363
+
364
+ child.stdout?.on('data', (chunk: Buffer) => {
365
+ if (output.length < outputByteLimit) {
366
+ output += chunk.toString();
367
+ if (output.length > outputByteLimit) {
368
+ output = output.slice(0, outputByteLimit);
369
+ truncated = true;
370
+ }
371
+ }
372
+ });
373
+ child.stderr?.on('data', (chunk: Buffer) => {
374
+ if (output.length < outputByteLimit) {
375
+ output += chunk.toString();
376
+ if (output.length > outputByteLimit) {
377
+ output = output.slice(0, outputByteLimit);
378
+ truncated = true;
379
+ }
380
+ }
381
+ });
382
+
383
+ // Store terminal in process-scoped map
384
+ const terminalMap = getOrCreateTerminalMap(acpProc.id);
385
+ terminalMap.set(terminalId, { child, output: () => output, truncated: () => truncated });
386
+
387
+ sendResponse(acpProc, req.id, { terminalId });
388
+ } catch (err) {
389
+ sendResponse(acpProc, req.id, { error: { code: -32603, message: (err as Error).message } });
390
+ }
391
+ return;
392
+ }
393
+
394
+ // ── Terminal: output ──
395
+ case 'terminal/output': {
396
+ const terminalId = String(params.terminalId ?? '');
397
+ const terminal = getTerminal(acpProc.id, terminalId);
398
+ if (!terminal) {
399
+ sendResponse(acpProc, req.id, { error: { code: -32002, message: `Terminal not found: ${terminalId}` } });
400
+ return;
401
+ }
402
+ const exitStatus = terminal.child.exitCode !== null ? { exitCode: terminal.child.exitCode } : undefined;
403
+ sendResponse(acpProc, req.id, { output: terminal.output(), truncated: terminal.truncated(), exitStatus });
404
+ return;
405
+ }
406
+
407
+ // ── Terminal: kill ──
408
+ case 'terminal/kill': {
409
+ const terminalId = String(params.terminalId ?? '');
410
+ const terminal = getTerminal(acpProc.id, terminalId);
411
+ if (!terminal) {
412
+ sendResponse(acpProc, req.id, { error: { code: -32002, message: `Terminal not found: ${terminalId}` } });
413
+ return;
414
+ }
415
+ terminal.child.kill('SIGTERM');
416
+ sendResponse(acpProc, req.id, {});
417
+ return;
418
+ }
419
+
420
+ // ── Terminal: wait_for_exit ──
421
+ case 'terminal/wait_for_exit': {
422
+ const terminalId = String(params.terminalId ?? '');
423
+ const terminal = getTerminal(acpProc.id, terminalId);
424
+ if (!terminal) {
425
+ sendResponse(acpProc, req.id, { error: { code: -32002, message: `Terminal not found: ${terminalId}` } });
426
+ return;
427
+ }
428
+ if (terminal.child.exitCode !== null) {
429
+ sendResponse(acpProc, req.id, { exitCode: terminal.child.exitCode, signal: terminal.child.signalCode });
430
+ return;
431
+ }
432
+ terminal.child.on('exit', (code: number | null, signal: string | null) => {
433
+ sendResponse(acpProc, req.id, { exitCode: code, signal });
434
+ });
435
+ return;
436
+ }
437
+
438
+ // ── Terminal: release ──
439
+ case 'terminal/release': {
440
+ const terminalId = String(params.terminalId ?? '');
441
+ const terminal = getTerminal(acpProc.id, terminalId);
442
+ if (!terminal) {
443
+ sendResponse(acpProc, req.id, { error: { code: -32002, message: `Terminal not found: ${terminalId}` } });
444
+ return;
445
+ }
446
+ if (terminal.child.exitCode === null) terminal.child.kill('SIGTERM');
447
+ removeTerminal(acpProc.id, terminalId);
448
+ sendResponse(acpProc, req.id, {});
449
+ return;
450
+ }
451
+
452
+ // ── Permission requests (auto-approve all) ──
453
+ case 'session/request_permission': {
454
+ console.log(`[ACP] Auto-approving permission: ${JSON.stringify(params.toolCall ?? {}).slice(0, 200)}`);
455
+ sendResponse(acpProc, req.id, { outcome: { selected: { optionId: 'allow_once' } } });
456
+ return;
457
+ }
458
+
459
+ // ── Unknown methods: auto-approve for backwards compat ──
460
+ default: {
461
+ console.log(`[ACP] Auto-approving unknown agent request: ${method} (id=${req.id})`);
462
+ sendResponse(acpProc, req.id, {});
463
+ }
464
+ }
465
+ });
466
+ }
467
+
468
+ /* ── Terminal management (per ACP process) ─────────────────────────────── */
469
+
470
+ interface TerminalEntry {
471
+ child: import('child_process').ChildProcess;
472
+ output: () => string;
473
+ truncated: () => boolean;
474
+ }
195
475
 
196
- function buildCommand(entry: AcpRegistryEntry): { cmd: string; args: string[] } {
197
- const transport: AcpTransportType = entry.transport;
476
+ const terminalMaps = new Map<string, Map<string, TerminalEntry>>();
198
477
 
199
- switch (transport) {
200
- case 'npx':
201
- return { cmd: 'npx', args: [entry.command, ...(entry.args ?? [])] };
202
- case 'uvx':
203
- return { cmd: 'uvx', args: [entry.command, ...(entry.args ?? [])] };
204
- case 'binary':
205
- case 'stdio':
206
- default:
207
- return { cmd: entry.command, args: entry.args ?? [] };
478
+ function getOrCreateTerminalMap(procId: string): Map<string, TerminalEntry> {
479
+ let map = terminalMaps.get(procId);
480
+ if (!map) {
481
+ map = new Map();
482
+ terminalMaps.set(procId, map);
208
483
  }
484
+ return map;
485
+ }
486
+
487
+ function getTerminal(procId: string, terminalId: string): TerminalEntry | undefined {
488
+ return terminalMaps.get(procId)?.get(terminalId);
489
+ }
490
+
491
+ function removeTerminal(procId: string, terminalId: string): void {
492
+ terminalMaps.get(procId)?.delete(terminalId);
209
493
  }
494
+
495
+ /* ── Internal — agent command resolution moved to agent-descriptors.ts ─ */
@@ -10,14 +10,63 @@
10
10
  /** How an ACP agent is spawned */
11
11
  export type AcpTransportType = 'stdio' | 'npx' | 'uvx' | 'binary';
12
12
 
13
+ /* ── ContentBlock (ACP prompt format) ─────────────────────────────────── */
14
+
15
+ export type AcpContentBlock =
16
+ | { type: 'text'; text: string }
17
+ | { type: 'image'; data: string; mimeType: string }
18
+ | { type: 'audio'; data: string; mimeType: string }
19
+ | { type: 'resource_link'; uri: string; name: string }
20
+ | { type: 'resource'; resource: { uri: string; text?: string; blob?: string } };
21
+
22
+ /* ── StopReason ───────────────────────────────────────────────────────── */
23
+
24
+ export type AcpStopReason = 'end_turn' | 'max_tokens' | 'max_turn_requests' | 'refusal' | 'cancelled';
25
+
26
+ /* ── Modes & Config ───────────────────────────────────────────────────── */
27
+
28
+ export interface AcpMode {
29
+ id: string;
30
+ name: string;
31
+ description?: string;
32
+ }
33
+
34
+ export interface AcpConfigOptionEntry {
35
+ id: string;
36
+ label: string;
37
+ }
38
+
39
+ export interface AcpConfigOption {
40
+ type: 'select';
41
+ configId: string;
42
+ category: 'mode' | 'model' | 'thought_level' | 'other' | string;
43
+ label?: string;
44
+ currentValue: string;
45
+ options: AcpConfigOptionEntry[];
46
+ }
47
+
48
+ /* ── Auth ──────────────────────────────────────────────────────────────── */
49
+
50
+ export interface AcpAuthMethod {
51
+ id: string;
52
+ name: string;
53
+ description?: string;
54
+ }
55
+
13
56
  /* ── Capabilities ─────────────────────────────────────────────────────── */
14
57
 
15
- /** What MindOS exposes as an ACP agent */
16
- export interface AcpCapabilities {
17
- streaming: boolean;
18
- toolCalls: boolean;
19
- multiTurn: boolean;
20
- cancellation: boolean;
58
+ /** What the agent declares it supports (from initialize response). */
59
+ export interface AcpAgentCapabilities {
60
+ loadSession?: boolean;
61
+ mcpCapabilities?: { http?: boolean; sse?: boolean };
62
+ promptCapabilities?: { audio?: boolean; embeddedContext?: boolean; image?: boolean };
63
+ sessionCapabilities?: { list?: boolean };
64
+ }
65
+
66
+ /** What MindOS declares as a client (sent in initialize request). */
67
+ export interface AcpClientCapabilities {
68
+ fs?: { readTextFile?: boolean; writeTextFile?: boolean };
69
+ terminal?: boolean;
21
70
  }
22
71
 
23
72
  /* ── Session ──────────────────────────────────────────────────────────── */
@@ -28,8 +77,25 @@ export interface AcpSession {
28
77
  id: string;
29
78
  agentId: string;
30
79
  state: AcpSessionState;
80
+ cwd?: string;
31
81
  createdAt: string;
32
82
  lastActivityAt: string;
83
+ /** Agent capabilities from initialize response */
84
+ agentCapabilities?: AcpAgentCapabilities;
85
+ /** Modes available from session/new or session/load response */
86
+ modes?: AcpMode[];
87
+ /** Config options from session/new or session/load response */
88
+ configOptions?: AcpConfigOption[];
89
+ /** Auth methods from initialize response */
90
+ authMethods?: AcpAuthMethod[];
91
+ }
92
+
93
+ /** Lightweight session info returned by session/list. */
94
+ export interface AcpSessionInfo {
95
+ sessionId: string;
96
+ title?: string;
97
+ cwd?: string;
98
+ updatedAt?: string;
33
99
  }
34
100
 
35
101
  /* ── JSON-RPC (ACP uses JSON-RPC 2.0 over stdio) ─────────────────────── */
@@ -58,32 +124,27 @@ export interface AcpJsonRpcError {
58
124
 
59
125
  export interface AcpPromptRequest {
60
126
  sessionId: string;
61
- text: string;
62
- metadata?: Record<string, unknown>;
127
+ prompt: AcpContentBlock[];
128
+ context?: { cwd?: string };
129
+ stream?: boolean;
63
130
  }
64
131
 
65
132
  export interface AcpPromptResponse {
66
133
  sessionId: string;
67
134
  text: string;
68
135
  done: boolean;
136
+ stopReason?: AcpStopReason;
69
137
  toolCalls?: AcpToolCall[];
70
138
  metadata?: Record<string, unknown>;
71
139
  }
72
140
 
73
- /* ── Session Updates (streaming) ──────────────────────────────────────── */
74
-
75
- export type AcpUpdateType = 'text' | 'tool_call' | 'tool_result' | 'done' | 'error';
141
+ /* ── ToolCall (full ACP model) ────────────────────────────────────────── */
76
142
 
77
- export interface AcpSessionUpdate {
78
- sessionId: string;
79
- type: AcpUpdateType;
80
- text?: string;
81
- toolCall?: AcpToolCall;
82
- toolResult?: AcpToolResult;
83
- error?: string;
84
- }
143
+ export type AcpToolCallKind =
144
+ | 'read' | 'edit' | 'delete' | 'move' | 'search'
145
+ | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
85
146
 
86
- /* ── Tool Calls ───────────────────────────────────────────────────────── */
147
+ export type AcpToolCallStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
87
148
 
88
149
  export interface AcpToolCall {
89
150
  id: string;
@@ -91,12 +152,85 @@ export interface AcpToolCall {
91
152
  arguments: Record<string, unknown>;
92
153
  }
93
154
 
155
+ /** Full tool call with status, kind, and content — used in session updates. */
156
+ export interface AcpToolCallFull {
157
+ toolCallId: string;
158
+ title?: string;
159
+ kind?: AcpToolCallKind;
160
+ status: AcpToolCallStatus;
161
+ rawInput?: string;
162
+ rawOutput?: string;
163
+ content?: AcpContentBlock[];
164
+ locations?: { path: string; line?: number }[];
165
+ }
166
+
94
167
  export interface AcpToolResult {
95
168
  callId: string;
96
169
  result: string;
97
170
  isError?: boolean;
98
171
  }
99
172
 
173
+ /* ── Plan ──────────────────────────────────────────────────────────────── */
174
+
175
+ export type AcpPlanEntryStatus = 'pending' | 'in_progress' | 'completed';
176
+ export type AcpPlanEntryPriority = 'high' | 'medium' | 'low';
177
+
178
+ export interface AcpPlanEntry {
179
+ content: string;
180
+ status: AcpPlanEntryStatus;
181
+ priority: AcpPlanEntryPriority;
182
+ }
183
+
184
+ export interface AcpPlan {
185
+ entries: AcpPlanEntry[];
186
+ }
187
+
188
+ /* ── Session Updates (streaming) — Full ACP spec ──────────────────────── */
189
+
190
+ export type AcpUpdateType =
191
+ | 'user_message_chunk'
192
+ | 'agent_message_chunk'
193
+ | 'agent_thought_chunk'
194
+ | 'tool_call'
195
+ | 'tool_call_update'
196
+ | 'plan'
197
+ | 'available_commands_update'
198
+ | 'current_mode_update'
199
+ | 'config_option_update'
200
+ | 'session_info_update'
201
+ // Legacy compat (mapped internally)
202
+ | 'text'
203
+ | 'tool_result'
204
+ | 'done'
205
+ | 'error';
206
+
207
+ export interface AcpSessionUpdate {
208
+ sessionId: string;
209
+ type: AcpUpdateType;
210
+ /** Text content for message chunk types */
211
+ text?: string;
212
+ /** Structured tool call data */
213
+ toolCall?: AcpToolCallFull;
214
+ /** Tool result (legacy) */
215
+ toolResult?: AcpToolResult;
216
+ /** Plan entries */
217
+ plan?: AcpPlan;
218
+ /** Available commands (opaque to client) */
219
+ availableCommands?: unknown[];
220
+ /** Current mode ID */
221
+ currentModeId?: string;
222
+ /** Updated config options */
223
+ configOptions?: AcpConfigOption[];
224
+ /** Session info update */
225
+ sessionInfo?: { title?: string; updatedAt?: string };
226
+ /** Error message */
227
+ error?: string;
228
+ }
229
+
230
+ /* ── Permission ───────────────────────────────────────────────────────── */
231
+
232
+ export type AcpPermissionOutcome = 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
233
+
100
234
  /* ── Registry ─────────────────────────────────────────────────────────── */
101
235
 
102
236
  /** An entry from the ACP registry (registry.json) */
@@ -107,6 +241,8 @@ export interface AcpRegistryEntry {
107
241
  version?: string;
108
242
  transport: AcpTransportType;
109
243
  command: string;
244
+ /** npm package name for npx-based agents (e.g. "@google/gemini-cli") */
245
+ packageName?: string;
110
246
  args?: string[];
111
247
  env?: Record<string, string>;
112
248
  tags?: string[];
@@ -128,6 +264,8 @@ export const ACP_ERRORS = {
128
264
  AGENT_NOT_FOUND: { code: -32003, message: 'Agent not found in registry' },
129
265
  SPAWN_FAILED: { code: -32004, message: 'Failed to spawn agent process' },
130
266
  TRANSPORT_ERROR: { code: -32005, message: 'Transport error' },
267
+ AUTH_REQUIRED: { code: -32000, message: 'Authentication required' },
268
+ RESOURCE_NOT_FOUND: { code: -32002, message: 'Resource not found' },
131
269
  PARSE_ERROR: { code: -32700, message: 'Parse error' },
132
270
  INVALID_REQUEST: { code: -32600, message: 'Invalid request' },
133
271
  METHOD_NOT_FOUND: { code: -32601, message: 'Method not found' },
@@ -1,6 +1,19 @@
1
1
  import { getModel as piGetModel, type Model } from '@mariozechner/pi-ai';
2
2
  import { effectiveAiConfig } from '@/lib/settings';
3
3
 
4
+ /** Check if any message in the conversation contains images */
5
+ export function hasImages(messages: Array<{ images?: unknown[] }>): boolean {
6
+ return messages.some(m => m.images && m.images.length > 0);
7
+ }
8
+
9
+ /** Ensure model input includes 'image' when images are present */
10
+ function ensureVisionCapable(model: Model<any>): Model<any> {
11
+ const inputs = model.input as readonly string[];
12
+ if (inputs.includes('image')) return model;
13
+ // Upgrade input to include image — most modern models support it
14
+ return { ...model, input: [...inputs, 'image'] as any };
15
+ }
16
+
4
17
  /**
5
18
  * Build a pi-ai Model for the configured provider.
6
19
  *
@@ -11,7 +24,7 @@ import { effectiveAiConfig } from '@/lib/settings';
11
24
  *
12
25
  * Returns { model, modelName, apiKey } — Agent needs model + apiKey via getApiKey hook.
13
26
  */
14
- export function getModelConfig(): {
27
+ export function getModelConfig(options?: { hasImages?: boolean }): {
15
28
  model: Model<any>;
16
29
  modelName: string;
17
30
  apiKey: string;
@@ -77,7 +90,8 @@ export function getModelConfig(): {
77
90
  }
78
91
  }
79
92
 
80
- return { model, modelName, apiKey: cfg.openaiApiKey, provider: 'openai' };
93
+ const finalModel = options?.hasImages ? ensureVisionCapable(model) : model;
94
+ return { model: finalModel, modelName, apiKey: cfg.openaiApiKey, provider: 'openai' };
81
95
  }
82
96
 
83
97
  // Anthropic
@@ -104,5 +118,6 @@ export function getModelConfig(): {
104
118
  };
105
119
  }
106
120
 
107
- return { model, modelName, apiKey: cfg.anthropicApiKey, provider: 'anthropic' };
121
+ const finalModel = options?.hasImages ? ensureVisionCapable(model) : model;
122
+ return { model: finalModel, modelName, apiKey: cfg.anthropicApiKey, provider: 'anthropic' };
108
123
  }