@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
@@ -0,0 +1,403 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ applyConversationEventToTimelineMessages,
6
+ CONVERSATION_EVENT_KINDS,
7
+ createConversationEvent,
8
+ extractAcpSessionMetadataFromConversationEvents,
9
+ normalizeLegacyHistoryEntries,
10
+ normalizeRealtimePayloadToConversationEvents
11
+ } from './conversationEvents.js';
12
+
13
+ test('createConversationEvent returns normalized event payload', () => {
14
+ const event = createConversationEvent({
15
+ kind: CONVERSATION_EVENT_KINDS.USER_MESSAGE,
16
+ provider: 'claude',
17
+ sessionId: 'session-1',
18
+ payload: {
19
+ text: 'hello'
20
+ }
21
+ });
22
+
23
+ assert.equal(event.kind, CONVERSATION_EVENT_KINDS.USER_MESSAGE);
24
+ assert.equal(event.provider, 'claude');
25
+ assert.equal(event.sessionId, 'session-1');
26
+ assert.equal(event.payload.text, 'hello');
27
+ assert.match(event.eventId, /^user_message:/);
28
+ });
29
+
30
+ test('applyConversationEventToTimelineMessages builds assistant and tool messages', () => {
31
+ const events = [
32
+ createConversationEvent({
33
+ kind: CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_START,
34
+ provider: 'claude',
35
+ sessionId: 'session-2',
36
+ payload: { messageId: 'assistant:1' }
37
+ }),
38
+ createConversationEvent({
39
+ kind: CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_DELTA,
40
+ provider: 'claude',
41
+ sessionId: 'session-2',
42
+ payload: { messageId: 'assistant:1', text: 'Hello from ACP.' }
43
+ }),
44
+ createConversationEvent({
45
+ kind: CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_END,
46
+ provider: 'claude',
47
+ sessionId: 'session-2',
48
+ payload: { messageId: 'assistant:1' }
49
+ }),
50
+ createConversationEvent({
51
+ kind: CONVERSATION_EVENT_KINDS.TOOL_CALL_START,
52
+ provider: 'claude',
53
+ sessionId: 'session-2',
54
+ payload: {
55
+ toolCallId: 'tool:1',
56
+ toolName: 'WriteFile',
57
+ title: 'Write file',
58
+ kind: 'edit',
59
+ status: 'in_progress'
60
+ }
61
+ }),
62
+ createConversationEvent({
63
+ kind: CONVERSATION_EVENT_KINDS.TOOL_CALL_INPUT,
64
+ provider: 'claude',
65
+ sessionId: 'session-2',
66
+ payload: {
67
+ toolCallId: 'tool:1',
68
+ input: { path: 'README.md' },
69
+ rawInput: { path: 'README.md' },
70
+ locations: [{ path: 'README.md', line: 1 }]
71
+ }
72
+ }),
73
+ createConversationEvent({
74
+ kind: CONVERSATION_EVENT_KINDS.TOOL_CALL_END,
75
+ provider: 'claude',
76
+ sessionId: 'session-2',
77
+ payload: {
78
+ toolCallId: 'tool:1',
79
+ toolName: 'WriteFile',
80
+ status: 'completed'
81
+ }
82
+ }),
83
+ createConversationEvent({
84
+ kind: CONVERSATION_EVENT_KINDS.TOOL_RESULT,
85
+ provider: 'claude',
86
+ sessionId: 'session-2',
87
+ payload: {
88
+ toolCallId: 'tool:1',
89
+ content: 'updated README',
90
+ contentBlocks: [
91
+ {
92
+ type: 'diff',
93
+ path: 'README.md',
94
+ oldText: 'old',
95
+ newText: 'new'
96
+ }
97
+ ],
98
+ isError: false
99
+ }
100
+ })
101
+ ];
102
+
103
+ const messages = events.reduce(
104
+ (timeline, event) => applyConversationEventToTimelineMessages(timeline, event, 'claude'),
105
+ []
106
+ );
107
+
108
+ assert.equal(messages.length, 2);
109
+ assert.equal(messages[0].content, 'Hello from ACP.');
110
+ assert.equal(messages[0].isStreaming, false);
111
+ assert.equal(messages[1].isToolUse, true);
112
+ assert.equal(messages[1].toolName, 'WriteFile');
113
+ assert.equal(messages[1].toolResult.content, 'updated README');
114
+ assert.equal(messages[1].acpToolCall.kind, 'edit');
115
+ assert.equal(messages[1].acpToolCall.status, 'completed');
116
+ assert.equal(messages[1].acpToolCall.locations[0].path, 'README.md');
117
+ assert.equal(messages[1].acpToolCall.content[0].type, 'diff');
118
+ });
119
+
120
+ test('applyConversationEventToTimelineMessages marks tool call end even without a tool result', () => {
121
+ const events = [
122
+ createConversationEvent({
123
+ kind: CONVERSATION_EVENT_KINDS.TOOL_CALL_START,
124
+ provider: 'codex',
125
+ sessionId: 'session-tool-end',
126
+ timestamp: '2026-03-30T09:10:34.879Z',
127
+ payload: {
128
+ toolCallId: 'tool:web:1',
129
+ toolName: 'Searching the Web'
130
+ }
131
+ }),
132
+ createConversationEvent({
133
+ kind: CONVERSATION_EVENT_KINDS.TOOL_CALL_INPUT,
134
+ provider: 'codex',
135
+ sessionId: 'session-tool-end',
136
+ timestamp: '2026-03-30T09:10:35.725Z',
137
+ payload: {
138
+ toolCallId: 'tool:web:1',
139
+ toolName: 'Searching for: weather: Guangzhou',
140
+ input: {
141
+ query: 'weather: Guangzhou'
142
+ }
143
+ }
144
+ }),
145
+ createConversationEvent({
146
+ kind: CONVERSATION_EVENT_KINDS.TOOL_CALL_END,
147
+ provider: 'codex',
148
+ sessionId: 'session-tool-end',
149
+ timestamp: '2026-03-30T09:10:36.142Z',
150
+ payload: {
151
+ toolCallId: 'tool:web:1',
152
+ toolName: 'Searching for: weather: Guangzhou'
153
+ }
154
+ })
155
+ ];
156
+
157
+ const messages = events.reduce(
158
+ (timeline, event) => applyConversationEventToTimelineMessages(timeline, event, 'codex'),
159
+ []
160
+ );
161
+
162
+ assert.equal(messages.length, 1);
163
+ assert.equal(messages[0].isToolUse, true);
164
+ assert.equal(messages[0].toolResult, undefined);
165
+ assert.equal(messages[0].toolEndedAt, '2026-03-30T09:10:36.142Z');
166
+ });
167
+
168
+ test('applyConversationEventToTimelineMessages merges echoed user events into optimistic messages', () => {
169
+ const optimisticMessage = {
170
+ type: 'user',
171
+ content: '广州天气怎么样?',
172
+ timestamp: '2026-03-30T09:00:00.000Z',
173
+ provider: 'codex',
174
+ clientRequestId: 'req-user-1',
175
+ contentBlocks: [
176
+ {
177
+ type: 'resource_link',
178
+ uri: '/tmp/weather.md',
179
+ name: 'weather.md'
180
+ }
181
+ ]
182
+ };
183
+
184
+ const echoedEvent = createConversationEvent({
185
+ kind: CONVERSATION_EVENT_KINDS.USER_MESSAGE,
186
+ provider: 'codex',
187
+ sessionId: 'session-user',
188
+ timestamp: '2026-03-30T09:00:01.000Z',
189
+ payload: {
190
+ text: '广州天气怎么样?',
191
+ contentBlocks: [
192
+ {
193
+ type: 'resource_link',
194
+ uri: '/tmp/weather.md',
195
+ name: 'weather.md'
196
+ }
197
+ ]
198
+ },
199
+ extensions: {
200
+ clientRequestId: 'req-user-1'
201
+ }
202
+ });
203
+
204
+ const messages = applyConversationEventToTimelineMessages([optimisticMessage], echoedEvent, 'codex');
205
+
206
+ assert.equal(messages.length, 1);
207
+ assert.equal(messages[0].content, '广州天气怎么样?');
208
+ assert.equal(messages[0].clientRequestId, 'req-user-1');
209
+ assert.equal(messages[0].eventId, echoedEvent.eventId);
210
+ assert.equal(messages[0].provider, 'codex');
211
+ });
212
+
213
+ test('applyConversationEventToTimelineMessages replaces ACP plan entries in-place', () => {
214
+ const firstPlan = createConversationEvent({
215
+ kind: CONVERSATION_EVENT_KINDS.PLAN_UPDATE,
216
+ provider: 'claude',
217
+ sessionId: 'session-plan',
218
+ payload: {
219
+ entries: [
220
+ { content: 'Read files', status: 'completed', priority: 'medium' },
221
+ { content: 'Implement UI', status: 'in_progress', priority: 'high' }
222
+ ],
223
+ message: 'Implementation plan updated'
224
+ }
225
+ });
226
+
227
+ const secondPlan = createConversationEvent({
228
+ kind: CONVERSATION_EVENT_KINDS.PLAN_UPDATE,
229
+ provider: 'claude',
230
+ sessionId: 'session-plan',
231
+ payload: {
232
+ entries: [
233
+ { content: 'Read files', status: 'completed', priority: 'medium' },
234
+ { content: 'Implement UI', status: 'completed', priority: 'high' }
235
+ ],
236
+ message: 'Implementation plan updated'
237
+ }
238
+ });
239
+
240
+ let messages = applyConversationEventToTimelineMessages([], firstPlan, 'claude');
241
+ messages = applyConversationEventToTimelineMessages(messages, secondPlan, 'claude');
242
+
243
+ assert.equal(messages.length, 1);
244
+ assert.equal(messages[0].isPlanUpdate, true);
245
+ assert.equal(messages[0].planEntries.length, 2);
246
+ assert.equal(messages[0].planEntries[1].status, 'completed');
247
+ });
248
+
249
+ test('applyConversationEventToTimelineMessages keeps assistant content blocks on the same message', () => {
250
+ const events = [
251
+ createConversationEvent({
252
+ kind: CONVERSATION_EVENT_KINDS.ASSISTANT_CONTENT_BLOCK,
253
+ provider: 'claude',
254
+ sessionId: 'session-content',
255
+ payload: {
256
+ messageId: 'assistant:content:1',
257
+ contentBlock: {
258
+ type: 'resource_link',
259
+ uri: '/tmp/spec.md',
260
+ name: 'spec.md'
261
+ }
262
+ }
263
+ }),
264
+ createConversationEvent({
265
+ kind: CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_START,
266
+ provider: 'claude',
267
+ sessionId: 'session-content',
268
+ payload: {
269
+ messageId: 'assistant:content:1'
270
+ }
271
+ }),
272
+ createConversationEvent({
273
+ kind: CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_DELTA,
274
+ provider: 'claude',
275
+ sessionId: 'session-content',
276
+ payload: {
277
+ messageId: 'assistant:content:1',
278
+ text: 'Attached the spec for review.'
279
+ }
280
+ }),
281
+ createConversationEvent({
282
+ kind: CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_END,
283
+ provider: 'claude',
284
+ sessionId: 'session-content',
285
+ payload: {
286
+ messageId: 'assistant:content:1'
287
+ }
288
+ })
289
+ ];
290
+
291
+ const messages = events.reduce(
292
+ (timeline, event) => applyConversationEventToTimelineMessages(timeline, event, 'claude'),
293
+ []
294
+ );
295
+
296
+ assert.equal(messages.length, 1);
297
+ assert.equal(messages[0].content, 'Attached the spec for review.');
298
+ assert.equal(messages[0].contentBlocks.length, 1);
299
+ assert.equal(messages[0].contentBlocks[0].type, 'resource_link');
300
+ });
301
+
302
+ test('extractAcpSessionMetadataFromConversationEvents keeps latest modes and commands', () => {
303
+ const metadata = extractAcpSessionMetadataFromConversationEvents([
304
+ createConversationEvent({
305
+ kind: CONVERSATION_EVENT_KINDS.MODE_UPDATE,
306
+ provider: 'claude',
307
+ sessionId: 'session-meta',
308
+ payload: {
309
+ availableModes: [
310
+ { id: 'ask', name: 'Ask', description: 'Answer questions' },
311
+ { id: 'code', name: 'Code', description: 'Edit files' }
312
+ ],
313
+ currentModeId: 'ask'
314
+ }
315
+ }),
316
+ createConversationEvent({
317
+ kind: CONVERSATION_EVENT_KINDS.AVAILABLE_COMMANDS_UPDATE,
318
+ provider: 'claude',
319
+ sessionId: 'session-meta',
320
+ payload: {
321
+ availableCommands: [
322
+ { name: 'create_plan', description: 'Create a plan' }
323
+ ]
324
+ }
325
+ }),
326
+ createConversationEvent({
327
+ kind: CONVERSATION_EVENT_KINDS.MODE_UPDATE,
328
+ provider: 'claude',
329
+ sessionId: 'session-meta',
330
+ payload: {
331
+ currentModeId: 'code'
332
+ }
333
+ })
334
+ ]);
335
+
336
+ assert.equal(metadata.modeState.currentModeId, 'code');
337
+ assert.equal(metadata.modeState.availableModes.length, 2);
338
+ assert.equal(metadata.availableCommands[0].name, 'create_plan');
339
+ assert.equal(metadata.availableCommands[0].source, 'acp');
340
+ });
341
+
342
+ test('normalizeLegacyHistoryEntries keeps user and assistant content across providers', () => {
343
+ const rawMessages = [
344
+ {
345
+ timestamp: '2026-03-30T10:00:00.000Z',
346
+ message: {
347
+ role: 'user',
348
+ content: 'Please summarize the diff.'
349
+ }
350
+ },
351
+ {
352
+ timestamp: '2026-03-30T10:00:01.000Z',
353
+ message: {
354
+ role: 'assistant',
355
+ content: 'Summary ready.'
356
+ }
357
+ }
358
+ ];
359
+
360
+ for (const provider of ['claude', 'codex', 'gemini', 'opencode']) {
361
+ const events = normalizeLegacyHistoryEntries(rawMessages, provider, `session-${provider}`);
362
+ assert.equal(events[0].kind, CONVERSATION_EVENT_KINDS.USER_MESSAGE);
363
+ assert.equal(events[0].payload.text, 'Please summarize the diff.');
364
+ assert.equal(events[1].kind, CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_START);
365
+ assert.equal(events[2].kind, CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_DELTA);
366
+ assert.equal(events[2].payload.text, 'Summary ready.');
367
+ assert.equal(events[3].kind, CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_END);
368
+ }
369
+ });
370
+
371
+ test('normalizeRealtimePayloadToConversationEvents maps realtime payloads into conversation events', () => {
372
+ const queuedEvents = normalizeRealtimePayloadToConversationEvents({
373
+ type: 'session-created',
374
+ provider: 'claude',
375
+ sessionId: 'session-3',
376
+ modes: {
377
+ availableModes: [
378
+ { id: 'ask', name: 'Ask' }
379
+ ],
380
+ currentModeId: 'ask'
381
+ }
382
+ }, 'claude');
383
+ assert.equal(queuedEvents[0].kind, CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED);
384
+ assert.equal(queuedEvents[0].payload.state, 'queued');
385
+ assert.equal(queuedEvents[1].kind, CONVERSATION_EVENT_KINDS.MODE_UPDATE);
386
+ assert.equal(queuedEvents[1].payload.currentModeId, 'ask');
387
+
388
+ const deltaEvents = normalizeRealtimePayloadToConversationEvents({
389
+ type: 'claude-response',
390
+ provider: 'claude',
391
+ sessionId: 'session-3',
392
+ data: {
393
+ type: 'content_block_delta',
394
+ delta: {
395
+ text: 'stream chunk'
396
+ }
397
+ }
398
+ }, 'claude');
399
+
400
+ assert.equal(deltaEvents[0].kind, CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_START);
401
+ assert.equal(deltaEvents[1].kind, CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_DELTA);
402
+ assert.equal(deltaEvents[1].payload.text, 'stream chunk');
403
+ });