@axhub/genie 0.2.9 → 0.2.10

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 (96) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-CYCCsgwf.js +264 -0
  3. package/dist/assets/{ReviewApp-C9K--AQE.js → ReviewApp-0srHIXwb.js} +1 -1
  4. package/dist/assets/{_basePickBy-DR_8uFCo.js → _basePickBy-DVVb07UV.js} +1 -1
  5. package/dist/assets/{_baseUniq-D0njlQ_7.js → _baseUniq-BtbziL5G.js} +1 -1
  6. package/dist/assets/{arc-CKlr_Rec.js → arc-BsCC8yBD.js} +1 -1
  7. package/dist/assets/{architectureDiagram-2XIMDMQ5-BmO_uLUH.js → architectureDiagram-2XIMDMQ5-woFp6eNI.js} +1 -1
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-DhAeO-56.js → blockDiagram-WCTKOSBZ-ya8VAc2k.js} +1 -1
  9. package/dist/assets/{c4Diagram-IC4MRINW-C67kFoXx.js → c4Diagram-IC4MRINW-CY1dZmIZ.js} +1 -1
  10. package/dist/assets/channel-BMhScXFe.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB-mLLagvJi.js → chunk-4BX2VUAB-CR1lAd74.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-Lx-hOjlM.js → chunk-55IACEB6-CP98WcFC.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-Bt-XmVUV.js → chunk-FMBD7UC4-D9c7ijAB.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-Cya6gaDV.js → chunk-JSJVCQXG-DQAGYOn-.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-Bd7Ig6tF.js → chunk-KX2RTZJC-BbTXiDq7.js} +1 -1
  16. package/dist/assets/{chunk-NQ4KR5QH-5UAE0Vg-.js → chunk-NQ4KR5QH-BI6AX0dr.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-BAxZ8m7w.js → chunk-QZHKN3VN-DB3V2Ifo.js} +1 -1
  18. package/dist/assets/{chunk-WL4C6EOR-DjDPvUUP.js → chunk-WL4C6EOR-DhzTthv6.js} +1 -1
  19. package/dist/assets/classDiagram-VBA2DB6C-CMIxlWcT.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-CMIxlWcT.js +1 -0
  21. package/dist/assets/clone-BPqOt4r3.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-D-60XrkJ.js → cose-bilkent-S5V4N54A-BQ09ZE2j.js} +1 -1
  23. package/dist/assets/{dagre-KLK3FWXG-bqu3ZS4K.js → dagre-KLK3FWXG-Dc2ueD_R.js} +1 -1
  24. package/dist/assets/{diagram-E7M64L7V-BueeqoYm.js → diagram-E7M64L7V-DP-LsQoL.js} +1 -1
  25. package/dist/assets/{diagram-IFDJBPK2-D4fDv2E7.js → diagram-IFDJBPK2-Cg6r42cB.js} +1 -1
  26. package/dist/assets/{diagram-P4PSJMXO-WqipY3fN.js → diagram-P4PSJMXO-aHsfoUZE.js} +1 -1
  27. package/dist/assets/{erDiagram-INFDFZHY-D0oVnO-x.js → erDiagram-INFDFZHY-qBXJ4aAz.js} +1 -1
  28. package/dist/assets/{flowDiagram-PKNHOUZH-DzbGyxrr.js → flowDiagram-PKNHOUZH-D_13emJM.js} +1 -1
  29. package/dist/assets/{ganttDiagram-A5KZAMGK-BwhbbgCP.js → ganttDiagram-A5KZAMGK-BvIcOLwz.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-DZgAh_KM.js → gitGraphDiagram-K3NZZRJ6-ad0vvNcU.js} +1 -1
  31. package/dist/assets/{graph-DzKos-N0.js → graph-CeJCMjan.js} +1 -1
  32. package/dist/assets/{highlighted-body-TPN3WLV5-CKDMgz3X.js → highlighted-body-TPN3WLV5-B_novwSz.js} +1 -1
  33. package/dist/assets/index-C514cLyb.js +2 -0
  34. package/dist/assets/index-h1DBl_g3.css +1 -0
  35. package/dist/assets/{infoDiagram-LFFYTUFH-BFicZbTf.js → infoDiagram-LFFYTUFH-lOxAqb3m.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-PHBUUO56-CtihxDxl.js → ishikawaDiagram-PHBUUO56-DIr-51gj.js} +1 -1
  37. package/dist/assets/{journeyDiagram-4ABVD52K-Du00J8_d.js → journeyDiagram-4ABVD52K-CYcIW0ZU.js} +1 -1
  38. package/dist/assets/{kanban-definition-K7BYSVSG-BJi9S0iQ.js → kanban-definition-K7BYSVSG-C1ZK616a.js} +1 -1
  39. package/dist/assets/{layout-B80Sityu.js → layout-CI2RM-v6.js} +1 -1
  40. package/dist/assets/{linear-sRQLOf5H.js → linear-DE7bISck.js} +1 -1
  41. package/dist/assets/{mermaid-O7DHMXV3-CBuVs4eJ.js → mermaid-O7DHMXV3-XxAJo8EK.js} +6 -6
  42. package/dist/assets/{mindmap-definition-YRQLILUH-C5IL_xi-.js → mindmap-definition-YRQLILUH-Dz6EFjmn.js} +1 -1
  43. package/dist/assets/{pieDiagram-SKSYHLDU-CeTwlJ8z.js → pieDiagram-SKSYHLDU-DPpEzUed.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-337W2JSQ-COfUcLWt.js → quadrantDiagram-337W2JSQ-xdoXNet7.js} +1 -1
  45. package/dist/assets/{requirementDiagram-Z7DCOOCP-DSb-CJ5B.js → requirementDiagram-Z7DCOOCP-DUq8H3CL.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-WA2Y5GQK-8jtuVb45.js → sankeyDiagram-WA2Y5GQK-CmqEUxRu.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-2WXFIKYE-C2VpkMwA.js → sequenceDiagram-2WXFIKYE-DhtXRNiH.js} +1 -1
  48. package/dist/assets/{stateDiagram-RAJIS63D-fmwMqxxc.js → stateDiagram-RAJIS63D-Dj0HOlbN.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-FVOUBMTO-C9utf5gv.js +1 -0
  50. package/dist/assets/{timeline-definition-YZTLITO2-Dx1hP5lg.js → timeline-definition-YZTLITO2-DUuJzZB5.js} +1 -1
  51. package/dist/assets/{treemap-KZPCXAKY-CkLOdYCZ.js → treemap-KZPCXAKY-DpYBQ0qr.js} +1 -1
  52. package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
  53. package/dist/assets/{vennDiagram-LZ73GAT5-D6KWcnln.js → vennDiagram-LZ73GAT5-DpePUyOd.js} +1 -1
  54. package/dist/assets/{xychartDiagram-JWTSCODW-6fh6qmzN.js → xychartDiagram-JWTSCODW-Cfp1I4_U.js} +1 -1
  55. package/dist/index.html +3 -3
  56. package/package.json +6 -5
  57. package/server/acp-runtime/client.js +120 -14
  58. package/server/acp-runtime/index.js +54 -0
  59. package/server/acp-runtime/registry.js +2 -2
  60. package/server/acp-runtime/session-store.js +75 -1
  61. package/server/cli.js +32 -8
  62. package/server/database/db.js +20 -0
  63. package/server/external-agent/ws.js +477 -24
  64. package/server/index.js +78 -146
  65. package/server/lan-access/core.js +79 -0
  66. package/server/lan-access/state.js +102 -0
  67. package/server/middleware/auth.js +57 -14
  68. package/server/projects.js +423 -535
  69. package/server/routes/auth.js +24 -4
  70. package/server/routes/cli-auth.js +21 -25
  71. package/server/routes/codex.js +84 -298
  72. package/server/routes/commands.js +322 -407
  73. package/server/routes/lan-access.js +231 -0
  74. package/server/routes/projects.js +154 -158
  75. package/server/routes/session-core.js +13 -7
  76. package/server/routes/settings.js +113 -99
  77. package/server/session-core/eventStore.js +15 -2
  78. package/server/session-core/providerAdapters.js +28 -28
  79. package/server/session-core/sessionListMerge.js +47 -0
  80. package/shared/conversationEvents.js +96 -1
  81. package/shared/modelConstants.js +79 -99
  82. package/dist/assets/App-GBcTeeUS.js +0 -460
  83. package/dist/assets/channel-V3MBjKys.js +0 -1
  84. package/dist/assets/classDiagram-VBA2DB6C-C790yYiY.js +0 -1
  85. package/dist/assets/classDiagram-v2-RAHNMMFH-C790yYiY.js +0 -1
  86. package/dist/assets/clone-BbMGfZwt.js +0 -1
  87. package/dist/assets/index-DiQlHzGj.js +0 -2
  88. package/dist/assets/index-Drat2nB9.css +0 -1
  89. package/dist/assets/stateDiagram-v2-FVOUBMTO-9GGXVWrR.js +0 -1
  90. package/dist/assets/vendor-codemirror-BxPY6emf.js +0 -39
  91. package/server/routes/git.js +0 -1110
  92. package/server/routes/mcp-utils.js +0 -48
  93. package/server/routes/mcp.js +0 -536
  94. package/server/routes/taskmaster.js +0 -1963
  95. package/server/utils/mcp-detector.js +0 -198
  96. package/server/utils/taskmaster-websocket.js +0 -129
@@ -2,40 +2,50 @@ import express from 'express';
2
2
  import { apiKeysDb, credentialsDb } from '../database/db.js';
3
3
 
4
4
  const router = express.Router();
5
-
6
- // ===============================
7
- // API Keys Management
8
- // ===============================
9
-
10
- // Get all API keys for the authenticated user
11
- router.get('/api-keys', async (req, res) => {
5
+ const DISALLOWED_CREDENTIAL_TYPES = new Set(['github_token']);
6
+
7
+ function parseNumericId(rawValue) {
8
+ const parsed = Number.parseInt(String(rawValue || ''), 10);
9
+ return Number.isFinite(parsed) ? parsed : null;
10
+ }
11
+
12
+ function sanitizeApiKeyRecord(record) {
13
+ const visiblePrefix = String(record?.api_key || '').slice(0, 10);
14
+ return {
15
+ ...record,
16
+ api_key: visiblePrefix ? `${visiblePrefix}...` : ''
17
+ };
18
+ }
19
+
20
+ function badRequest(res, error) {
21
+ return res.status(400).json({ error });
22
+ }
23
+
24
+ function notFound(res, error) {
25
+ return res.status(404).json({ error });
26
+ }
27
+
28
+ router.get('/api-keys', (req, res) => {
12
29
  try {
13
- const apiKeys = apiKeysDb.getApiKeys(req.user.id);
14
- // Don't send the full API key in the list for security
15
- const sanitizedKeys = apiKeys.map(key => ({
16
- ...key,
17
- api_key: key.api_key.substring(0, 10) + '...'
18
- }));
19
- res.json({ apiKeys: sanitizedKeys });
30
+ const apiKeys = apiKeysDb.getApiKeys(req.user.id).map(sanitizeApiKeyRecord);
31
+ res.json({ apiKeys });
20
32
  } catch (error) {
21
33
  console.error('Error fetching API keys:', error);
22
34
  res.status(500).json({ error: 'Failed to fetch API keys' });
23
35
  }
24
36
  });
25
37
 
26
- // Create a new API key
27
- router.post('/api-keys', async (req, res) => {
28
- try {
29
- const { keyName } = req.body;
30
-
31
- if (!keyName || !keyName.trim()) {
32
- return res.status(400).json({ error: 'Key name is required' });
33
- }
38
+ router.post('/api-keys', (req, res) => {
39
+ const keyName = String(req.body?.keyName || '').trim();
40
+ if (!keyName) {
41
+ return badRequest(res, 'Key name is required');
42
+ }
34
43
 
35
- const result = apiKeysDb.createApiKey(req.user.id, keyName.trim());
44
+ try {
45
+ const apiKey = apiKeysDb.createApiKey(req.user.id, keyName);
36
46
  res.json({
37
47
  success: true,
38
- apiKey: result
48
+ apiKey
39
49
  });
40
50
  } catch (error) {
41
51
  console.error('Error creating API key:', error);
@@ -43,56 +53,54 @@ router.post('/api-keys', async (req, res) => {
43
53
  }
44
54
  });
45
55
 
46
- // Delete an API key
47
- router.delete('/api-keys/:keyId', async (req, res) => {
48
- try {
49
- const { keyId } = req.params;
50
- const success = apiKeysDb.deleteApiKey(req.user.id, parseInt(keyId));
56
+ router.delete('/api-keys/:keyId', (req, res) => {
57
+ const keyId = parseNumericId(req.params.keyId);
58
+ if (keyId === null) {
59
+ return badRequest(res, 'Invalid API key id');
60
+ }
51
61
 
52
- if (success) {
53
- res.json({ success: true });
54
- } else {
55
- res.status(404).json({ error: 'API key not found' });
62
+ try {
63
+ const removed = apiKeysDb.deleteApiKey(req.user.id, keyId);
64
+ if (!removed) {
65
+ return notFound(res, 'API key not found');
56
66
  }
67
+
68
+ res.json({ success: true });
57
69
  } catch (error) {
58
70
  console.error('Error deleting API key:', error);
59
71
  res.status(500).json({ error: 'Failed to delete API key' });
60
72
  }
61
73
  });
62
74
 
63
- // Toggle API key active status
64
- router.patch('/api-keys/:keyId/toggle', async (req, res) => {
65
- try {
66
- const { keyId } = req.params;
67
- const { isActive } = req.body;
75
+ router.patch('/api-keys/:keyId/toggle', (req, res) => {
76
+ const keyId = parseNumericId(req.params.keyId);
77
+ const isActive = req.body?.isActive;
68
78
 
69
- if (typeof isActive !== 'boolean') {
70
- return res.status(400).json({ error: 'isActive must be a boolean' });
71
- }
79
+ if (keyId === null) {
80
+ return badRequest(res, 'Invalid API key id');
81
+ }
72
82
 
73
- const success = apiKeysDb.toggleApiKey(req.user.id, parseInt(keyId), isActive);
83
+ if (typeof isActive !== 'boolean') {
84
+ return badRequest(res, 'isActive must be a boolean');
85
+ }
74
86
 
75
- if (success) {
76
- res.json({ success: true });
77
- } else {
78
- res.status(404).json({ error: 'API key not found' });
87
+ try {
88
+ const updated = apiKeysDb.toggleApiKey(req.user.id, keyId, isActive);
89
+ if (!updated) {
90
+ return notFound(res, 'API key not found');
79
91
  }
92
+
93
+ res.json({ success: true });
80
94
  } catch (error) {
81
95
  console.error('Error toggling API key:', error);
82
96
  res.status(500).json({ error: 'Failed to toggle API key' });
83
97
  }
84
98
  });
85
99
 
86
- // ===============================
87
- // Generic Credentials Management
88
- // ===============================
89
-
90
- // Get all credentials for the authenticated user (optionally filtered by type)
91
- router.get('/credentials', async (req, res) => {
100
+ router.get('/credentials', (req, res) => {
92
101
  try {
93
- const { type } = req.query;
94
- const credentials = credentialsDb.getCredentials(req.user.id, type || null);
95
- // Don't send the actual credential values for security
102
+ const credentialType = typeof req.query.type === 'string' ? req.query.type.trim() : null;
103
+ const credentials = credentialsDb.getCredentials(req.user.id, credentialType || null);
96
104
  res.json({ credentials });
97
105
  } catch (error) {
98
106
  console.error('Error fetching credentials:', error);
@@ -100,38 +108,40 @@ router.get('/credentials', async (req, res) => {
100
108
  }
101
109
  });
102
110
 
103
- // Create a new credential
104
- router.post('/credentials', async (req, res) => {
105
- try {
106
- const { credentialName, credentialType, credentialValue, description } = req.body;
111
+ router.post('/credentials', (req, res) => {
112
+ const credentialName = String(req.body?.credentialName || '').trim();
113
+ const credentialType = String(req.body?.credentialType || '').trim();
114
+ const credentialValue = String(req.body?.credentialValue || '').trim();
115
+ const description = String(req.body?.description || '').trim();
107
116
 
108
- if (!credentialName || !credentialName.trim()) {
109
- return res.status(400).json({ error: 'Credential name is required' });
110
- }
117
+ if (!credentialName) {
118
+ return badRequest(res, 'Credential name is required');
119
+ }
111
120
 
112
- if (!credentialType || !credentialType.trim()) {
113
- return res.status(400).json({ error: 'Credential type is required' });
114
- }
121
+ if (!credentialType) {
122
+ return badRequest(res, 'Credential type is required');
123
+ }
115
124
 
116
- if (!credentialValue || !credentialValue.trim()) {
117
- return res.status(400).json({ error: 'Credential value is required' });
118
- }
125
+ if (!credentialValue) {
126
+ return badRequest(res, 'Credential value is required');
127
+ }
119
128
 
120
- if (credentialType.trim() === 'github_token') {
121
- return res.status(400).json({ error: 'GitHub tokens are no longer supported.' });
122
- }
129
+ if (DISALLOWED_CREDENTIAL_TYPES.has(credentialType.toLowerCase())) {
130
+ return badRequest(res, 'GitHub tokens are no longer supported.');
131
+ }
123
132
 
124
- const result = credentialsDb.createCredential(
133
+ try {
134
+ const credential = credentialsDb.createCredential(
125
135
  req.user.id,
126
- credentialName.trim(),
127
- credentialType.trim(),
128
- credentialValue.trim(),
129
- description?.trim() || null
136
+ credentialName,
137
+ credentialType,
138
+ credentialValue,
139
+ description || null
130
140
  );
131
141
 
132
142
  res.json({
133
143
  success: true,
134
- credential: result
144
+ credential
135
145
  });
136
146
  } catch (error) {
137
147
  console.error('Error creating credential:', error);
@@ -139,40 +149,44 @@ router.post('/credentials', async (req, res) => {
139
149
  }
140
150
  });
141
151
 
142
- // Delete a credential
143
- router.delete('/credentials/:credentialId', async (req, res) => {
144
- try {
145
- const { credentialId } = req.params;
146
- const success = credentialsDb.deleteCredential(req.user.id, parseInt(credentialId));
152
+ router.delete('/credentials/:credentialId', (req, res) => {
153
+ const credentialId = parseNumericId(req.params.credentialId);
154
+ if (credentialId === null) {
155
+ return badRequest(res, 'Invalid credential id');
156
+ }
147
157
 
148
- if (success) {
149
- res.json({ success: true });
150
- } else {
151
- res.status(404).json({ error: 'Credential not found' });
158
+ try {
159
+ const removed = credentialsDb.deleteCredential(req.user.id, credentialId);
160
+ if (!removed) {
161
+ return notFound(res, 'Credential not found');
152
162
  }
163
+
164
+ res.json({ success: true });
153
165
  } catch (error) {
154
166
  console.error('Error deleting credential:', error);
155
167
  res.status(500).json({ error: 'Failed to delete credential' });
156
168
  }
157
169
  });
158
170
 
159
- // Toggle credential active status
160
- router.patch('/credentials/:credentialId/toggle', async (req, res) => {
161
- try {
162
- const { credentialId } = req.params;
163
- const { isActive } = req.body;
171
+ router.patch('/credentials/:credentialId/toggle', (req, res) => {
172
+ const credentialId = parseNumericId(req.params.credentialId);
173
+ const isActive = req.body?.isActive;
164
174
 
165
- if (typeof isActive !== 'boolean') {
166
- return res.status(400).json({ error: 'isActive must be a boolean' });
167
- }
175
+ if (credentialId === null) {
176
+ return badRequest(res, 'Invalid credential id');
177
+ }
168
178
 
169
- const success = credentialsDb.toggleCredential(req.user.id, parseInt(credentialId), isActive);
179
+ if (typeof isActive !== 'boolean') {
180
+ return badRequest(res, 'isActive must be a boolean');
181
+ }
170
182
 
171
- if (success) {
172
- res.json({ success: true });
173
- } else {
174
- res.status(404).json({ error: 'Credential not found' });
183
+ try {
184
+ const updated = credentialsDb.toggleCredential(req.user.id, credentialId, isActive);
185
+ if (!updated) {
186
+ return notFound(res, 'Credential not found');
175
187
  }
188
+
189
+ res.json({ success: true });
176
190
  } catch (error) {
177
191
  console.error('Error toggling credential:', error);
178
192
  res.status(500).json({ error: 'Failed to toggle credential' });
@@ -9,8 +9,18 @@ import {
9
9
  isConversationEvent
10
10
  } from '../../shared/conversationEvents.js';
11
11
 
12
- const MIRRORED_EVENT_STORE_ROOT = path.join(os.homedir(), '.axhub-genie', 'session-events');
12
+ function getMirroredEventStoreRoot() {
13
+ if (process.env.AXHUB_GENIE_SESSION_EVENTS_ROOT) {
14
+ return path.resolve(process.env.AXHUB_GENIE_SESSION_EVENTS_ROOT);
15
+ }
16
+
17
+ return path.join(os.homedir(), '.axhub-genie', 'session-events');
18
+ }
13
19
  const PERSISTED_EVENT_KINDS = new Set([
20
+ CONVERSATION_EVENT_KINDS.USER_MESSAGE,
21
+ CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_START,
22
+ CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_DELTA,
23
+ CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_END,
14
24
  CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED,
15
25
  CONVERSATION_EVENT_KINDS.ERROR,
16
26
  CONVERSATION_EVENT_KINDS.APPROVAL_REQUEST,
@@ -18,12 +28,15 @@ const PERSISTED_EVENT_KINDS = new Set([
18
28
  CONVERSATION_EVENT_KINDS.SYSTEM_NOTICE,
19
29
  CONVERSATION_EVENT_KINDS.MODE_UPDATE,
20
30
  CONVERSATION_EVENT_KINDS.AVAILABLE_COMMANDS_UPDATE,
31
+ CONVERSATION_EVENT_KINDS.CONFIG_OPTION_UPDATE,
32
+ CONVERSATION_EVENT_KINDS.SESSION_INFO_UPDATE,
33
+ CONVERSATION_EVENT_KINDS.USAGE_UPDATE,
21
34
  CONVERSATION_EVENT_KINDS.PLAN_UPDATE,
22
35
  CONVERSATION_EVENT_KINDS.ARTIFACT_CREATED
23
36
  ]);
24
37
 
25
38
  function getSessionEventFilePath(provider, sessionId) {
26
- return path.join(MIRRORED_EVENT_STORE_ROOT, String(provider || 'claude'), `${sessionId}.jsonl`);
39
+ return path.join(getMirroredEventStoreRoot(), String(provider || 'claude'), `${sessionId}.jsonl`);
27
40
  }
28
41
 
29
42
  function normalizePersistedEvents(events = []) {
@@ -14,12 +14,26 @@ import {
14
14
  findAcpSessionRecord,
15
15
  listAcpSessions
16
16
  } from '../acp-runtime/session-store.js';
17
+ import { mergeSessionLists } from './sessionListMerge.js';
17
18
 
18
19
  async function flattenLegacyMessages(result) {
19
20
  if (Array.isArray(result)) return result;
20
21
  return Array.isArray(result?.messages) ? result.messages : [];
21
22
  }
22
23
 
24
+ function hasReplayableTranscriptContent(events = []) {
25
+ return (Array.isArray(events) ? events : []).some((event) => (
26
+ event?.kind === 'user_message' ||
27
+ event?.kind === 'assistant_text_delta' ||
28
+ event?.kind === 'assistant_content_block' ||
29
+ event?.kind === 'reasoning_delta' ||
30
+ event?.kind === 'tool_call_start' ||
31
+ event?.kind === 'tool_result' ||
32
+ event?.kind === 'plan_update' ||
33
+ event?.kind === 'system_notice'
34
+ ));
35
+ }
36
+
23
37
  async function normalizeLegacyLoadResult(result, provider, sessionId) {
24
38
  const messages = await flattenLegacyMessages(result);
25
39
  const legacyEvents = normalizeLegacyHistoryEntries(messages, provider, sessionId);
@@ -49,34 +63,17 @@ function createEventLoadResult(events = [], source = 'acp') {
49
63
  };
50
64
  }
51
65
 
52
- function mergeSessionLists(legacySessions = [], acpSessions = []) {
53
- const merged = new Map();
54
-
55
- legacySessions.forEach((session) => {
56
- if (session?.id) {
57
- merged.set(session.id, session);
58
- }
59
- });
60
-
61
- acpSessions.forEach((session) => {
62
- if (session?.id) {
63
- merged.set(session.id, session);
64
- }
65
- });
66
-
67
- return Array.from(merged.values()).sort((left, right) => {
68
- const leftTime = new Date(left?.lastActivity || left?.updatedAt || left?.createdAt || 0).getTime();
69
- const rightTime = new Date(right?.lastActivity || right?.updatedAt || right?.createdAt || 0).getTime();
70
- return rightTime - leftTime;
71
- });
72
- }
73
-
74
66
  async function loadAcpEvents(provider, sessionId, nativeHistoryLoader = null) {
75
67
  const record = await findAcpSessionRecord(sessionId, provider);
76
68
  if (!record) {
77
69
  return null;
78
70
  }
79
71
 
72
+ const mirroredEvents = await readMirroredConversationEvents(provider, sessionId);
73
+ if (hasReplayableTranscriptContent(mirroredEvents)) {
74
+ return createEventLoadResult(mirroredEvents, 'acp');
75
+ }
76
+
80
77
  if (typeof nativeHistoryLoader === 'function') {
81
78
  try {
82
79
  const nativeResult = await nativeHistoryLoader();
@@ -93,8 +90,7 @@ async function loadAcpEvents(provider, sessionId, nativeHistoryLoader = null) {
93
90
  }
94
91
  }
95
92
 
96
- const events = await readMirroredConversationEvents(provider, sessionId);
97
- return createEventLoadResult(events, 'acp');
93
+ return createEventLoadResult(mirroredEvents, 'acp');
98
94
  }
99
95
 
100
96
  const PROVIDER_ADAPTERS = {
@@ -106,7 +102,8 @@ const PROVIDER_ADAPTERS = {
106
102
  ]);
107
103
  return mergeSessionLists(
108
104
  (result?.sessions || []).map((session) => ({ ...session, provider: 'claude', source: 'legacy' })),
109
- acpSessions
105
+ acpSessions,
106
+ { fallbackProvider: 'claude' }
110
107
  );
111
108
  },
112
109
  async loadEvents({ projectName, sessionId, limit = null, offset = 0 }) {
@@ -131,7 +128,8 @@ const PROVIDER_ADAPTERS = {
131
128
  ]);
132
129
  return mergeSessionLists(
133
130
  sessions.map((session) => ({ ...session, provider: 'codex', source: 'legacy' })),
134
- acpSessions
131
+ acpSessions,
132
+ { fallbackProvider: 'codex' }
135
133
  );
136
134
  },
137
135
  async loadEvents({ sessionId, limit = null, offset = 0 }) {
@@ -156,7 +154,8 @@ const PROVIDER_ADAPTERS = {
156
154
  ]);
157
155
  return mergeSessionLists(
158
156
  sessions.map((session) => ({ ...session, provider: 'gemini', source: 'legacy' })),
159
- acpSessions
157
+ acpSessions,
158
+ { fallbackProvider: 'gemini' }
160
159
  );
161
160
  },
162
161
  async loadEvents({ sessionId, limit = null, offset = 0 }) {
@@ -181,7 +180,8 @@ const PROVIDER_ADAPTERS = {
181
180
  ]);
182
181
  return mergeSessionLists(
183
182
  sessions.map((session) => ({ ...session, provider: 'opencode', source: 'legacy' })),
184
- acpSessions
183
+ acpSessions,
184
+ { fallbackProvider: 'opencode' }
185
185
  );
186
186
  },
187
187
  async loadEvents({ sessionId, limit = null, offset = 0 }) {
@@ -0,0 +1,47 @@
1
+ function getSessionActivityTime(session) {
2
+ return new Date(
3
+ session?.lastActivity
4
+ || session?.updatedAt
5
+ || session?.updated_at
6
+ || session?.createdAt
7
+ || session?.created_at
8
+ || 0
9
+ ).getTime();
10
+ }
11
+
12
+ function buildSessionMergeKey(session, fallbackProvider = '') {
13
+ const provider = String(session?.provider || session?.__provider || fallbackProvider || '').trim().toLowerCase();
14
+ const sessionId = String(session?.id || session?.sessionId || '').trim();
15
+
16
+ if (!sessionId) {
17
+ return null;
18
+ }
19
+
20
+ return `${provider}:${sessionId}`;
21
+ }
22
+
23
+ export function mergeSessionLists(legacySessions = [], acpSessions = [], options = {}) {
24
+ const { fallbackProvider = '' } = options;
25
+ const merged = new Map();
26
+
27
+ for (const session of legacySessions) {
28
+ const mergeKey = buildSessionMergeKey(session, fallbackProvider);
29
+ if (!mergeKey) {
30
+ continue;
31
+ }
32
+
33
+ merged.set(mergeKey, { ...session });
34
+ }
35
+
36
+ for (const session of acpSessions) {
37
+ const mergeKey = buildSessionMergeKey(session, fallbackProvider);
38
+ if (!mergeKey) {
39
+ continue;
40
+ }
41
+
42
+ const existing = merged.get(mergeKey) || {};
43
+ merged.set(mergeKey, { ...existing, ...session });
44
+ }
45
+
46
+ return Array.from(merged.values()).sort((left, right) => getSessionActivityTime(right) - getSessionActivityTime(left));
47
+ }
@@ -27,6 +27,9 @@ export const CONVERSATION_EVENT_KINDS = {
27
27
  PLAN_UPDATE: 'plan_update',
28
28
  MODE_UPDATE: 'mode_update',
29
29
  AVAILABLE_COMMANDS_UPDATE: 'available_commands_update',
30
+ CONFIG_OPTION_UPDATE: 'config_option_update',
31
+ SESSION_INFO_UPDATE: 'session_info_update',
32
+ USAGE_UPDATE: 'usage_update',
30
33
  APPROVAL_REQUEST: 'approval_request',
31
34
  APPROVAL_RESOLVED: 'approval_resolved',
32
35
  ARTIFACT_CREATED: 'artifact_created',
@@ -417,6 +420,71 @@ function normalizeAcpAvailableCommands(value) {
417
420
  }));
418
421
  }
419
422
 
423
+ function normalizeAcpConfigOptions(value) {
424
+ const configOptions = Array.isArray(value?.configOptions)
425
+ ? value.configOptions
426
+ : Array.isArray(value)
427
+ ? value
428
+ : [];
429
+
430
+ return configOptions
431
+ .filter((option) => option && typeof option === 'object' && String(option.key || '').trim())
432
+ .map((option) => ({
433
+ key: String(option.key).trim(),
434
+ description: typeof option.description === 'string' ? option.description : '',
435
+ value: cloneJsonValue(option.value),
436
+ schema: option.schema && typeof option.schema === 'object' ? cloneJsonValue(option.schema) : null
437
+ }));
438
+ }
439
+
440
+ function normalizeAcpSessionInfo(value) {
441
+ if (!value || typeof value !== 'object') {
442
+ return null;
443
+ }
444
+
445
+ const title = Object.prototype.hasOwnProperty.call(value, 'title')
446
+ ? (value.title == null ? null : String(value.title))
447
+ : undefined;
448
+ const updatedAt = Object.prototype.hasOwnProperty.call(value, 'updatedAt')
449
+ ? (value.updatedAt == null ? null : String(value.updatedAt))
450
+ : undefined;
451
+
452
+ if (title === undefined && updatedAt === undefined) {
453
+ return null;
454
+ }
455
+
456
+ return {
457
+ ...(title !== undefined ? { title } : {}),
458
+ ...(updatedAt !== undefined ? { updatedAt } : {})
459
+ };
460
+ }
461
+
462
+ function normalizeAcpTokenUsage(value) {
463
+ if (!value || typeof value !== 'object') {
464
+ return null;
465
+ }
466
+
467
+ const used = Number(value.used);
468
+ const total = Number(value.size);
469
+ if (!Number.isFinite(used) || used < 0 || !Number.isFinite(total) || total < 0) {
470
+ return null;
471
+ }
472
+
473
+ const safeUsed = Math.round(used);
474
+ const safeTotal = Math.round(total);
475
+ const percentage = safeTotal > 0
476
+ ? Math.min(100, Math.max(0, Math.round((safeUsed / safeTotal) * 100)))
477
+ : 0;
478
+
479
+ return {
480
+ used: safeUsed,
481
+ total: safeTotal,
482
+ percentage,
483
+ remaining: Math.max(0, safeTotal - safeUsed),
484
+ cost: value.cost && typeof value.cost === 'object' ? cloneJsonValue(value.cost) : null
485
+ };
486
+ }
487
+
420
488
  function mergeToolCallSnapshot(existingSnapshot = null, payload = {}) {
421
489
  const nextSnapshot = existingSnapshot && typeof existingSnapshot === 'object'
422
490
  ? { ...existingSnapshot }
@@ -993,6 +1061,9 @@ export function applyConversationEventToTimelineMessages(messages = [], event, s
993
1061
  case CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED:
994
1062
  case CONVERSATION_EVENT_KINDS.MODE_UPDATE:
995
1063
  case CONVERSATION_EVENT_KINDS.AVAILABLE_COMMANDS_UPDATE:
1064
+ case CONVERSATION_EVENT_KINDS.CONFIG_OPTION_UPDATE:
1065
+ case CONVERSATION_EVENT_KINDS.SESSION_INFO_UPDATE:
1066
+ case CONVERSATION_EVENT_KINDS.USAGE_UPDATE:
996
1067
  case CONVERSATION_EVENT_KINDS.ARTIFACT_CREATED:
997
1068
  default:
998
1069
  break;
@@ -1016,7 +1087,10 @@ export function conversationEventsToTimelineMessages(events = [], sessionProvide
1016
1087
  export function extractAcpSessionMetadataFromConversationEvents(events = []) {
1017
1088
  const metadata = {
1018
1089
  modeState: null,
1019
- availableCommands: []
1090
+ availableCommands: [],
1091
+ configOptions: [],
1092
+ sessionInfo: null,
1093
+ tokenUsage: null
1020
1094
  };
1021
1095
 
1022
1096
  for (const event of Array.isArray(events) ? events : []) {
@@ -1041,6 +1115,27 @@ export function extractAcpSessionMetadataFromConversationEvents(events = []) {
1041
1115
  if (event.kind === CONVERSATION_EVENT_KINDS.AVAILABLE_COMMANDS_UPDATE) {
1042
1116
  metadata.availableCommands = normalizeAcpAvailableCommands(event.payload || {});
1043
1117
  }
1118
+
1119
+ if (event.kind === CONVERSATION_EVENT_KINDS.CONFIG_OPTION_UPDATE) {
1120
+ metadata.configOptions = normalizeAcpConfigOptions(event.payload || {});
1121
+ }
1122
+
1123
+ if (event.kind === CONVERSATION_EVENT_KINDS.SESSION_INFO_UPDATE) {
1124
+ const nextSessionInfo = normalizeAcpSessionInfo(event.payload || {});
1125
+ if (nextSessionInfo) {
1126
+ metadata.sessionInfo = {
1127
+ ...(metadata.sessionInfo && typeof metadata.sessionInfo === 'object' ? metadata.sessionInfo : {}),
1128
+ ...nextSessionInfo
1129
+ };
1130
+ }
1131
+ }
1132
+
1133
+ if (event.kind === CONVERSATION_EVENT_KINDS.USAGE_UPDATE) {
1134
+ const nextTokenUsage = normalizeAcpTokenUsage(event.payload || {});
1135
+ if (nextTokenUsage) {
1136
+ metadata.tokenUsage = nextTokenUsage;
1137
+ }
1138
+ }
1044
1139
  }
1045
1140
 
1046
1141
  return metadata;