@axhub/genie 0.2.6 → 0.2.8

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 (102) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-CTKZtqB1.js +460 -0
  3. package/dist/assets/{ReviewApp-BEicSBzW.js → ReviewApp-DM6BNAzR.js} +1 -1
  4. package/dist/assets/{_basePickBy-DkiHsp3X.js → _basePickBy-CqJbRZ9y.js} +1 -1
  5. package/dist/assets/{_baseUniq-7ElXb2sX.js → _baseUniq-BS8YH8jO.js} +1 -1
  6. package/dist/assets/{arc-CEsS3MdK.js → arc-BBmKEN-S.js} +1 -1
  7. package/dist/assets/{architectureDiagram-2XIMDMQ5-BubZ7T3U.js → architectureDiagram-2XIMDMQ5-N5lcb82R.js} +1 -1
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-Cza6M6Ht.js → blockDiagram-WCTKOSBZ-DTMwHuLn.js} +1 -1
  9. package/dist/assets/{c4Diagram-IC4MRINW-jhjtOQ12.js → c4Diagram-IC4MRINW-BTKlkXI9.js} +1 -1
  10. package/dist/assets/channel-1oJBvF-0.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB--HkodwbY.js → chunk-4BX2VUAB-DUdoTxAc.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-CyBuez4e.js → chunk-55IACEB6-Bm_92xe4.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-CuzG4iAl.js → chunk-FMBD7UC4-CGW0g62g.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-BNi8S861.js → chunk-JSJVCQXG-DYkTH3w1.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-D817O-GT.js → chunk-KX2RTZJC-C9oTlISU.js} +1 -1
  16. package/dist/assets/{chunk-NQ4KR5QH-DyujyOvx.js → chunk-NQ4KR5QH-CM50ygWP.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-VMEn-zxh.js → chunk-QZHKN3VN-7dzpYeNJ.js} +1 -1
  18. package/dist/assets/{chunk-WL4C6EOR-CQHHFLvx.js → chunk-WL4C6EOR-Cm9nQrsr.js} +1 -1
  19. package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +1 -0
  21. package/dist/assets/clone-CinxIlEu.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-qykDd54p.js → cose-bilkent-S5V4N54A-Ccp_p0JZ.js} +1 -1
  23. package/dist/assets/{dagre-KLK3FWXG-Bqp7DjEa.js → dagre-KLK3FWXG-fBwTLUp9.js} +1 -1
  24. package/dist/assets/{diagram-E7M64L7V-BKtx468K.js → diagram-E7M64L7V-CeNVmFUp.js} +1 -1
  25. package/dist/assets/{diagram-IFDJBPK2--fHfW6V2.js → diagram-IFDJBPK2-CtavyLGa.js} +1 -1
  26. package/dist/assets/{diagram-P4PSJMXO-D1kQI5RB.js → diagram-P4PSJMXO-CpQTjQwc.js} +1 -1
  27. package/dist/assets/{erDiagram-INFDFZHY-DT9YzdNw.js → erDiagram-INFDFZHY-B8R5vwhd.js} +1 -1
  28. package/dist/assets/{flowDiagram-PKNHOUZH-DWeNr4yg.js → flowDiagram-PKNHOUZH-BvkVVwIQ.js} +1 -1
  29. package/dist/assets/{ganttDiagram-A5KZAMGK--IgwcUhI.js → ganttDiagram-A5KZAMGK-DOu3hSNa.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-B5a8UWjN.js → gitGraphDiagram-K3NZZRJ6-C7zT67YE.js} +1 -1
  31. package/dist/assets/{graph-Cw1rYoD9.js → graph-D11wiwHo.js} +1 -1
  32. package/dist/assets/{highlighted-body-TPN3WLV5-BCxJHuqY.js → highlighted-body-TPN3WLV5-Babpthg-.js} +1 -1
  33. package/dist/assets/index-DFxzgWoO.js +2 -0
  34. package/dist/assets/index-YCFGDVKw.css +1 -0
  35. package/dist/assets/{infoDiagram-LFFYTUFH-D2u70rhN.js → infoDiagram-LFFYTUFH-BmA7IpQG.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-PHBUUO56-Cl8yrezU.js → ishikawaDiagram-PHBUUO56-BEquZd3E.js} +1 -1
  37. package/dist/assets/{journeyDiagram-4ABVD52K-ddP0AMU9.js → journeyDiagram-4ABVD52K-BfemGz7f.js} +1 -1
  38. package/dist/assets/{kanban-definition-K7BYSVSG-DbVt0v29.js → kanban-definition-K7BYSVSG-CWja3mln.js} +1 -1
  39. package/dist/assets/{layout-W_tRx4UV.js → layout-BLUNf-PJ.js} +1 -1
  40. package/dist/assets/{linear-CcMb2ay-.js → linear-DukIV_Xv.js} +1 -1
  41. package/dist/assets/{mermaid-O7DHMXV3-BBJqt8pT.js → mermaid-O7DHMXV3-SgtM28qI.js} +265 -215
  42. package/dist/assets/{mindmap-definition-YRQLILUH-BGhZa7Na.js → mindmap-definition-YRQLILUH-4UjqXITU.js} +1 -1
  43. package/dist/assets/{pieDiagram-SKSYHLDU-CDyJaACv.js → pieDiagram-SKSYHLDU-8AxqJd0M.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-337W2JSQ-BSYuqf0Q.js → quadrantDiagram-337W2JSQ-D60m8V8r.js} +1 -1
  45. package/dist/assets/{requirementDiagram-Z7DCOOCP-Cfi9YX9H.js → requirementDiagram-Z7DCOOCP-zqh9jBVf.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-WA2Y5GQK-Di1ShaMF.js → sankeyDiagram-WA2Y5GQK-CDZILTLI.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-2WXFIKYE-CYTTG38e.js → sequenceDiagram-2WXFIKYE-7BReFd0L.js} +1 -1
  48. package/dist/assets/{stateDiagram-RAJIS63D-CVZYMqyW.js → stateDiagram-RAJIS63D-HPTVdIG4.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +1 -0
  50. package/dist/assets/{timeline-definition-YZTLITO2-B1sdb5mK.js → timeline-definition-YZTLITO2-CTVllFgr.js} +1 -1
  51. package/dist/assets/{treemap-KZPCXAKY-CGG4gx3C.js → treemap-KZPCXAKY-BtyxboJZ.js} +1 -1
  52. package/dist/assets/{vennDiagram-LZ73GAT5-Dds37L2k.js → vennDiagram-LZ73GAT5-D96ZI6Mg.js} +1 -1
  53. package/dist/assets/{xychartDiagram-JWTSCODW-C8QKSyRR.js → xychartDiagram-JWTSCODW-eRk-39YO.js} +1 -1
  54. package/dist/index.html +2 -2
  55. package/package.json +35 -33
  56. package/server/_legacy-providers/README.md +30 -0
  57. package/server/_legacy-providers/claude-sdk.js +956 -0
  58. package/server/_legacy-providers/gemini-cli.js +368 -0
  59. package/server/_legacy-providers/openai-codex.js +705 -0
  60. package/server/_legacy-providers/opencode-cli.js +674 -0
  61. package/server/acp-runtime/client.js +1872 -0
  62. package/server/acp-runtime/index.js +408 -0
  63. package/server/acp-runtime/registry.js +45 -0
  64. package/server/acp-runtime/session-store.js +254 -0
  65. package/server/channels/runtime/AgentRuntimeAdapter.js +22 -80
  66. package/server/claude-sdk.js +24 -946
  67. package/server/cli.js +140 -2
  68. package/server/external-agent/service.js +52 -63
  69. package/server/gemini-cli.js +21 -360
  70. package/server/index.js +133 -58
  71. package/server/openai-codex.js +19 -695
  72. package/server/opencode-cli.js +68 -640
  73. package/server/projects.js +128 -85
  74. package/server/routes/agent.js +2 -0
  75. package/server/routes/cc-connect.js +1131 -0
  76. package/server/routes/cli-auth.js +1 -73
  77. package/server/routes/commands.js +4 -9
  78. package/server/routes/git.js +3 -20
  79. package/server/routes/projects.js +45 -24
  80. package/server/routes/session-core.js +44 -10
  81. package/server/session-core/abortSession.js +2 -18
  82. package/server/session-core/eventStore.js +5 -1
  83. package/server/session-core/providerAdapters.js +98 -10
  84. package/server/session-core/providerDiscovery.js +8 -3
  85. package/server/session-core/runtimeState.js +16 -17
  86. package/server/session-core/runtimeWriter.js +19 -12
  87. package/server/utils/ccConnectManager.js +390 -0
  88. package/server/utils/ccConnectState.js +575 -0
  89. package/server/utils/resolveCommandPath.js +71 -0
  90. package/server/utils/workspaceRoots.js +154 -0
  91. package/shared/conversationEvents.js +347 -10
  92. package/dist/assets/App-CYTE30Cf.js +0 -484
  93. package/dist/assets/channel-RmqTALN0.js +0 -1
  94. package/dist/assets/classDiagram-VBA2DB6C-wvVV1ggz.js +0 -1
  95. package/dist/assets/classDiagram-v2-RAHNMMFH-wvVV1ggz.js +0 -1
  96. package/dist/assets/clone-oT5aWXpf.js +0 -1
  97. package/dist/assets/index-CBuAXA5S.js +0 -2
  98. package/dist/assets/index-CyLWKyxy.css +0 -1
  99. package/dist/assets/stateDiagram-v2-FVOUBMTO-Bbl0b4-i.js +0 -1
  100. package/server/cli.test.js +0 -76
  101. package/server/external-agent/service.test.js +0 -53
  102. package/server/external-agent/ws.test.js +0 -289
@@ -0,0 +1,154 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+
4
+ export const UNIX_WORKSPACE_ROOTS = '__workspace_roots__';
5
+
6
+ export function parseWorkspaceRoots(value) {
7
+ if (!value || typeof value !== 'string') {
8
+ return [];
9
+ }
10
+
11
+ const seen = new Set();
12
+ const roots = [];
13
+
14
+ for (const entry of value.split(',')) {
15
+ const trimmed = entry.trim();
16
+ if (!trimmed) continue;
17
+
18
+ const resolved = path.resolve(trimmed);
19
+ if (seen.has(resolved)) continue;
20
+
21
+ seen.add(resolved);
22
+ roots.push(resolved);
23
+ }
24
+
25
+ return roots;
26
+ }
27
+
28
+ export function getDefaultWorkspaceRoots({
29
+ platform = process.platform,
30
+ homeDir = os.homedir()
31
+ } = {}) {
32
+ if (platform === 'win32') {
33
+ return [];
34
+ }
35
+
36
+ const roots = [path.resolve(homeDir)];
37
+
38
+ if (platform === 'darwin') {
39
+ roots.push('/Volumes');
40
+ } else if (platform === 'linux') {
41
+ roots.push('/mnt', '/media');
42
+ const userName = path.basename(homeDir);
43
+ if (userName) {
44
+ roots.push(path.join('/run/media', userName));
45
+ }
46
+ }
47
+
48
+ return [...new Set(roots.map((root) => path.resolve(root)))];
49
+ }
50
+
51
+ export function resolveWorkspaceRootSettings({
52
+ env = process.env,
53
+ platform = process.platform,
54
+ homeDir = os.homedir()
55
+ } = {}) {
56
+ const configuredRoots = parseWorkspaceRoots(env.WORKSPACES_ROOTS);
57
+ const hasWorkspaceRootRestriction = configuredRoots.length > 0 || platform !== 'win32';
58
+ const allowedWorkspaceRoots = configuredRoots.length > 0
59
+ ? configuredRoots
60
+ : (hasWorkspaceRootRestriction ? getDefaultWorkspaceRoots({ platform, homeDir }) : []);
61
+
62
+ return {
63
+ defaultWorkspaceRoot: path.resolve(homeDir),
64
+ hasWorkspaceRootRestriction,
65
+ allowedWorkspaceRoots,
66
+ };
67
+ }
68
+
69
+ const workspaceRootSettings = resolveWorkspaceRootSettings();
70
+
71
+ export const DEFAULT_WORKSPACES_ROOT = workspaceRootSettings.defaultWorkspaceRoot;
72
+ export const HAS_WORKSPACES_ROOT_RESTRICTION = workspaceRootSettings.hasWorkspaceRootRestriction;
73
+ export const WORKSPACES_ROOTS = workspaceRootSettings.allowedWorkspaceRoots;
74
+
75
+ export function expandWorkspacePath(inputPath, homeDir = DEFAULT_WORKSPACES_ROOT) {
76
+ if (!inputPath) return inputPath;
77
+ if (inputPath === '~') {
78
+ return homeDir;
79
+ }
80
+ if (inputPath.startsWith('~/') || inputPath.startsWith('~\\')) {
81
+ return path.join(homeDir, inputPath.slice(2));
82
+ }
83
+ return inputPath;
84
+ }
85
+
86
+ export function isPathWithinAllowedRoots(candidatePath, allowedRoots) {
87
+ const normalizedCandidate = path.resolve(candidatePath);
88
+
89
+ return allowedRoots.some((allowedRoot) => {
90
+ const normalizedRoot = path.resolve(allowedRoot);
91
+ return normalizedCandidate === normalizedRoot
92
+ || normalizedCandidate.startsWith(normalizedRoot + path.sep);
93
+ });
94
+ }
95
+
96
+ export async function resolveAllowedWorkspaceRoots({
97
+ fsImpl,
98
+ allowedWorkspaceRoots = WORKSPACES_ROOTS
99
+ } = {}) {
100
+ const resolvedRoots = [];
101
+ const seen = new Set();
102
+
103
+ for (const root of allowedWorkspaceRoots) {
104
+ let resolvedRoot;
105
+
106
+ try {
107
+ resolvedRoot = await fsImpl.realpath(root);
108
+ } catch (error) {
109
+ resolvedRoot = path.resolve(root);
110
+ }
111
+
112
+ if (seen.has(resolvedRoot)) continue;
113
+ seen.add(resolvedRoot);
114
+ resolvedRoots.push(resolvedRoot);
115
+ }
116
+
117
+ return resolvedRoots;
118
+ }
119
+
120
+ export function formatAllowedWorkspaceRoots(allowedRoots) {
121
+ return allowedRoots.join(', ');
122
+ }
123
+
124
+ export function shouldUseVirtualWorkspaceRoots({
125
+ dirPath,
126
+ hasWorkspaceRootRestriction = HAS_WORKSPACES_ROOT_RESTRICTION
127
+ } = {}) {
128
+ return hasWorkspaceRootRestriction
129
+ && (!dirPath || dirPath === UNIX_WORKSPACE_ROOTS);
130
+ }
131
+
132
+ export function buildWorkspaceRootSuggestions({
133
+ allowedWorkspaceRoots = WORKSPACES_ROOTS,
134
+ homeDir = DEFAULT_WORKSPACES_ROOT
135
+ } = {}) {
136
+ const resolvedHomeDir = path.resolve(homeDir);
137
+ const seen = new Set();
138
+ const suggestions = [];
139
+
140
+ for (const root of allowedWorkspaceRoots) {
141
+ const resolvedRoot = path.resolve(root);
142
+ if (seen.has(resolvedRoot)) continue;
143
+
144
+ seen.add(resolvedRoot);
145
+ suggestions.push({
146
+ path: resolvedRoot,
147
+ name: resolvedRoot === resolvedHomeDir ? 'Home' : (path.basename(resolvedRoot) || resolvedRoot),
148
+ type: 'directory',
149
+ isWorkspaceRoot: true
150
+ });
151
+ }
152
+
153
+ return suggestions;
154
+ }
@@ -16,6 +16,7 @@ export const CONVERSATION_EVENT_KINDS = {
16
16
  ASSISTANT_TEXT_START: 'assistant_text_start',
17
17
  ASSISTANT_TEXT_DELTA: 'assistant_text_delta',
18
18
  ASSISTANT_TEXT_END: 'assistant_text_end',
19
+ ASSISTANT_CONTENT_BLOCK: 'assistant_content_block',
19
20
  REASONING_START: 'reasoning_start',
20
21
  REASONING_DELTA: 'reasoning_delta',
21
22
  REASONING_END: 'reasoning_end',
@@ -23,6 +24,9 @@ export const CONVERSATION_EVENT_KINDS = {
23
24
  TOOL_CALL_INPUT: 'tool_call_input',
24
25
  TOOL_CALL_END: 'tool_call_end',
25
26
  TOOL_RESULT: 'tool_result',
27
+ PLAN_UPDATE: 'plan_update',
28
+ MODE_UPDATE: 'mode_update',
29
+ AVAILABLE_COMMANDS_UPDATE: 'available_commands_update',
26
30
  APPROVAL_REQUEST: 'approval_request',
27
31
  APPROVAL_RESOLVED: 'approval_resolved',
28
32
  ARTIFACT_CREATED: 'artifact_created',
@@ -236,6 +240,167 @@ function stringifyToolContent(value) {
236
240
  }
237
241
  }
238
242
 
243
+ function cloneJsonValue(value) {
244
+ if (value == null) {
245
+ return value;
246
+ }
247
+
248
+ try {
249
+ return JSON.parse(JSON.stringify(value));
250
+ } catch {
251
+ return value;
252
+ }
253
+ }
254
+
255
+ function normalizeMessageContentBlocks(value) {
256
+ if (!Array.isArray(value)) {
257
+ return [];
258
+ }
259
+
260
+ return value
261
+ .filter((item) => item && typeof item === 'object')
262
+ .map((item) => cloneJsonValue(item));
263
+ }
264
+
265
+ function getConversationEventClientRequestId(event) {
266
+ const rawValue = event?.extensions?.clientRequestId || event?.payload?.clientRequestId || null;
267
+ return typeof rawValue === 'string' && rawValue.trim()
268
+ ? rawValue.trim()
269
+ : null;
270
+ }
271
+
272
+ function getTimelineMessageClientRequestId(message) {
273
+ const rawValue = message?.clientRequestId || message?.extensions?.clientRequestId || null;
274
+ return typeof rawValue === 'string' && rawValue.trim()
275
+ ? rawValue.trim()
276
+ : null;
277
+ }
278
+
279
+ function buildComparableUserMessageContentBlocks(message = {}) {
280
+ const imageBlocks = Array.isArray(message?.images)
281
+ ? message.images.map((image) => ({
282
+ type: 'image',
283
+ data: image?.data || image?.image || '',
284
+ mimeType: image?.mimeType || 'application/octet-stream',
285
+ name: image?.name || image?.filename || ''
286
+ }))
287
+ : [];
288
+
289
+ return [
290
+ ...imageBlocks,
291
+ ...normalizeMessageContentBlocks(message?.contentBlocks)
292
+ ];
293
+ }
294
+
295
+ function findMatchingOptimisticUserMessageIndex(messages, event, payload = {}) {
296
+ const clientRequestId = getConversationEventClientRequestId(event);
297
+ if (!clientRequestId) {
298
+ return -1;
299
+ }
300
+
301
+ const expectedContentBlocks = JSON.stringify(normalizeMessageContentBlocks(payload.contentBlocks));
302
+
303
+ return findLatestTimelineMessageIndex(messages, (message) => {
304
+ if (String(message?.type || '').trim().toLowerCase() !== 'user') {
305
+ return false;
306
+ }
307
+
308
+ if (getTimelineMessageClientRequestId(message) !== clientRequestId) {
309
+ return false;
310
+ }
311
+
312
+ if (String(message?.content || '') !== String(payload.text || '')) {
313
+ return false;
314
+ }
315
+
316
+ const actualContentBlocks = JSON.stringify(buildComparableUserMessageContentBlocks(message));
317
+ return actualContentBlocks === expectedContentBlocks;
318
+ });
319
+ }
320
+
321
+ function normalizeAcpModeState(value) {
322
+ if (!value || typeof value !== 'object') {
323
+ return null;
324
+ }
325
+
326
+ const availableModes = Array.isArray(value.availableModes)
327
+ ? value.availableModes
328
+ .filter((mode) => mode && typeof mode === 'object' && String(mode.id || '').trim())
329
+ .map((mode) => ({
330
+ id: String(mode.id).trim(),
331
+ name: String(mode.name || mode.id || '').trim() || String(mode.id).trim(),
332
+ description: mode.description == null ? null : String(mode.description)
333
+ }))
334
+ : [];
335
+ const currentModeId = typeof value.currentModeId === 'string' && value.currentModeId.trim()
336
+ ? value.currentModeId.trim()
337
+ : null;
338
+
339
+ if (!availableModes.length && !currentModeId) {
340
+ return null;
341
+ }
342
+
343
+ return {
344
+ availableModes,
345
+ currentModeId: currentModeId || availableModes[0]?.id || null
346
+ };
347
+ }
348
+
349
+ function normalizeAcpAvailableCommands(value) {
350
+ const commands = Array.isArray(value?.availableCommands)
351
+ ? value.availableCommands
352
+ : Array.isArray(value)
353
+ ? value
354
+ : [];
355
+
356
+ return commands
357
+ .filter((command) => command && typeof command === 'object' && String(command.name || '').trim())
358
+ .map((command) => ({
359
+ name: String(command.name).trim(),
360
+ description: command.description == null ? '' : String(command.description),
361
+ input: command.input && typeof command.input === 'object' ? cloneJsonValue(command.input) : null,
362
+ source: 'acp',
363
+ namespace: 'acp',
364
+ type: 'acp'
365
+ }));
366
+ }
367
+
368
+ function mergeToolCallSnapshot(existingSnapshot = null, payload = {}) {
369
+ const nextSnapshot = existingSnapshot && typeof existingSnapshot === 'object'
370
+ ? { ...existingSnapshot }
371
+ : {};
372
+
373
+ if (payload.title != null) {
374
+ nextSnapshot.title = String(payload.title);
375
+ }
376
+
377
+ if (payload.kind != null) {
378
+ nextSnapshot.kind = String(payload.kind);
379
+ }
380
+
381
+ if (payload.status != null) {
382
+ nextSnapshot.status = String(payload.status);
383
+ }
384
+
385
+ if (payload.rawInput !== undefined) {
386
+ nextSnapshot.rawInput = cloneJsonValue(payload.rawInput);
387
+ }
388
+
389
+ if (payload.rawOutput !== undefined) {
390
+ nextSnapshot.rawOutput = cloneJsonValue(payload.rawOutput);
391
+ }
392
+
393
+ if (Array.isArray(payload.contentBlocks)) {
394
+ nextSnapshot.content = cloneJsonValue(payload.contentBlocks);
395
+ }
396
+
397
+ if (Array.isArray(payload.locations)) {
398
+ nextSnapshot.locations = cloneJsonValue(payload.locations);
399
+ }
400
+
401
+ return Object.keys(nextSnapshot).length > 0 ? nextSnapshot : null;
402
+ }
403
+
239
404
  function collectClaudeToolResults(rawMessages) {
240
405
  const toolResults = new Map();
241
406
  for (const msg of rawMessages || []) {
@@ -279,7 +444,6 @@ function ensureStreamingTextMessage(messages, {
279
444
  }) {
280
445
  const index = findLatestTimelineMessageIndex(messages, (message) => (
281
446
  message?.eventMessageId === messageId &&
282
- !!message?.isStreaming &&
283
447
  !message.isToolUse &&
284
448
  !!message.isThinking === !!isThinking
285
449
  ));
@@ -310,6 +474,40 @@ function ensureStreamingTextMessage(messages, {
310
474
  return { messages, index: messages.length - 1 };
311
475
  }
312
476
 
477
+ function ensureConversationMessage(messages, {
478
+ messageId,
479
+ timestamp,
480
+ provider,
481
+ role = 'assistant'
482
+ }) {
483
+ const index = findLatestTimelineMessageIndex(messages, (message) => (
484
+ message?.eventMessageId === messageId &&
485
+ !message.isToolUse &&
486
+ !message.isThinking &&
487
+ String(message?.type || '').trim().toLowerCase() === String(role || 'assistant').trim().toLowerCase()
488
+ ));
489
+
490
+ if (index >= 0) {
491
+ const existing = { ...messages[index] };
492
+ if (provider) existing.provider = provider;
493
+ if (timestamp && !existing.timestamp) existing.timestamp = timestamp;
494
+ messages[index] = existing;
495
+ return { messages, index };
496
+ }
497
+
498
+ messages.push({
499
+ type: role,
500
+ content: '',
501
+ timestamp,
502
+ provider,
503
+ isStreaming: false,
504
+ eventMessageId: messageId,
505
+ contentBlocks: []
506
+ });
507
+
508
+ return { messages, index: messages.length - 1 };
509
+ }
510
+
313
511
  function ensureToolMessage(messages, {
314
512
  toolCallId,
315
513
  toolName,
@@ -458,14 +656,41 @@ export function applyConversationEventToTimelineMessages(messages = [], event, s
458
656
  const payload = event.payload || {};
459
657
 
460
658
  switch (event.kind) {
461
- case CONVERSATION_EVENT_KINDS.USER_MESSAGE:
659
+ case CONVERSATION_EVENT_KINDS.USER_MESSAGE: {
660
+ const clientRequestId = getConversationEventClientRequestId(event);
661
+ const optimisticIndex = findMatchingOptimisticUserMessageIndex(nextMessages, event, payload);
662
+ const normalizedContentBlocks = normalizeMessageContentBlocks(payload.contentBlocks);
663
+
664
+ if (optimisticIndex >= 0) {
665
+ const existingMessage = nextMessages[optimisticIndex] || {};
666
+ const nextContentBlocks = Array.isArray(existingMessage.images) && existingMessage.images.length > 0
667
+ ? normalizedContentBlocks.filter((block) => block?.type !== 'image')
668
+ : normalizedContentBlocks;
669
+
670
+ nextMessages[optimisticIndex] = {
671
+ ...existingMessage,
672
+ type: 'user',
673
+ content: payload.text || existingMessage.content || '',
674
+ timestamp: existingMessage.timestamp || timestamp,
675
+ provider,
676
+ contentBlocks: nextContentBlocks,
677
+ clientRequestId: clientRequestId || existingMessage.clientRequestId || null,
678
+ eventId: event.eventId || existingMessage.eventId,
679
+ id: existingMessage.id || event.eventId || existingMessage.id
680
+ };
681
+ break;
682
+ }
683
+
462
684
  nextMessages.push({
463
685
  type: 'user',
464
686
  content: payload.text || '',
465
687
  timestamp,
466
- provider
688
+ provider,
689
+ contentBlocks: normalizedContentBlocks,
690
+ clientRequestId
467
691
  });
468
692
  break;
693
+ }
469
694
 
470
695
  case CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_START: {
471
696
  const messageId = payload.messageId || event.extensions?.messageId || createEventId('assistant_text');
@@ -496,6 +721,26 @@ export function applyConversationEventToTimelineMessages(messages = [], event, s
496
721
  break;
497
722
  }
498
723
 
724
+ case CONVERSATION_EVENT_KINDS.ASSISTANT_CONTENT_BLOCK: {
725
+ const messageId = payload.messageId || event.extensions?.messageId || createEventId('assistant_content');
726
+ const { index } = ensureConversationMessage(nextMessages, {
727
+ messageId,
728
+ timestamp,
729
+ provider,
730
+ role: 'assistant'
731
+ });
732
+ const target = { ...nextMessages[index] };
733
+ const nextBlocks = Array.isArray(target.contentBlocks) ? [...target.contentBlocks] : [];
734
+ if (payload.contentBlock && typeof payload.contentBlock === 'object') {
735
+ nextBlocks.push(cloneJsonValue(payload.contentBlock));
736
+ }
737
+ target.contentBlocks = nextBlocks;
738
+ target.provider = target.provider || provider;
739
+ target.timestamp = target.timestamp || timestamp;
740
+ nextMessages[index] = target;
741
+ break;
742
+ }
743
+
499
744
  case CONVERSATION_EVENT_KINDS.REASONING_START: {
500
745
  const messageId = payload.messageId || event.extensions?.messageId || createEventId('reasoning');
501
746
  ensureStreamingTextMessage(nextMessages, { messageId, timestamp, provider, isThinking: true });
@@ -534,7 +779,11 @@ export function applyConversationEventToTimelineMessages(messages = [], event, s
534
779
  timestamp,
535
780
  provider
536
781
  });
537
- nextMessages[index] = { ...nextMessages[index], toolName: payload.toolName || nextMessages[index].toolName };
782
+ nextMessages[index] = {
783
+ ...nextMessages[index],
784
+ toolName: payload.toolName || nextMessages[index].toolName,
785
+ acpToolCall: mergeToolCallSnapshot(nextMessages[index].acpToolCall, payload)
786
+ };
538
787
  break;
539
788
  }
540
789
 
@@ -548,8 +797,13 @@ export function applyConversationEventToTimelineMessages(messages = [], event, s
548
797
  });
549
798
  nextMessages[index] = {
550
799
  ...nextMessages[index],
551
- toolInput: stringifyToolContent(payload.input || ''),
552
- provider: nextMessages[index].provider || provider
800
+ toolInput: payload.input !== undefined
801
+ ? stringifyToolContent(payload.input)
802
+ : payload.rawInput !== undefined
803
+ ? stringifyToolContent(payload.rawInput)
804
+ : nextMessages[index].toolInput,
805
+ provider: nextMessages[index].provider || provider,
806
+ acpToolCall: mergeToolCallSnapshot(nextMessages[index].acpToolCall, payload)
553
807
  };
554
808
  break;
555
809
  }
@@ -563,7 +817,12 @@ export function applyConversationEventToTimelineMessages(messages = [], event, s
563
817
  timestamp,
564
818
  provider
565
819
  });
566
- nextMessages[index] = { ...nextMessages[index], toolName: payload.toolName || nextMessages[index].toolName };
820
+ nextMessages[index] = {
821
+ ...nextMessages[index],
822
+ toolName: payload.toolName || nextMessages[index].toolName,
823
+ toolEndedAt: timestamp,
824
+ acpToolCall: mergeToolCallSnapshot(nextMessages[index].acpToolCall, payload)
825
+ };
567
826
  break;
568
827
  }
569
828
 
@@ -578,17 +837,44 @@ export function applyConversationEventToTimelineMessages(messages = [], event, s
578
837
  nextMessages[index] = {
579
838
  ...nextMessages[index],
580
839
  toolResult: {
581
- content: stringifyToolContent(payload.content || ''),
840
+ content: stringifyToolContent(payload.content ?? ''),
582
841
  isError: !!payload.isError,
583
842
  toolUseResult: payload.toolUseResult || null,
843
+ rawOutput: payload.rawOutput,
844
+ contentBlocks: Array.isArray(payload.contentBlocks) ? cloneJsonValue(payload.contentBlocks) : null,
845
+ status: payload.status || null,
584
846
  timestamp
585
847
  },
586
848
  toolError: !!payload.isError,
587
- provider: nextMessages[index].provider || provider
849
+ toolEndedAt: nextMessages[index].toolEndedAt || timestamp,
850
+ provider: nextMessages[index].provider || provider,
851
+ acpToolCall: mergeToolCallSnapshot(nextMessages[index].acpToolCall, payload)
588
852
  };
589
853
  break;
590
854
  }
591
855
 
856
+ case CONVERSATION_EVENT_KINDS.PLAN_UPDATE: {
857
+ const planEntries = Array.isArray(payload.entries) ? cloneJsonValue(payload.entries) : [];
858
+ const index = findLatestTimelineMessageIndex(nextMessages, (message) => !!message?.isPlanUpdate);
859
+ const nextPlanMessage = {
860
+ ...(index >= 0 ? nextMessages[index] : {}),
861
+ id: (index >= 0 ? nextMessages[index]?.id : null) || `plan:${event.sessionId || provider}`,
862
+ type: 'assistant',
863
+ content: String(payload.message || 'Plan updated'),
864
+ timestamp,
865
+ provider,
866
+ isPlanUpdate: true,
867
+ planEntries
868
+ };
869
+
870
+ if (index >= 0) {
871
+ nextMessages[index] = nextPlanMessage;
872
+ } else {
873
+ nextMessages.push(nextPlanMessage);
874
+ }
875
+ break;
876
+ }
877
+
592
878
  case CONVERSATION_EVENT_KINDS.APPROVAL_REQUEST:
593
879
  if (payload.message) {
594
880
  nextMessages.push({
@@ -641,6 +927,8 @@ export function applyConversationEventToTimelineMessages(messages = [], event, s
641
927
  break;
642
928
 
643
929
  case CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED:
930
+ case CONVERSATION_EVENT_KINDS.MODE_UPDATE:
931
+ case CONVERSATION_EVENT_KINDS.AVAILABLE_COMMANDS_UPDATE:
644
932
  case CONVERSATION_EVENT_KINDS.ARTIFACT_CREATED:
645
933
  default:
646
934
  break;
@@ -661,6 +949,39 @@ export function conversationEventsToTimelineMessages(events = [], sessionProvide
661
949
  );
662
950
  }
663
951
 
952
+ export function extractAcpSessionMetadataFromConversationEvents(events = []) {
953
+ const metadata = {
954
+ modeState: null,
955
+ availableCommands: []
956
+ };
957
+
958
+ for (const event of Array.isArray(events) ? events : []) {
959
+ if (!isConversationEvent(event)) {
960
+ continue;
961
+ }
962
+
963
+ if (event.kind === CONVERSATION_EVENT_KINDS.MODE_UPDATE) {
964
+ const nextModeState = normalizeAcpModeState(event.payload || {});
965
+ if (nextModeState) {
966
+ metadata.modeState = metadata.modeState
967
+ ? {
968
+ availableModes: nextModeState.availableModes.length > 0
969
+ ? nextModeState.availableModes
970
+ : metadata.modeState.availableModes,
971
+ currentModeId: nextModeState.currentModeId || metadata.modeState.currentModeId || null
972
+ }
973
+ : nextModeState;
974
+ }
975
+ }
976
+
977
+ if (event.kind === CONVERSATION_EVENT_KINDS.AVAILABLE_COMMANDS_UPDATE) {
978
+ metadata.availableCommands = normalizeAcpAvailableCommands(event.payload || {});
979
+ }
980
+ }
981
+
982
+ return metadata;
983
+ }
984
+
664
985
  function inferRealtimeMessageId(payload, provider, sessionId, kindPrefix = 'assistant_text') {
665
986
  const data = payload?.data || {};
666
987
  const idCandidate =
@@ -736,7 +1057,7 @@ export function normalizeRealtimePayloadToConversationEvents(payload, fallbackPr
736
1057
  const rawRef = { type: payload.type };
737
1058
 
738
1059
  if (payload.type === 'session-created') {
739
- return [
1060
+ const events = [
740
1061
  createConversationEvent({
741
1062
  kind: CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED,
742
1063
  provider,
@@ -746,6 +1067,22 @@ export function normalizeRealtimePayloadToConversationEvents(payload, fallbackPr
746
1067
  rawRef
747
1068
  })
748
1069
  ];
1070
+
1071
+ const initialModeState = normalizeAcpModeState(payload.modes || null);
1072
+ if (initialModeState) {
1073
+ events.push(
1074
+ createConversationEvent({
1075
+ kind: CONVERSATION_EVENT_KINDS.MODE_UPDATE,
1076
+ provider,
1077
+ sessionId,
1078
+ timestamp,
1079
+ payload: initialModeState,
1080
+ rawRef
1081
+ })
1082
+ );
1083
+ }
1084
+
1085
+ return events;
749
1086
  }
750
1087
 
751
1088
  if (payload.type === 'claude-permission-request') {