@axhub/genie 0.2.5 → 0.2.7

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 (139) hide show
  1. package/dist/assets/App-BWSqiXAT.js +220 -0
  2. package/dist/assets/App-DrlLKa8f.css +1 -0
  3. package/dist/assets/ReviewApp-nz3mbArg.js +1 -0
  4. package/dist/assets/{_basePickBy-CFRQvihx.js → _basePickBy-C19AekOu.js} +1 -1
  5. package/dist/assets/{_baseUniq-Dhh8nCvs.js → _baseUniq-JsnevLw_.js} +1 -1
  6. package/dist/assets/{arc-DQ0v3dU4.js → arc-BLpcuBlf.js} +1 -1
  7. package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +36 -0
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-Bbxhj5KC.js → blockDiagram-WCTKOSBZ-DQBLwsUS.js} +3 -3
  9. package/dist/assets/c4Diagram-IC4MRINW-CGobwBIj.js +10 -0
  10. package/dist/assets/channel-DkFNxV_H.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB-DlvtrM0q.js → chunk-4BX2VUAB-De63kbgc.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-DJUSHyTa.js → chunk-55IACEB6-DtTDDdM9.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-C6Ch-htf.js → chunk-FMBD7UC4-DHuwd8tw.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-DzQIht58.js → chunk-JSJVCQXG-BgytFtmO.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-C05jARMH.js → chunk-KX2RTZJC-nZdp86aN.js} +1 -1
  16. package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +220 -0
  17. package/dist/assets/{chunk-QZHKN3VN-jxti9HTX.js → chunk-QZHKN3VN-DvUQ3mnO.js} +1 -1
  18. package/dist/assets/chunk-WL4C6EOR-Dn7db_6t.js +189 -0
  19. package/dist/assets/classDiagram-VBA2DB6C-DtwCEe8S.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-DtwCEe8S.js +1 -0
  21. package/dist/assets/clone-C0lCEIEO.js +1 -0
  22. package/dist/assets/cose-bilkent-S5V4N54A-DD_nzqsz.js +1 -0
  23. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  24. package/dist/assets/{dagre-KLK3FWXG-DJ3dNSYk.js → dagre-KLK3FWXG-CHYIvW47.js} +1 -1
  25. package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +24 -0
  26. package/dist/assets/{diagram-IFDJBPK2-Da6K4aP-.js → diagram-IFDJBPK2-Dzsiln_C.js} +1 -1
  27. package/dist/assets/{diagram-P4PSJMXO-vZZKB92A.js → diagram-P4PSJMXO-DKnGbUpE.js} +1 -1
  28. package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +70 -0
  29. package/dist/assets/{flowDiagram-PKNHOUZH-DUV13pHi.js → flowDiagram-PKNHOUZH-BAZ2-jKp.js} +4 -4
  30. package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +292 -0
  31. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BZ5gW69I.js → gitGraphDiagram-K3NZZRJ6-BflpyjGy.js} +1 -1
  32. package/dist/assets/{graph-BbvHswRd.js → graph-suelaXFh.js} +1 -1
  33. package/dist/assets/highlighted-body-OFNGDK62-CZrBMazC.js +1 -0
  34. package/dist/assets/index-B01NxbUv.css +1 -0
  35. package/dist/assets/index-DW5pGgQ_.js +2 -0
  36. package/dist/assets/{infoDiagram-LFFYTUFH-8auUIPKW.js → infoDiagram-LFFYTUFH-pfD1FA3p.js} +1 -1
  37. package/dist/assets/ishikawaDiagram-PHBUUO56-ndm9snwO.js +70 -0
  38. package/dist/assets/journeyDiagram-4ABVD52K-HgF2t7z5.js +139 -0
  39. package/dist/assets/{kanban-definition-K7BYSVSG-Bappd2YO.js → kanban-definition-K7BYSVSG-FWinmur1.js} +5 -5
  40. package/dist/assets/{layout-BmbfFZKy.js → layout-vcz43XvZ.js} +1 -1
  41. package/dist/assets/{linear-WZnF-PT6.js → linear-le4gc0vx.js} +1 -1
  42. package/dist/assets/mermaid-GHXKKRXX-CK8m3lad.js +870 -0
  43. package/dist/assets/mindmap-definition-YRQLILUH-CNq9SKj4.js +68 -0
  44. package/dist/assets/{pieDiagram-SKSYHLDU-uxjlAy1t.js → pieDiagram-SKSYHLDU-C7PKDh3b.js} +2 -2
  45. package/dist/assets/quadrantDiagram-337W2JSQ-B7FnztNO.js +7 -0
  46. package/dist/assets/requirementDiagram-Z7DCOOCP-Bl_BM2Th.js +73 -0
  47. package/dist/assets/{sankeyDiagram-WA2Y5GQK-2-FHHM-R.js → sankeyDiagram-WA2Y5GQK-4gulcOP4.js} +3 -3
  48. package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +145 -0
  49. package/dist/assets/{stateDiagram-RAJIS63D-DoW8U53H.js → stateDiagram-RAJIS63D-CB4Vl7qM.js} +1 -1
  50. package/dist/assets/stateDiagram-v2-FVOUBMTO-C85ucl39.js +1 -0
  51. package/dist/assets/timeline-definition-YZTLITO2-BPGKhi7f.js +61 -0
  52. package/dist/assets/{treemap-KZPCXAKY-ajdAP-72.js → treemap-KZPCXAKY-DZSEE6Hz.js} +58 -58
  53. package/dist/assets/vendor-codemirror-CyOKkaQZ.js +31 -0
  54. package/dist/assets/vendor-react-CP4yFTs7.js +8 -0
  55. package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
  56. package/dist/assets/{vennDiagram-LZ73GAT5-C9If0AT0.js → vennDiagram-LZ73GAT5-8E_G06fI.js} +4 -4
  57. package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +7 -0
  58. package/dist/favicon.png +0 -0
  59. package/dist/icons/icon-128x128.png +0 -0
  60. package/dist/icons/icon-144x144.png +0 -0
  61. package/dist/icons/icon-152x152.png +0 -0
  62. package/dist/icons/icon-192x192.png +0 -0
  63. package/dist/icons/icon-384x384.png +0 -0
  64. package/dist/icons/icon-512x512.png +0 -0
  65. package/dist/icons/icon-72x72.png +0 -0
  66. package/dist/icons/icon-96x96.png +0 -0
  67. package/dist/index.html +4 -5
  68. package/dist/logo-128.png +0 -0
  69. package/dist/logo-256.png +0 -0
  70. package/dist/logo-32.png +0 -0
  71. package/dist/logo-512.png +0 -0
  72. package/dist/logo-64.png +0 -0
  73. package/package.json +2 -1
  74. package/server/_legacy-providers/README.md +30 -0
  75. package/server/_legacy-providers/claude-sdk.js +956 -0
  76. package/server/_legacy-providers/gemini-cli.js +368 -0
  77. package/server/_legacy-providers/openai-codex.js +705 -0
  78. package/server/_legacy-providers/opencode-cli.js +674 -0
  79. package/server/acp-runtime/client.js +1805 -0
  80. package/server/acp-runtime/client.test.js +688 -0
  81. package/server/acp-runtime/index.js +419 -0
  82. package/server/acp-runtime/registry.js +45 -0
  83. package/server/acp-runtime/session-store.js +254 -0
  84. package/server/acp-runtime/session-store.test.js +89 -0
  85. package/server/channels/runtime/AgentRuntimeAdapter.js +21 -70
  86. package/server/claude-sdk.js +24 -944
  87. package/server/cli.js +11 -5
  88. package/server/external-agent/service.js +77 -63
  89. package/server/gemini-cli.js +23 -360
  90. package/server/index.js +54 -46
  91. package/server/openai-codex.js +24 -698
  92. package/server/opencode-cli.js +70 -640
  93. package/server/routes/agent.js +2 -0
  94. package/server/routes/codex.js +5 -5
  95. package/server/routes/git.js +3 -20
  96. package/server/routes/mcp.js +18 -34
  97. package/server/routes/session-core.js +44 -10
  98. package/server/session-core/abortSession.js +2 -18
  99. package/server/session-core/eventStore.js +5 -1
  100. package/server/session-core/providerAdapters.js +98 -10
  101. package/server/session-core/providerDiscovery.js +2 -2
  102. package/server/session-core/runtimeState.js +16 -17
  103. package/server/session-core/runtimeWriter.js +19 -12
  104. package/server/utils/codexPath.js +3 -1
  105. package/server/utils/spawnCommand.js +7 -0
  106. package/shared/conversationEvents.js +347 -10
  107. package/shared/conversationEvents.test.js +403 -0
  108. package/dist/assets/App-BxazfNJn.js +0 -484
  109. package/dist/assets/App-qxJ8_QYu.css +0 -32
  110. package/dist/assets/ReviewApp-CsqTAlGU.js +0 -1
  111. package/dist/assets/architectureDiagram-2XIMDMQ5-DmUHdvQH.js +0 -36
  112. package/dist/assets/c4Diagram-IC4MRINW-BOivDlQU.js +0 -10
  113. package/dist/assets/channel-Cj8xVD0X.js +0 -1
  114. package/dist/assets/chunk-NQ4KR5QH-Ci-n7jfu.js +0 -220
  115. package/dist/assets/chunk-WL4C6EOR-C559Mk71.js +0 -189
  116. package/dist/assets/classDiagram-VBA2DB6C-CI2zklxw.js +0 -1
  117. package/dist/assets/classDiagram-v2-RAHNMMFH-CI2zklxw.js +0 -1
  118. package/dist/assets/clone-BEVqubrI.js +0 -1
  119. package/dist/assets/cose-bilkent-S5V4N54A-DNO9ncXL.js +0 -1
  120. package/dist/assets/cytoscape.esm-2ZfV8NB5.js +0 -331
  121. package/dist/assets/diagram-E7M64L7V-Ba_LGLun.js +0 -24
  122. package/dist/assets/erDiagram-INFDFZHY-Csb8dFdP.js +0 -70
  123. package/dist/assets/ganttDiagram-A5KZAMGK-B5Kv9Wfz.js +0 -292
  124. package/dist/assets/highlighted-body-TPN3WLV5-DZJajMGm.js +0 -1
  125. package/dist/assets/index-BFX9lxRB.css +0 -1
  126. package/dist/assets/index-BiErUGrv.js +0 -2
  127. package/dist/assets/ishikawaDiagram-PHBUUO56-JmsNlo2I.js +0 -70
  128. package/dist/assets/journeyDiagram-4ABVD52K-Cuudv7Vv.js +0 -139
  129. package/dist/assets/mermaid-O7DHMXV3-D-2fQRvw.js +0 -988
  130. package/dist/assets/mindmap-definition-YRQLILUH-BQHnzzud.js +0 -68
  131. package/dist/assets/quadrantDiagram-337W2JSQ-DpwZU-f_.js +0 -7
  132. package/dist/assets/requirementDiagram-Z7DCOOCP-C_9ClOWm.js +0 -73
  133. package/dist/assets/sequenceDiagram-2WXFIKYE-egns-0XI.js +0 -145
  134. package/dist/assets/stateDiagram-v2-FVOUBMTO-BoFZZ4Ds.js +0 -1
  135. package/dist/assets/timeline-definition-YZTLITO2-chPa8ppH.js +0 -61
  136. package/dist/assets/vendor-codemirror-Dz7_EqNA.js +0 -39
  137. package/dist/assets/vendor-react-Cpt6D04s.js +0 -59
  138. package/dist/assets/vendor-xterm-DfaPXD3y.js +0 -66
  139. package/dist/assets/xychartDiagram-JWTSCODW-DD42U6Or.js +0 -7
@@ -97,6 +97,7 @@ router.post('/abort', validateExternalApiKey, async (req, res) => {
97
97
  return res.json({
98
98
  success: false,
99
99
  aborted: false,
100
+ runtime: 'acp',
100
101
  sessionId: normalized.sessionId,
101
102
  provider: normalized.provider,
102
103
  error: 'Active session not found'
@@ -106,6 +107,7 @@ router.post('/abort', validateExternalApiKey, async (req, res) => {
106
107
  return res.json({
107
108
  success: true,
108
109
  aborted: true,
110
+ runtime: 'acp',
109
111
  sessionId: normalized.sessionId,
110
112
  provider: normalized.provider,
111
113
  message: 'Session aborted'
@@ -1,10 +1,10 @@
1
1
  import express from 'express';
2
- import { spawn } from 'child_process';
3
2
  import { promises as fs } from 'fs';
4
3
  import path from 'path';
5
4
  import os from 'os';
6
5
  import TOML from '@iarna/toml';
7
6
  import { getCodexSessions, getCodexSessionMessages, deleteCodexSession } from '../projects.js';
7
+ import { spawnCommand } from '../utils/spawnCommand.js';
8
8
 
9
9
  const router = express.Router();
10
10
 
@@ -100,7 +100,7 @@ router.delete('/sessions/:sessionId', async (req, res) => {
100
100
  router.get('/mcp/cli/list', async (req, res) => {
101
101
  try {
102
102
  const respond = createCliResponder(res);
103
- const proc = spawn('codex', ['mcp', 'list'], { stdio: ['pipe', 'pipe', 'pipe'] });
103
+ const proc = spawnCommand('codex', ['mcp', 'list'], { stdio: ['pipe', 'pipe', 'pipe'] });
104
104
 
105
105
  let stdout = '';
106
106
  let stderr = '';
@@ -151,7 +151,7 @@ router.post('/mcp/cli/add', async (req, res) => {
151
151
  }
152
152
 
153
153
  const respond = createCliResponder(res);
154
- const proc = spawn('codex', cliArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
154
+ const proc = spawnCommand('codex', cliArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
155
155
 
156
156
  let stdout = '';
157
157
  let stderr = '';
@@ -185,7 +185,7 @@ router.delete('/mcp/cli/remove/:name', async (req, res) => {
185
185
  const { name } = req.params;
186
186
 
187
187
  const respond = createCliResponder(res);
188
- const proc = spawn('codex', ['mcp', 'remove', name], { stdio: ['pipe', 'pipe', 'pipe'] });
188
+ const proc = spawnCommand('codex', ['mcp', 'remove', name], { stdio: ['pipe', 'pipe', 'pipe'] });
189
189
 
190
190
  let stdout = '';
191
191
  let stderr = '';
@@ -219,7 +219,7 @@ router.get('/mcp/cli/get/:name', async (req, res) => {
219
219
  const { name } = req.params;
220
220
 
221
221
  const respond = createCliResponder(res);
222
- const proc = spawn('codex', ['mcp', 'get', name], { stdio: ['pipe', 'pipe', 'pipe'] });
222
+ const proc = spawnCommand('codex', ['mcp', 'get', name], { stdio: ['pipe', 'pipe', 'pipe'] });
223
223
 
224
224
  let stdout = '';
225
225
  let stderr = '';
@@ -610,26 +610,9 @@ Generate the commit message:`;
610
610
  const parsed = typeof data === 'string' ? JSON.parse(data) : data;
611
611
  console.log('🔍 Writer received message type:', parsed.type);
612
612
 
613
- // Handle different message formats from supported providers
614
- // Claude SDK sends: {type: 'claude-response', data: {message: {content: [...]}}}
615
- if (parsed.type === 'claude-response' && parsed.data) {
616
- const message = parsed.data.message || parsed.data;
617
- console.log('📦 Claude response message:', JSON.stringify(message, null, 2).substring(0, 500));
618
- if (message.content && Array.isArray(message.content)) {
619
- // Extract text from content array
620
- for (const item of message.content) {
621
- if (item.type === 'text' && item.text) {
622
- console.log('✅ Extracted text chunk:', item.text.substring(0, 100));
623
- responseText += item.text;
624
- }
625
- }
626
- }
627
- }
628
- else if (parsed.type === 'claude-response' && parsed.data?.type === 'content_block_delta' && parsed.data?.delta?.text) {
629
- responseText += parsed.data.delta.text;
630
- }
631
- // Also handle direct text messages
632
- else if (parsed.type === 'text' && parsed.text) {
613
+ if (parsed.type === 'conversation-event' && parsed.event?.kind === 'assistant_text_delta' && parsed.event?.payload?.text) {
614
+ responseText += parsed.event.payload.text;
615
+ } else if (parsed.type === 'text' && parsed.text) {
633
616
  console.log('✅ Direct text:', parsed.text.substring(0, 100));
634
617
  responseText += parsed.text;
635
618
  }
@@ -2,13 +2,9 @@ import express from 'express';
2
2
  import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
- import { fileURLToPath } from 'url';
6
- import { dirname } from 'path';
7
- import { spawn } from 'child_process';
5
+ import { spawnCommand } from '../utils/spawnCommand.js';
8
6
 
9
7
  const router = express.Router();
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = dirname(__filename);
12
8
 
13
9
  // Claude CLI command routes
14
10
 
@@ -16,12 +12,8 @@ const __dirname = dirname(__filename);
16
12
  router.get('/cli/list', async (req, res) => {
17
13
  try {
18
14
  console.log('📋 Listing MCP servers using Claude CLI');
19
-
20
- const { spawn } = await import('child_process');
21
- const { promisify } = await import('util');
22
- const exec = promisify(spawn);
23
-
24
- const process = spawn('claude', ['mcp', 'list'], {
15
+
16
+ const process = spawnCommand('claude', ['mcp', 'list'], {
25
17
  stdio: ['pipe', 'pipe', 'pipe']
26
18
  });
27
19
 
@@ -59,11 +51,9 @@ router.get('/cli/list', async (req, res) => {
59
51
  router.post('/cli/add', async (req, res) => {
60
52
  try {
61
53
  const { name, type = 'stdio', command, args = [], url, headers = {}, env = {}, scope = 'user', projectPath } = req.body;
62
-
54
+
63
55
  console.log(`➕ Adding MCP server using Claude CLI (${scope} scope):`, name);
64
-
65
- const { spawn } = await import('child_process');
66
-
56
+
67
57
  let cliArgs = ['mcp', 'add'];
68
58
 
69
59
  // Add scope flag
@@ -105,8 +95,8 @@ router.post('/cli/add', async (req, res) => {
105
95
  spawnOptions.cwd = projectPath;
106
96
  console.log('📁 Running in project directory:', projectPath);
107
97
  }
108
-
109
- const process = spawn('claude', cliArgs, spawnOptions);
98
+
99
+ const process = spawnCommand('claude', cliArgs, spawnOptions);
110
100
 
111
101
  let stdout = '';
112
102
  let stderr = '';
@@ -142,9 +132,9 @@ router.post('/cli/add', async (req, res) => {
142
132
  router.post('/cli/add-json', async (req, res) => {
143
133
  try {
144
134
  const { name, jsonConfig, scope = 'user', projectPath } = req.body;
145
-
135
+
146
136
  console.log('➕ Adding MCP server using JSON format:', name);
147
-
137
+
148
138
  // Validate and parse JSON config
149
139
  let parsedConfig;
150
140
  try {
@@ -178,8 +168,6 @@ router.post('/cli/add-json', async (req, res) => {
178
168
  });
179
169
  }
180
170
 
181
- const { spawn } = await import('child_process');
182
-
183
171
  // Build the command: claude mcp add-json --scope <scope> <name> '<json>'
184
172
  const cliArgs = ['mcp', 'add-json', '--scope', scope, name];
185
173
 
@@ -198,8 +186,8 @@ router.post('/cli/add-json', async (req, res) => {
198
186
  spawnOptions.cwd = projectPath;
199
187
  console.log('📁 Running in project directory:', projectPath);
200
188
  }
201
-
202
- const process = spawn('claude', cliArgs, spawnOptions);
189
+
190
+ const process = spawnCommand('claude', cliArgs, spawnOptions);
203
191
 
204
192
  let stdout = '';
205
193
  let stderr = '';
@@ -247,11 +235,9 @@ router.delete('/cli/remove/:name', async (req, res) => {
247
235
  actualName = serverName;
248
236
  actualScope = actualScope || prefix; // Use prefix as scope if not provided in query
249
237
  }
250
-
238
+
251
239
  console.log('🗑️ Removing MCP server using Claude CLI:', actualName, 'scope:', actualScope);
252
-
253
- const { spawn } = await import('child_process');
254
-
240
+
255
241
  // Build command args based on scope
256
242
  let cliArgs = ['mcp', 'remove'];
257
243
 
@@ -267,7 +253,7 @@ router.delete('/cli/remove/:name', async (req, res) => {
267
253
 
268
254
  console.log('🔧 Running Claude CLI command:', 'claude', cliArgs.join(' '));
269
255
 
270
- const process = spawn('claude', cliArgs, {
256
+ const process = spawnCommand('claude', cliArgs, {
271
257
  stdio: ['pipe', 'pipe', 'pipe']
272
258
  });
273
259
 
@@ -305,12 +291,10 @@ router.delete('/cli/remove/:name', async (req, res) => {
305
291
  router.get('/cli/get/:name', async (req, res) => {
306
292
  try {
307
293
  const { name } = req.params;
308
-
294
+
309
295
  console.log('📄 Getting MCP server details using Claude CLI:', name);
310
-
311
- const { spawn } = await import('child_process');
312
-
313
- const process = spawn('claude', ['mcp', 'get', name], {
296
+
297
+ const process = spawnCommand('claude', ['mcp', 'get', name], {
314
298
  stdio: ['pipe', 'pipe', 'pipe']
315
299
  });
316
300
 
@@ -549,4 +533,4 @@ function parseClaudeGetOutput(output) {
549
533
  }
550
534
  }
551
535
 
552
- export default router;
536
+ export default router;
@@ -2,10 +2,17 @@ import express from 'express';
2
2
  import { getProjects } from '../projects.js';
3
3
  import { discoverAllProviders, discoverProvider } from '../session-core/providerDiscovery.js';
4
4
  import { getProviderAdapter } from '../session-core/providerAdapters.js';
5
+ import { listAcpSessions } from '../acp-runtime/session-store.js';
5
6
 
6
7
  const router = express.Router();
7
8
 
8
- function flattenProjectSessions(project) {
9
+ router.use((req, res, next) => {
10
+ res.setHeader('X-Runtime-Engine', 'acp');
11
+ next();
12
+ });
13
+
14
+ async function flattenProjectSessions(project) {
15
+ const projectPath = project.fullPath || project.path || null;
9
16
  const groups = [
10
17
  { provider: 'claude', items: project.sessions || [] },
11
18
  { provider: 'codex', items: project.codexSessions || [] },
@@ -13,7 +20,12 @@ function flattenProjectSessions(project) {
13
20
  { provider: 'opencode', items: project.opencodeSessions || [] }
14
21
  ];
15
22
 
16
- return groups.flatMap(({ provider, items }) => items.map((item) => ({ ...item, provider, __provider: provider })))
23
+ const acpSessions = await listAcpSessions({ projectPath });
24
+
25
+ return [
26
+ ...groups.flatMap(({ provider, items }) => items.map((item) => ({ ...item, provider, __provider: provider, source: item?.source || 'legacy' }))),
27
+ ...acpSessions.map((item) => ({ ...item, provider: item.provider, __provider: item.provider, source: 'acp' }))
28
+ ]
17
29
  .sort((a, b) => new Date(b.lastActivity || b.updated_at || b.createdAt || 0) - new Date(a.lastActivity || a.updated_at || a.createdAt || 0));
18
30
  }
19
31
 
@@ -42,7 +54,15 @@ router.get('/projects/:projectName/history-index', async (req, res) => {
42
54
  if (!project) {
43
55
  return res.status(404).json({ success: false, error: 'Project not found' });
44
56
  }
45
- res.json({ success: true, project: { name: project.name, fullPath: project.fullPath || project.path, displayName: project.displayName || project.name }, sessions: flattenProjectSessions(project) });
57
+ res.json({
58
+ success: true,
59
+ project: {
60
+ name: project.name,
61
+ fullPath: project.fullPath || project.path,
62
+ displayName: project.displayName || project.name
63
+ },
64
+ sessions: await flattenProjectSessions(project)
65
+ });
46
66
  } catch (error) {
47
67
  res.status(500).json({ success: false, error: error.message });
48
68
  }
@@ -66,19 +86,26 @@ router.get('/sessions/:sessionId/resolve', async (req, res) => {
66
86
  'opencode'
67
87
  ].filter((provider, index, all) => provider && all.indexOf(provider) === index);
68
88
 
69
- const directProjectMatch = projects.find((project) => {
70
- const flattened = flattenProjectSessions(project);
71
- return flattened.some((session) => session.id === requestedSessionId);
72
- });
89
+ let directProjectMatch = null;
90
+ let directMatchSessions = [];
91
+
92
+ for (const project of projects) {
93
+ const flattened = await flattenProjectSessions(project);
94
+ if (flattened.some((session) => session.id === requestedSessionId)) {
95
+ directProjectMatch = project;
96
+ directMatchSessions = flattened;
97
+ break;
98
+ }
99
+ }
73
100
 
74
101
  if (directProjectMatch) {
75
- const flattened = flattenProjectSessions(directProjectMatch);
76
- const matchedSession = flattened.find((session) => session.id === requestedSessionId);
102
+ const matchedSession = directMatchSessions.find((session) => session.id === requestedSessionId);
77
103
 
78
104
  return res.json({
79
105
  success: true,
80
106
  found: true,
81
107
  provider: matchedSession.provider,
108
+ source: matchedSession.source || 'legacy',
82
109
  session: matchedSession,
83
110
  project: {
84
111
  name: directProjectMatch.name,
@@ -97,7 +124,12 @@ router.get('/sessions/:sessionId/resolve', async (req, res) => {
97
124
 
98
125
  try {
99
126
  if (provider === 'claude') {
100
- const result = await adapter.listSessions({ projectName: project.name, limit: 1000, offset: 0 });
127
+ const result = await adapter.listSessions({
128
+ projectName: project.name,
129
+ projectPath: project.fullPath || project.path,
130
+ limit: 1000,
131
+ offset: 0
132
+ });
101
133
  sessions = Array.isArray(result) ? result : [];
102
134
  } else {
103
135
  sessions = await adapter.listSessions({ projectPath: project.fullPath || project.path, limit: 0 });
@@ -115,6 +147,7 @@ router.get('/sessions/:sessionId/resolve', async (req, res) => {
115
147
  success: true,
116
148
  found: true,
117
149
  provider,
150
+ source: matchedSession.source || 'legacy',
118
151
  session: matchedSession,
119
152
  project: {
120
153
  name: project.name,
@@ -157,6 +190,7 @@ router.get('/sessions/:provider/:sessionId/events', async (req, res) => {
157
190
  success: true,
158
191
  provider: req.params.provider,
159
192
  sessionId: req.params.sessionId,
193
+ source: result?.source || 'legacy',
160
194
  events,
161
195
  total,
162
196
  hasMore,
@@ -1,7 +1,4 @@
1
- import { abortClaudeSDKSession } from '../claude-sdk.js';
2
- import { abortCodexSession } from '../openai-codex.js';
3
- import { abortGeminiSession } from '../gemini-cli.js';
4
- import { abortOpencodeSession } from '../opencode-cli.js';
1
+ import { abortAgentSession as abortAcpAgentSession } from '../acp-runtime/index.js';
5
2
 
6
3
  export const ABORTABLE_AGENT_PROVIDERS = ['claude', 'codex', 'gemini', 'opencode'];
7
4
 
@@ -11,20 +8,7 @@ export function isAbortableAgentProvider(provider) {
11
8
 
12
9
  export async function abortAgentSession(provider, sessionId) {
13
10
  const normalizedProvider = String(provider || 'claude').trim().toLowerCase();
14
-
15
- if (normalizedProvider === 'codex') {
16
- return abortCodexSession(sessionId);
17
- }
18
-
19
- if (normalizedProvider === 'gemini') {
20
- return abortGeminiSession(sessionId);
21
- }
22
-
23
- if (normalizedProvider === 'opencode') {
24
- return abortOpencodeSession(sessionId);
25
- }
26
-
27
- return abortClaudeSDKSession(sessionId);
11
+ return abortAcpAgentSession(normalizedProvider, sessionId);
28
12
  }
29
13
 
30
14
  export async function abortAgentSessionWithWriter({ provider = 'claude', sessionId, writer }) {
@@ -24,7 +24,11 @@ function normalizePersistedEvents(events = []) {
24
24
  return events.filter((event) => (
25
25
  isConversationEvent(event) &&
26
26
  event.sessionId &&
27
- PERSISTED_EVENT_KINDS.has(event.kind)
27
+ (
28
+ event.extensions?.runtimeSource === 'acp' ||
29
+ event.rawRef?.runtime === 'acp' ||
30
+ PERSISTED_EVENT_KINDS.has(event.kind)
31
+ )
28
32
  ));
29
33
  }
30
34
 
@@ -10,6 +10,10 @@ import {
10
10
  getOpencodeSessions,
11
11
  getGeminiSessions
12
12
  } from '../projects.js';
13
+ import {
14
+ findAcpSessionRecord,
15
+ listAcpSessions
16
+ } from '../acp-runtime/session-store.js';
13
17
 
14
18
  async function flattenLegacyMessages(result) {
15
19
  if (Array.isArray(result)) return result;
@@ -28,47 +32,131 @@ async function normalizeLegacyLoadResult(result, provider, sessionId) {
28
32
 
29
33
  return {
30
34
  ...result,
31
- events
35
+ events,
36
+ source: 'legacy'
37
+ };
38
+ }
39
+
40
+ function mergeSessionLists(legacySessions = [], acpSessions = []) {
41
+ const merged = new Map();
42
+
43
+ legacySessions.forEach((session) => {
44
+ if (session?.id) {
45
+ merged.set(session.id, session);
46
+ }
47
+ });
48
+
49
+ acpSessions.forEach((session) => {
50
+ if (session?.id) {
51
+ merged.set(session.id, session);
52
+ }
53
+ });
54
+
55
+ return Array.from(merged.values()).sort((left, right) => {
56
+ const leftTime = new Date(left?.lastActivity || left?.updatedAt || left?.createdAt || 0).getTime();
57
+ const rightTime = new Date(right?.lastActivity || right?.updatedAt || right?.createdAt || 0).getTime();
58
+ return rightTime - leftTime;
59
+ });
60
+ }
61
+
62
+ async function loadAcpEvents(provider, sessionId) {
63
+ const record = await findAcpSessionRecord(sessionId, provider);
64
+ if (!record) {
65
+ return null;
66
+ }
67
+
68
+ const events = await readMirroredConversationEvents(provider, sessionId);
69
+ return {
70
+ events,
71
+ total: events.length,
72
+ hasMore: false,
73
+ offset: 0,
74
+ limit: null,
75
+ source: 'acp'
32
76
  };
33
77
  }
34
78
 
35
79
  const PROVIDER_ADAPTERS = {
36
80
  claude: {
37
- async listSessions({ projectName, limit = 50, offset = 0 }) {
38
- const result = await getSessions(projectName, limit, offset);
39
- return (result?.sessions || []).map((session) => ({ ...session, provider: 'claude' }));
81
+ async listSessions({ projectName, projectPath, limit = 50, offset = 0 }) {
82
+ const [result, acpSessions] = await Promise.all([
83
+ getSessions(projectName, limit, offset),
84
+ listAcpSessions({ provider: 'claude', projectPath: projectPath || null })
85
+ ]);
86
+ return mergeSessionLists(
87
+ (result?.sessions || []).map((session) => ({ ...session, provider: 'claude', source: 'legacy' })),
88
+ acpSessions
89
+ );
40
90
  },
41
91
  async loadEvents({ projectName, sessionId, limit = null, offset = 0 }) {
92
+ const acpResult = await loadAcpEvents('claude', sessionId);
93
+ if (acpResult) {
94
+ return acpResult;
95
+ }
96
+
42
97
  const rawMessages = await getSessionMessages(projectName, sessionId, limit, offset);
43
98
  return normalizeLegacyLoadResult(rawMessages, 'claude', sessionId);
44
99
  }
45
100
  },
46
101
  codex: {
47
102
  async listSessions({ projectPath, limit = 50 }) {
48
- const sessions = await getCodexSessions(projectPath, { limit });
49
- return sessions.map((session) => ({ ...session, provider: 'codex' }));
103
+ const [sessions, acpSessions] = await Promise.all([
104
+ getCodexSessions(projectPath, { limit }),
105
+ listAcpSessions({ provider: 'codex', projectPath: projectPath || null })
106
+ ]);
107
+ return mergeSessionLists(
108
+ sessions.map((session) => ({ ...session, provider: 'codex', source: 'legacy' })),
109
+ acpSessions
110
+ );
50
111
  },
51
112
  async loadEvents({ sessionId, limit = null, offset = 0 }) {
113
+ const acpResult = await loadAcpEvents('codex', sessionId);
114
+ if (acpResult) {
115
+ return acpResult;
116
+ }
117
+
52
118
  const rawMessages = await getCodexSessionMessages(sessionId, limit, offset);
53
119
  return normalizeLegacyLoadResult(rawMessages, 'codex', sessionId);
54
120
  }
55
121
  },
56
122
  gemini: {
57
123
  async listSessions({ projectPath, limit = 50 }) {
58
- const sessions = await getGeminiSessions(projectPath, { limit });
59
- return sessions.map((session) => ({ ...session, provider: 'gemini' }));
124
+ const [sessions, acpSessions] = await Promise.all([
125
+ getGeminiSessions(projectPath, { limit }),
126
+ listAcpSessions({ provider: 'gemini', projectPath: projectPath || null })
127
+ ]);
128
+ return mergeSessionLists(
129
+ sessions.map((session) => ({ ...session, provider: 'gemini', source: 'legacy' })),
130
+ acpSessions
131
+ );
60
132
  },
61
133
  async loadEvents({ sessionId, limit = null, offset = 0 }) {
134
+ const acpResult = await loadAcpEvents('gemini', sessionId);
135
+ if (acpResult) {
136
+ return acpResult;
137
+ }
138
+
62
139
  const rawMessages = await getGeminiSessionMessages(sessionId, limit, offset);
63
140
  return normalizeLegacyLoadResult(rawMessages, 'gemini', sessionId);
64
141
  }
65
142
  },
66
143
  opencode: {
67
144
  async listSessions({ projectPath, limit = 50 }) {
68
- const sessions = await getOpencodeSessions(projectPath, { limit });
69
- return sessions.map((session) => ({ ...session, provider: 'opencode' }));
145
+ const [sessions, acpSessions] = await Promise.all([
146
+ getOpencodeSessions(projectPath, { limit }),
147
+ listAcpSessions({ provider: 'opencode', projectPath: projectPath || null })
148
+ ]);
149
+ return mergeSessionLists(
150
+ sessions.map((session) => ({ ...session, provider: 'opencode', source: 'legacy' })),
151
+ acpSessions
152
+ );
70
153
  },
71
154
  async loadEvents({ sessionId, limit = null, offset = 0 }) {
155
+ const acpResult = await loadAcpEvents('opencode', sessionId);
156
+ if (acpResult) {
157
+ return acpResult;
158
+ }
159
+
72
160
  const rawMessages = await getOpencodeSessionMessages(sessionId, limit, offset);
73
161
  return normalizeLegacyLoadResult(rawMessages, 'opencode', sessionId);
74
162
  }
@@ -1,4 +1,3 @@
1
- import { spawn } from 'child_process';
2
1
  import { promises as fs } from 'fs';
3
2
  import path from 'path';
4
3
  import os from 'os';
@@ -10,6 +9,7 @@ import {
10
9
  checkOpencodeCredentials
11
10
  } from '../routes/cli-auth.js';
12
11
  import { listOpencodeModels } from '../opencode-cli.js';
12
+ import { spawnCommand } from '../utils/spawnCommand.js';
13
13
  import {
14
14
  CLAUDE_MODELS,
15
15
  CODEX_MODELS, GEMINI_MODELS,
@@ -36,7 +36,7 @@ function runCommand(command, args = [], options = {}) {
36
36
  };
37
37
 
38
38
  try {
39
- child = spawn(command, args, {
39
+ child = spawnCommand(command, args, {
40
40
  cwd: options.cwd || process.cwd(),
41
41
  stdio: ['ignore', 'pipe', 'pipe'],
42
42
  env: { ...process.env, ...(options.env || {}) }
@@ -7,10 +7,8 @@ import {
7
7
  } from './eventStore.js';
8
8
  import { getProviderAdapter } from './providerAdapters.js';
9
9
  import { getProjects } from '../projects.js';
10
- import { isClaudeSDKSessionActive } from '../claude-sdk.js';
11
- import { isCodexSessionActive } from '../openai-codex.js';
12
- import { isGeminiSessionActive } from '../gemini-cli.js';
13
- import { isOpencodeSessionActive } from '../opencode-cli.js';
10
+ import { findAcpSessionRecord } from '../acp-runtime/session-store.js';
11
+ import { isAgentSessionActive } from '../acp-runtime/index.js';
14
12
 
15
13
  export const AGENT_RUNTIME_PHASES = {
16
14
  IDLE: 'idle',
@@ -86,6 +84,18 @@ async function resolveSessionProjectContext(provider, sessionId) {
86
84
  return null;
87
85
  }
88
86
 
87
+ const acpRecord = await findAcpSessionRecord(normalizedSessionId, normalizedProvider);
88
+ if (acpRecord) {
89
+ return {
90
+ projectName: null,
91
+ projectPath: acpRecord.projectPath || null,
92
+ session: {
93
+ id: normalizedSessionId,
94
+ source: 'acp'
95
+ }
96
+ };
97
+ }
98
+
89
99
  const projects = await getProjects();
90
100
 
91
101
  for (const project of projects) {
@@ -149,19 +159,7 @@ function isSessionActive(provider, sessionId) {
149
159
  return false;
150
160
  }
151
161
 
152
- if (normalizedProvider === 'codex') {
153
- return Boolean(isCodexSessionActive(normalizedSessionId));
154
- }
155
-
156
- if (normalizedProvider === 'gemini') {
157
- return Boolean(isGeminiSessionActive(normalizedSessionId));
158
- }
159
-
160
- if (normalizedProvider === 'opencode') {
161
- return Boolean(isOpencodeSessionActive(normalizedSessionId));
162
- }
163
-
164
- return Boolean(isClaudeSDKSessionActive(normalizedSessionId));
162
+ return Boolean(isAgentSessionActive(normalizedProvider, normalizedSessionId));
165
163
  }
166
164
 
167
165
  function inferPhaseFromEvents(events = []) {
@@ -204,6 +202,7 @@ function inferPhaseFromEvents(events = []) {
204
202
  event.kind === CONVERSATION_EVENT_KINDS.TOOL_CALL_INPUT ||
205
203
  event.kind === CONVERSATION_EVENT_KINDS.TOOL_CALL_END ||
206
204
  event.kind === CONVERSATION_EVENT_KINDS.TOOL_RESULT ||
205
+ event.kind === CONVERSATION_EVENT_KINDS.PLAN_UPDATE ||
207
206
  event.kind === CONVERSATION_EVENT_KINDS.SYSTEM_NOTICE
208
207
  ) {
209
208
  inferredPhase = AGENT_RUNTIME_PHASES.STREAMING;
@@ -1,4 +1,7 @@
1
- import { normalizeRealtimePayloadToConversationEvents } from '../../shared/conversationEvents.js';
1
+ import {
2
+ isConversationEvent,
3
+ normalizeRealtimePayloadToConversationEvents
4
+ } from '../../shared/conversationEvents.js';
2
5
  import { appendMirroredConversationEvents } from './eventStore.js';
3
6
  import { publishSessionRuntimeStateChanges } from './runtimeState.js';
4
7
 
@@ -18,19 +21,23 @@ export class SessionEventMirrorWriter {
18
21
 
19
22
  this.writer.send(data);
20
23
 
21
- const normalizedEvents = normalizeRealtimePayloadToConversationEvents({
22
- ...data,
23
- provider: data?.provider || this.provider,
24
- sessionId: data?.sessionId || this.sessionId
25
- }, this.provider);
24
+ const normalizedEvents = data?.type === 'conversation-event' && isConversationEvent(data?.event)
25
+ ? [data.event]
26
+ : normalizeRealtimePayloadToConversationEvents({
27
+ ...data,
28
+ provider: data?.provider || this.provider,
29
+ sessionId: data?.sessionId || this.sessionId
30
+ }, this.provider);
26
31
 
27
32
  normalizedEvents.forEach((event) => {
28
- this.writer.send({
29
- type: 'conversation-event',
30
- provider: event.provider,
31
- sessionId: event.sessionId,
32
- event
33
- });
33
+ if (!(data?.type === 'conversation-event' && data?.event?.eventId === event.eventId)) {
34
+ this.writer.send({
35
+ type: 'conversation-event',
36
+ provider: event.provider,
37
+ sessionId: event.sessionId,
38
+ event
39
+ });
40
+ }
34
41
  });
35
42
 
36
43
  if (normalizedEvents.length > 0) {
@@ -45,7 +45,9 @@ export function getBundledCodexPath() {
45
45
 
46
46
  export function getCodexPathOverride() {
47
47
  if (process.platform === 'win32') {
48
- return path.join(__dirname, '..', 'bin', 'codex-sdk-wrapper.cmd');
48
+ // The SDK uses child_process.spawn() directly. Handing it a .cmd wrapper
49
+ // is brittle on Windows, so point it at the bundled executable instead.
50
+ return getBundledCodexPath();
49
51
  }
50
52
 
51
53
  return path.join(__dirname, '..', 'bin', 'codex-sdk-wrapper.js');