@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.
- package/dist/assets/App-BWSqiXAT.js +220 -0
- package/dist/assets/App-DrlLKa8f.css +1 -0
- package/dist/assets/ReviewApp-nz3mbArg.js +1 -0
- package/dist/assets/{_basePickBy-CFRQvihx.js → _basePickBy-C19AekOu.js} +1 -1
- package/dist/assets/{_baseUniq-Dhh8nCvs.js → _baseUniq-JsnevLw_.js} +1 -1
- package/dist/assets/{arc-DQ0v3dU4.js → arc-BLpcuBlf.js} +1 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +36 -0
- package/dist/assets/{blockDiagram-WCTKOSBZ-Bbxhj5KC.js → blockDiagram-WCTKOSBZ-DQBLwsUS.js} +3 -3
- package/dist/assets/c4Diagram-IC4MRINW-CGobwBIj.js +10 -0
- package/dist/assets/channel-DkFNxV_H.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-DlvtrM0q.js → chunk-4BX2VUAB-De63kbgc.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DJUSHyTa.js → chunk-55IACEB6-DtTDDdM9.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C6Ch-htf.js → chunk-FMBD7UC4-DHuwd8tw.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-DzQIht58.js → chunk-JSJVCQXG-BgytFtmO.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-C05jARMH.js → chunk-KX2RTZJC-nZdp86aN.js} +1 -1
- package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +220 -0
- package/dist/assets/{chunk-QZHKN3VN-jxti9HTX.js → chunk-QZHKN3VN-DvUQ3mnO.js} +1 -1
- package/dist/assets/chunk-WL4C6EOR-Dn7db_6t.js +189 -0
- package/dist/assets/classDiagram-VBA2DB6C-DtwCEe8S.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-DtwCEe8S.js +1 -0
- package/dist/assets/clone-C0lCEIEO.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-DD_nzqsz.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/{dagre-KLK3FWXG-DJ3dNSYk.js → dagre-KLK3FWXG-CHYIvW47.js} +1 -1
- package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +24 -0
- package/dist/assets/{diagram-IFDJBPK2-Da6K4aP-.js → diagram-IFDJBPK2-Dzsiln_C.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-vZZKB92A.js → diagram-P4PSJMXO-DKnGbUpE.js} +1 -1
- package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +70 -0
- package/dist/assets/{flowDiagram-PKNHOUZH-DUV13pHi.js → flowDiagram-PKNHOUZH-BAZ2-jKp.js} +4 -4
- package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +292 -0
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BZ5gW69I.js → gitGraphDiagram-K3NZZRJ6-BflpyjGy.js} +1 -1
- package/dist/assets/{graph-BbvHswRd.js → graph-suelaXFh.js} +1 -1
- package/dist/assets/highlighted-body-OFNGDK62-CZrBMazC.js +1 -0
- package/dist/assets/index-B01NxbUv.css +1 -0
- package/dist/assets/index-DW5pGgQ_.js +2 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-8auUIPKW.js → infoDiagram-LFFYTUFH-pfD1FA3p.js} +1 -1
- package/dist/assets/ishikawaDiagram-PHBUUO56-ndm9snwO.js +70 -0
- package/dist/assets/journeyDiagram-4ABVD52K-HgF2t7z5.js +139 -0
- package/dist/assets/{kanban-definition-K7BYSVSG-Bappd2YO.js → kanban-definition-K7BYSVSG-FWinmur1.js} +5 -5
- package/dist/assets/{layout-BmbfFZKy.js → layout-vcz43XvZ.js} +1 -1
- package/dist/assets/{linear-WZnF-PT6.js → linear-le4gc0vx.js} +1 -1
- package/dist/assets/mermaid-GHXKKRXX-CK8m3lad.js +870 -0
- package/dist/assets/mindmap-definition-YRQLILUH-CNq9SKj4.js +68 -0
- package/dist/assets/{pieDiagram-SKSYHLDU-uxjlAy1t.js → pieDiagram-SKSYHLDU-C7PKDh3b.js} +2 -2
- package/dist/assets/quadrantDiagram-337W2JSQ-B7FnztNO.js +7 -0
- package/dist/assets/requirementDiagram-Z7DCOOCP-Bl_BM2Th.js +73 -0
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-2-FHHM-R.js → sankeyDiagram-WA2Y5GQK-4gulcOP4.js} +3 -3
- package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +145 -0
- package/dist/assets/{stateDiagram-RAJIS63D-DoW8U53H.js → stateDiagram-RAJIS63D-CB4Vl7qM.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-C85ucl39.js +1 -0
- package/dist/assets/timeline-definition-YZTLITO2-BPGKhi7f.js +61 -0
- package/dist/assets/{treemap-KZPCXAKY-ajdAP-72.js → treemap-KZPCXAKY-DZSEE6Hz.js} +58 -58
- package/dist/assets/vendor-codemirror-CyOKkaQZ.js +31 -0
- package/dist/assets/vendor-react-CP4yFTs7.js +8 -0
- package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
- package/dist/assets/{vennDiagram-LZ73GAT5-C9If0AT0.js → vennDiagram-LZ73GAT5-8E_G06fI.js} +4 -4
- package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +7 -0
- package/dist/favicon.png +0 -0
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/index.html +4 -5
- package/dist/logo-128.png +0 -0
- package/dist/logo-256.png +0 -0
- package/dist/logo-32.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-64.png +0 -0
- package/package.json +2 -1
- package/server/_legacy-providers/README.md +30 -0
- package/server/_legacy-providers/claude-sdk.js +956 -0
- package/server/_legacy-providers/gemini-cli.js +368 -0
- package/server/_legacy-providers/openai-codex.js +705 -0
- package/server/_legacy-providers/opencode-cli.js +674 -0
- package/server/acp-runtime/client.js +1805 -0
- package/server/acp-runtime/client.test.js +688 -0
- package/server/acp-runtime/index.js +419 -0
- package/server/acp-runtime/registry.js +45 -0
- package/server/acp-runtime/session-store.js +254 -0
- package/server/acp-runtime/session-store.test.js +89 -0
- package/server/channels/runtime/AgentRuntimeAdapter.js +21 -70
- package/server/claude-sdk.js +24 -944
- package/server/cli.js +11 -5
- package/server/external-agent/service.js +77 -63
- package/server/gemini-cli.js +23 -360
- package/server/index.js +54 -46
- package/server/openai-codex.js +24 -698
- package/server/opencode-cli.js +70 -640
- package/server/routes/agent.js +2 -0
- package/server/routes/codex.js +5 -5
- package/server/routes/git.js +3 -20
- package/server/routes/mcp.js +18 -34
- package/server/routes/session-core.js +44 -10
- package/server/session-core/abortSession.js +2 -18
- package/server/session-core/eventStore.js +5 -1
- package/server/session-core/providerAdapters.js +98 -10
- package/server/session-core/providerDiscovery.js +2 -2
- package/server/session-core/runtimeState.js +16 -17
- package/server/session-core/runtimeWriter.js +19 -12
- package/server/utils/codexPath.js +3 -1
- package/server/utils/spawnCommand.js +7 -0
- package/shared/conversationEvents.js +347 -10
- package/shared/conversationEvents.test.js +403 -0
- package/dist/assets/App-BxazfNJn.js +0 -484
- package/dist/assets/App-qxJ8_QYu.css +0 -32
- package/dist/assets/ReviewApp-CsqTAlGU.js +0 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-DmUHdvQH.js +0 -36
- package/dist/assets/c4Diagram-IC4MRINW-BOivDlQU.js +0 -10
- package/dist/assets/channel-Cj8xVD0X.js +0 -1
- package/dist/assets/chunk-NQ4KR5QH-Ci-n7jfu.js +0 -220
- package/dist/assets/chunk-WL4C6EOR-C559Mk71.js +0 -189
- package/dist/assets/classDiagram-VBA2DB6C-CI2zklxw.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-CI2zklxw.js +0 -1
- package/dist/assets/clone-BEVqubrI.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-DNO9ncXL.js +0 -1
- package/dist/assets/cytoscape.esm-2ZfV8NB5.js +0 -331
- package/dist/assets/diagram-E7M64L7V-Ba_LGLun.js +0 -24
- package/dist/assets/erDiagram-INFDFZHY-Csb8dFdP.js +0 -70
- package/dist/assets/ganttDiagram-A5KZAMGK-B5Kv9Wfz.js +0 -292
- package/dist/assets/highlighted-body-TPN3WLV5-DZJajMGm.js +0 -1
- package/dist/assets/index-BFX9lxRB.css +0 -1
- package/dist/assets/index-BiErUGrv.js +0 -2
- package/dist/assets/ishikawaDiagram-PHBUUO56-JmsNlo2I.js +0 -70
- package/dist/assets/journeyDiagram-4ABVD52K-Cuudv7Vv.js +0 -139
- package/dist/assets/mermaid-O7DHMXV3-D-2fQRvw.js +0 -988
- package/dist/assets/mindmap-definition-YRQLILUH-BQHnzzud.js +0 -68
- package/dist/assets/quadrantDiagram-337W2JSQ-DpwZU-f_.js +0 -7
- package/dist/assets/requirementDiagram-Z7DCOOCP-C_9ClOWm.js +0 -73
- package/dist/assets/sequenceDiagram-2WXFIKYE-egns-0XI.js +0 -145
- package/dist/assets/stateDiagram-v2-FVOUBMTO-BoFZZ4Ds.js +0 -1
- package/dist/assets/timeline-definition-YZTLITO2-chPa8ppH.js +0 -61
- package/dist/assets/vendor-codemirror-Dz7_EqNA.js +0 -39
- package/dist/assets/vendor-react-Cpt6D04s.js +0 -59
- package/dist/assets/vendor-xterm-DfaPXD3y.js +0 -66
- package/dist/assets/xychartDiagram-JWTSCODW-DD42U6Or.js +0 -7
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { AcpClient, normalizeTerminalCommandForSpawn, splitCommandLine } from './client.js';
|
|
5
|
+
|
|
6
|
+
function createWriter() {
|
|
7
|
+
const messages = [];
|
|
8
|
+
return {
|
|
9
|
+
messages,
|
|
10
|
+
send(payload) {
|
|
11
|
+
messages.push(payload);
|
|
12
|
+
},
|
|
13
|
+
setSessionId() {},
|
|
14
|
+
getSessionId() {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getConversationEvents(writer) {
|
|
21
|
+
return writer.messages
|
|
22
|
+
.filter((payload) => payload.type === 'conversation-event')
|
|
23
|
+
.map((payload) => payload.event);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('splitCommandLine keeps quoted arguments together', () => {
|
|
27
|
+
const parsed = splitCommandLine('npx -y "pkg with spaces" --flag=\'value one\'');
|
|
28
|
+
assert.equal(parsed.command, 'npx');
|
|
29
|
+
assert.deepEqual(parsed.args, ['-y', 'pkg with spaces', '--flag=value one']);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('normalizeTerminalCommandForSpawn disables zsh nomatch for inline commands', () => {
|
|
33
|
+
const normalized = normalizeTerminalCommandForSpawn('/bin/zsh', ['-lc', 'curl -sS https://wttr.in/Guangzhou?format=3']);
|
|
34
|
+
assert.equal(normalized.command, '/bin/zsh');
|
|
35
|
+
assert.deepEqual(normalized.args, ['-lc', 'setopt no_nomatch; curl -sS https://wttr.in/Guangzhou?format=3']);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('normalizeTerminalCommandForSpawn leaves non-zsh commands unchanged', () => {
|
|
39
|
+
const normalized = normalizeTerminalCommandForSpawn('/bin/bash', ['-lc', 'echo hello']);
|
|
40
|
+
assert.equal(normalized.command, '/bin/bash');
|
|
41
|
+
assert.deepEqual(normalized.args, ['-lc', 'echo hello']);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('AcpClient streams assistant chunks into conversation events', async () => {
|
|
45
|
+
const writer = createWriter();
|
|
46
|
+
const client = new AcpClient({
|
|
47
|
+
agentKey: 'claude',
|
|
48
|
+
writer,
|
|
49
|
+
projectPath: process.cwd()
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
client.sessionId = 'session-5';
|
|
53
|
+
|
|
54
|
+
await client.handleSessionUpdate({
|
|
55
|
+
sessionId: 'session-5',
|
|
56
|
+
update: {
|
|
57
|
+
sessionUpdate: 'agent_message_chunk',
|
|
58
|
+
content: {
|
|
59
|
+
type: 'text',
|
|
60
|
+
text: 'Hello ACP'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
client.flushOpenTextStreams('2026-03-30T12:00:00.000Z');
|
|
66
|
+
client.resetTurnState();
|
|
67
|
+
|
|
68
|
+
await client.handleSessionUpdate({
|
|
69
|
+
sessionId: 'session-5',
|
|
70
|
+
update: {
|
|
71
|
+
sessionUpdate: 'agent_message_chunk',
|
|
72
|
+
content: {
|
|
73
|
+
type: 'text',
|
|
74
|
+
text: 'Second turn'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
client.flushOpenTextStreams('2026-03-30T12:00:01.000Z');
|
|
80
|
+
|
|
81
|
+
const events = getConversationEvents(writer);
|
|
82
|
+
const kinds = events.map((event) => event.kind);
|
|
83
|
+
const assistantStartEvents = events.filter((event) => event.kind === 'assistant_text_start');
|
|
84
|
+
|
|
85
|
+
assert.deepEqual(kinds, [
|
|
86
|
+
'assistant_text_start',
|
|
87
|
+
'assistant_text_delta',
|
|
88
|
+
'assistant_text_end',
|
|
89
|
+
'assistant_text_start',
|
|
90
|
+
'assistant_text_delta',
|
|
91
|
+
'assistant_text_end'
|
|
92
|
+
]);
|
|
93
|
+
assert.equal(assistantStartEvents.length, 2);
|
|
94
|
+
assert.notEqual(assistantStartEvents[0].payload.messageId, assistantStartEvents[1].payload.messageId);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('AcpClient maps reasoning chunks and end_turn into reasoning events', async () => {
|
|
98
|
+
const writer = createWriter();
|
|
99
|
+
const client = new AcpClient({
|
|
100
|
+
agentKey: 'claude',
|
|
101
|
+
writer,
|
|
102
|
+
projectPath: process.cwd()
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
client.sessionId = 'session-reasoning';
|
|
106
|
+
|
|
107
|
+
await client.handleSessionUpdate({
|
|
108
|
+
sessionId: 'session-reasoning',
|
|
109
|
+
update: {
|
|
110
|
+
sessionUpdate: 'agent_thought_chunk',
|
|
111
|
+
content: {
|
|
112
|
+
type: 'text',
|
|
113
|
+
text: 'Thinking...'
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await client.handleSessionUpdate({
|
|
119
|
+
sessionId: 'session-reasoning',
|
|
120
|
+
update: {
|
|
121
|
+
sessionUpdate: 'end_turn'
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const events = getConversationEvents(writer);
|
|
126
|
+
assert.deepEqual(
|
|
127
|
+
events.map((event) => event.kind),
|
|
128
|
+
['reasoning_start', 'reasoning_delta', 'reasoning_end']
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('AcpClient maps tool_call updates into tool lifecycle events', async () => {
|
|
133
|
+
const writer = createWriter();
|
|
134
|
+
const client = new AcpClient({
|
|
135
|
+
agentKey: 'claude',
|
|
136
|
+
writer,
|
|
137
|
+
projectPath: process.cwd()
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
client.sessionId = 'session-tool';
|
|
141
|
+
|
|
142
|
+
await client.handleSessionUpdate({
|
|
143
|
+
sessionId: 'session-tool',
|
|
144
|
+
update: {
|
|
145
|
+
sessionUpdate: 'tool_call',
|
|
146
|
+
toolCallId: 'tool-123',
|
|
147
|
+
title: 'Read file',
|
|
148
|
+
kind: 'read',
|
|
149
|
+
status: 'in_progress',
|
|
150
|
+
rawInput: { path: 'README.md' },
|
|
151
|
+
locations: [{ path: 'README.md', line: 1 }]
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await client.handleSessionUpdate({
|
|
156
|
+
sessionId: 'session-tool',
|
|
157
|
+
update: {
|
|
158
|
+
sessionUpdate: 'tool_call_update',
|
|
159
|
+
toolCallId: 'tool-123',
|
|
160
|
+
title: 'Read file',
|
|
161
|
+
kind: 'read',
|
|
162
|
+
status: 'completed',
|
|
163
|
+
rawInput: { path: 'README.md' },
|
|
164
|
+
rawOutput: 'file contents',
|
|
165
|
+
content: [
|
|
166
|
+
{
|
|
167
|
+
type: 'content',
|
|
168
|
+
content: {
|
|
169
|
+
type: 'text',
|
|
170
|
+
text: 'Opened README.md'
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const events = getConversationEvents(writer);
|
|
178
|
+
assert.deepEqual(
|
|
179
|
+
events.map((event) => event.kind),
|
|
180
|
+
['tool_call_start', 'tool_call_input', 'tool_call_input', 'tool_call_end', 'tool_result']
|
|
181
|
+
);
|
|
182
|
+
assert.equal(events[4].payload.content, 'file contents');
|
|
183
|
+
assert.equal(events[0].payload.kind, 'read');
|
|
184
|
+
assert.equal(events[1].payload.locations[0].path, 'README.md');
|
|
185
|
+
assert.equal(events[4].payload.contentBlocks[0].type, 'content');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('AcpClient splits assistant text around tool calls within the same turn', async () => {
|
|
189
|
+
const writer = createWriter();
|
|
190
|
+
const client = new AcpClient({
|
|
191
|
+
agentKey: 'claude',
|
|
192
|
+
writer,
|
|
193
|
+
projectPath: process.cwd()
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
client.sessionId = 'session-tool-boundary';
|
|
197
|
+
|
|
198
|
+
await client.handleSessionUpdate({
|
|
199
|
+
sessionId: 'session-tool-boundary',
|
|
200
|
+
update: {
|
|
201
|
+
sessionUpdate: 'agent_message_chunk',
|
|
202
|
+
content: {
|
|
203
|
+
type: 'text',
|
|
204
|
+
text: 'First, I will inspect the file.'
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await client.handleSessionUpdate({
|
|
210
|
+
sessionId: 'session-tool-boundary',
|
|
211
|
+
update: {
|
|
212
|
+
sessionUpdate: 'tool_call',
|
|
213
|
+
toolCallId: 'tool-boundary-1',
|
|
214
|
+
title: 'Read file',
|
|
215
|
+
kind: 'read',
|
|
216
|
+
status: 'in_progress',
|
|
217
|
+
rawInput: { path: 'README.md' }
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await client.handleSessionUpdate({
|
|
222
|
+
sessionId: 'session-tool-boundary',
|
|
223
|
+
update: {
|
|
224
|
+
sessionUpdate: 'tool_call_update',
|
|
225
|
+
toolCallId: 'tool-boundary-1',
|
|
226
|
+
title: 'Read file',
|
|
227
|
+
kind: 'read',
|
|
228
|
+
status: 'completed',
|
|
229
|
+
rawInput: { path: 'README.md' },
|
|
230
|
+
rawOutput: 'done'
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await client.handleSessionUpdate({
|
|
235
|
+
sessionId: 'session-tool-boundary',
|
|
236
|
+
update: {
|
|
237
|
+
sessionUpdate: 'agent_message_chunk',
|
|
238
|
+
content: {
|
|
239
|
+
type: 'text',
|
|
240
|
+
text: 'Here is what I found.'
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
client.flushOpenTextStreams('2026-03-30T12:00:03.000Z');
|
|
246
|
+
|
|
247
|
+
const events = getConversationEvents(writer);
|
|
248
|
+
const assistantStarts = events.filter((event) => event.kind === 'assistant_text_start');
|
|
249
|
+
const assistantEnds = events.filter((event) => event.kind === 'assistant_text_end');
|
|
250
|
+
|
|
251
|
+
assert.deepEqual(
|
|
252
|
+
events.map((event) => event.kind),
|
|
253
|
+
[
|
|
254
|
+
'assistant_text_start',
|
|
255
|
+
'assistant_text_delta',
|
|
256
|
+
'assistant_text_end',
|
|
257
|
+
'tool_call_start',
|
|
258
|
+
'tool_call_input',
|
|
259
|
+
'tool_call_input',
|
|
260
|
+
'tool_call_end',
|
|
261
|
+
'tool_result',
|
|
262
|
+
'assistant_text_start',
|
|
263
|
+
'assistant_text_delta',
|
|
264
|
+
'assistant_text_end'
|
|
265
|
+
]
|
|
266
|
+
);
|
|
267
|
+
assert.equal(assistantStarts.length, 2);
|
|
268
|
+
assert.equal(assistantEnds.length, 2);
|
|
269
|
+
assert.notEqual(assistantStarts[0].payload.messageId, assistantStarts[1].payload.messageId);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('AcpClient waits for explicit permission decisions in default mode', async () => {
|
|
273
|
+
const writer = createWriter();
|
|
274
|
+
const client = new AcpClient({
|
|
275
|
+
agentKey: 'claude',
|
|
276
|
+
writer,
|
|
277
|
+
projectPath: process.cwd(),
|
|
278
|
+
permissionMode: 'default'
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
client.sessionId = 'session-6';
|
|
282
|
+
|
|
283
|
+
const permissionPromise = client.handleRequestPermission({
|
|
284
|
+
sessionId: 'session-6',
|
|
285
|
+
toolCall: {
|
|
286
|
+
toolCallId: 'tool-1',
|
|
287
|
+
title: 'Write file',
|
|
288
|
+
kind: 'edit',
|
|
289
|
+
rawInput: {
|
|
290
|
+
path: 'README.md'
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
options: [
|
|
294
|
+
{
|
|
295
|
+
optionId: 'allow-once',
|
|
296
|
+
kind: 'allow_once',
|
|
297
|
+
name: 'Allow once'
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
optionId: 'reject-once',
|
|
301
|
+
kind: 'reject_once',
|
|
302
|
+
name: 'Reject once'
|
|
303
|
+
}
|
|
304
|
+
]
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const requestPayload = writer.messages.find((payload) => payload.type === 'conversation-event' && payload.event.kind === 'approval_request');
|
|
308
|
+
assert.ok(requestPayload);
|
|
309
|
+
|
|
310
|
+
const requestId = requestPayload.event.payload.requestId;
|
|
311
|
+
assert.ok(AcpClient.resolvePermissionRequest(requestId, { allow: true }));
|
|
312
|
+
|
|
313
|
+
const response = await permissionPromise;
|
|
314
|
+
assert.equal(response.outcome.outcome, 'selected');
|
|
315
|
+
assert.equal(response.outcome.optionId, 'allow-once');
|
|
316
|
+
|
|
317
|
+
const resolutionPayload = writer.messages.find((payload) => payload.type === 'conversation-event' && payload.event.kind === 'approval_resolved');
|
|
318
|
+
assert.ok(resolutionPayload);
|
|
319
|
+
assert.equal(resolutionPayload.event.payload.status, 'approved');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('AcpClient accepts explicit ACP option selections and emits rich permission payload', async () => {
|
|
323
|
+
const writer = createWriter();
|
|
324
|
+
const client = new AcpClient({
|
|
325
|
+
agentKey: 'claude',
|
|
326
|
+
writer,
|
|
327
|
+
projectPath: process.cwd(),
|
|
328
|
+
permissionMode: 'default'
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
client.sessionId = 'session-6b';
|
|
332
|
+
|
|
333
|
+
const permissionPromise = client.handleRequestPermission({
|
|
334
|
+
sessionId: 'session-6b',
|
|
335
|
+
toolCall: {
|
|
336
|
+
toolCallId: 'tool-3',
|
|
337
|
+
title: 'Edit file',
|
|
338
|
+
kind: 'edit',
|
|
339
|
+
rawInput: {
|
|
340
|
+
path: 'README.md'
|
|
341
|
+
},
|
|
342
|
+
locations: [{ path: 'README.md', line: 9 }]
|
|
343
|
+
},
|
|
344
|
+
options: [
|
|
345
|
+
{
|
|
346
|
+
optionId: 'allow-always',
|
|
347
|
+
kind: 'allow_always',
|
|
348
|
+
name: 'Always allow'
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
optionId: 'reject-once',
|
|
352
|
+
kind: 'reject_once',
|
|
353
|
+
name: 'Reject once'
|
|
354
|
+
}
|
|
355
|
+
]
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const requestPayload = writer.messages.find((payload) => payload.type === 'conversation-event' && payload.event.kind === 'approval_request');
|
|
359
|
+
assert.ok(requestPayload);
|
|
360
|
+
assert.equal(requestPayload.event.payload.toolCall.kind, 'edit');
|
|
361
|
+
assert.equal(requestPayload.event.payload.options[0].optionId, 'allow-always');
|
|
362
|
+
|
|
363
|
+
assert.ok(AcpClient.resolvePermissionRequest(requestPayload.event.payload.requestId, {
|
|
364
|
+
outcome: {
|
|
365
|
+
outcome: 'selected',
|
|
366
|
+
optionId: 'allow-always'
|
|
367
|
+
}
|
|
368
|
+
}));
|
|
369
|
+
|
|
370
|
+
const response = await permissionPromise;
|
|
371
|
+
assert.equal(response.outcome.outcome, 'selected');
|
|
372
|
+
assert.equal(response.outcome.optionId, 'allow-always');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('AcpClient maps plan updates into conversation events', async () => {
|
|
376
|
+
const writer = createWriter();
|
|
377
|
+
const client = new AcpClient({
|
|
378
|
+
agentKey: 'claude',
|
|
379
|
+
writer,
|
|
380
|
+
projectPath: process.cwd()
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
client.sessionId = 'session-plan';
|
|
384
|
+
|
|
385
|
+
await client.handleSessionUpdate({
|
|
386
|
+
sessionId: 'session-plan',
|
|
387
|
+
update: {
|
|
388
|
+
sessionUpdate: 'plan',
|
|
389
|
+
entries: [
|
|
390
|
+
{ content: 'Inspect adapter', status: 'completed', priority: 'medium' },
|
|
391
|
+
{ content: 'Render plan UI', status: 'in_progress', priority: 'high' }
|
|
392
|
+
]
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const events = getConversationEvents(writer);
|
|
397
|
+
assert.equal(events.length, 1);
|
|
398
|
+
assert.equal(events[0].kind, 'plan_update');
|
|
399
|
+
assert.equal(events[0].payload.entries.length, 2);
|
|
400
|
+
assert.equal(events[0].payload.entries[1].priority, 'high');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('AcpClient maps mode and available commands updates into conversation events', async () => {
|
|
404
|
+
const writer = createWriter();
|
|
405
|
+
const client = new AcpClient({
|
|
406
|
+
agentKey: 'claude',
|
|
407
|
+
writer,
|
|
408
|
+
projectPath: process.cwd()
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
client.sessionId = 'session-mode';
|
|
412
|
+
|
|
413
|
+
await client.handleSessionUpdate({
|
|
414
|
+
sessionId: 'session-mode',
|
|
415
|
+
update: {
|
|
416
|
+
sessionUpdate: 'current_mode_update',
|
|
417
|
+
currentModeId: 'code'
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await client.handleSessionUpdate({
|
|
422
|
+
sessionId: 'session-mode',
|
|
423
|
+
update: {
|
|
424
|
+
sessionUpdate: 'available_commands_update',
|
|
425
|
+
availableCommands: [
|
|
426
|
+
{
|
|
427
|
+
name: 'create_plan',
|
|
428
|
+
description: 'Create a plan'
|
|
429
|
+
}
|
|
430
|
+
]
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const events = getConversationEvents(writer);
|
|
435
|
+
assert.equal(events[0].kind, 'mode_update');
|
|
436
|
+
assert.equal(events[0].payload.currentModeId, 'code');
|
|
437
|
+
assert.equal(events[1].kind, 'available_commands_update');
|
|
438
|
+
assert.equal(events[1].payload.availableCommands[0].name, 'create_plan');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('AcpClient maps non-text assistant chunks into content block events', async () => {
|
|
442
|
+
const writer = createWriter();
|
|
443
|
+
const client = new AcpClient({
|
|
444
|
+
agentKey: 'claude',
|
|
445
|
+
writer,
|
|
446
|
+
projectPath: process.cwd()
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
client.sessionId = 'session-content-blocks';
|
|
450
|
+
|
|
451
|
+
await client.handleSessionUpdate({
|
|
452
|
+
sessionId: 'session-content-blocks',
|
|
453
|
+
update: {
|
|
454
|
+
sessionUpdate: 'agent_message_chunk',
|
|
455
|
+
content: {
|
|
456
|
+
type: 'resource_link',
|
|
457
|
+
uri: '/tmp/spec.md',
|
|
458
|
+
name: 'spec.md'
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await client.handleSessionUpdate({
|
|
464
|
+
sessionId: 'session-content-blocks',
|
|
465
|
+
update: {
|
|
466
|
+
sessionUpdate: 'agent_message_chunk',
|
|
467
|
+
content: {
|
|
468
|
+
type: 'text',
|
|
469
|
+
text: 'See the attached spec.'
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
client.flushOpenTextStreams('2026-03-30T12:00:02.000Z');
|
|
475
|
+
|
|
476
|
+
const events = getConversationEvents(writer);
|
|
477
|
+
assert.equal(events[0].kind, 'assistant_content_block');
|
|
478
|
+
assert.equal(events[0].payload.contentBlock.type, 'resource_link');
|
|
479
|
+
assert.equal(events[1].kind, 'assistant_text_start');
|
|
480
|
+
assert.equal(events[1].payload.messageId, events[0].payload.messageId);
|
|
481
|
+
assert.equal(events[2].kind, 'assistant_text_delta');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('AcpClient auto-approves editable tools in acceptEdits mode', async () => {
|
|
485
|
+
const writer = createWriter();
|
|
486
|
+
const client = new AcpClient({
|
|
487
|
+
agentKey: 'claude',
|
|
488
|
+
writer,
|
|
489
|
+
projectPath: process.cwd(),
|
|
490
|
+
permissionMode: 'acceptEdits'
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
client.sessionId = 'session-7';
|
|
494
|
+
|
|
495
|
+
const response = await client.handleRequestPermission({
|
|
496
|
+
sessionId: 'session-7',
|
|
497
|
+
toolCall: {
|
|
498
|
+
toolCallId: 'tool-2',
|
|
499
|
+
title: 'Edit file',
|
|
500
|
+
kind: 'edit'
|
|
501
|
+
},
|
|
502
|
+
options: [
|
|
503
|
+
{
|
|
504
|
+
optionId: 'allow-once',
|
|
505
|
+
kind: 'allow_once',
|
|
506
|
+
name: 'Allow once'
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
optionId: 'reject-once',
|
|
510
|
+
kind: 'reject_once',
|
|
511
|
+
name: 'Reject once'
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
assert.equal(response.outcome.outcome, 'selected');
|
|
517
|
+
assert.equal(response.outcome.optionId, 'allow-once');
|
|
518
|
+
assert.equal(writer.messages.length, 0);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test('AcpClient auto-approves all tools in bypassPermissions mode', async () => {
|
|
522
|
+
const writer = createWriter();
|
|
523
|
+
const client = new AcpClient({
|
|
524
|
+
agentKey: 'claude',
|
|
525
|
+
writer,
|
|
526
|
+
projectPath: process.cwd(),
|
|
527
|
+
permissionMode: 'bypassPermissions'
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
client.sessionId = 'session-8';
|
|
531
|
+
|
|
532
|
+
const response = await client.handleRequestPermission({
|
|
533
|
+
sessionId: 'session-8',
|
|
534
|
+
toolCall: {
|
|
535
|
+
toolCallId: 'tool-3',
|
|
536
|
+
title: 'Run shell command',
|
|
537
|
+
kind: 'execute'
|
|
538
|
+
},
|
|
539
|
+
options: [
|
|
540
|
+
{
|
|
541
|
+
optionId: 'allow-once',
|
|
542
|
+
kind: 'allow_once',
|
|
543
|
+
name: 'Allow once'
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
optionId: 'reject-once',
|
|
547
|
+
kind: 'reject_once',
|
|
548
|
+
name: 'Reject once'
|
|
549
|
+
}
|
|
550
|
+
]
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
assert.equal(response.outcome.outcome, 'selected');
|
|
554
|
+
assert.equal(response.outcome.optionId, 'allow-once');
|
|
555
|
+
assert.equal(writer.messages.length, 0);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test('AcpClient plan mode rejects non-read tools and allows read-like tools', async () => {
|
|
559
|
+
const writer = createWriter();
|
|
560
|
+
const client = new AcpClient({
|
|
561
|
+
agentKey: 'claude',
|
|
562
|
+
writer,
|
|
563
|
+
projectPath: process.cwd(),
|
|
564
|
+
permissionMode: 'plan'
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
client.sessionId = 'session-9';
|
|
568
|
+
|
|
569
|
+
const denied = await client.handleRequestPermission({
|
|
570
|
+
sessionId: 'session-9',
|
|
571
|
+
toolCall: {
|
|
572
|
+
toolCallId: 'tool-4',
|
|
573
|
+
title: 'Edit file',
|
|
574
|
+
kind: 'edit'
|
|
575
|
+
},
|
|
576
|
+
options: [
|
|
577
|
+
{
|
|
578
|
+
optionId: 'allow-once',
|
|
579
|
+
kind: 'allow_once',
|
|
580
|
+
name: 'Allow once'
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
optionId: 'reject-once',
|
|
584
|
+
kind: 'reject_once',
|
|
585
|
+
name: 'Reject once'
|
|
586
|
+
}
|
|
587
|
+
]
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const allowed = await client.handleRequestPermission({
|
|
591
|
+
sessionId: 'session-9',
|
|
592
|
+
toolCall: {
|
|
593
|
+
toolCallId: 'tool-5',
|
|
594
|
+
title: 'Read file',
|
|
595
|
+
kind: 'read'
|
|
596
|
+
},
|
|
597
|
+
options: [
|
|
598
|
+
{
|
|
599
|
+
optionId: 'allow-once',
|
|
600
|
+
kind: 'allow_once',
|
|
601
|
+
name: 'Allow once'
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
optionId: 'reject-once',
|
|
605
|
+
kind: 'reject_once',
|
|
606
|
+
name: 'Reject once'
|
|
607
|
+
}
|
|
608
|
+
]
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
assert.equal(denied.outcome.outcome, 'selected');
|
|
612
|
+
assert.equal(denied.outcome.optionId, 'reject-once');
|
|
613
|
+
assert.equal(allowed.outcome.outcome, 'selected');
|
|
614
|
+
assert.equal(allowed.outcome.optionId, 'allow-once');
|
|
615
|
+
assert.equal(writer.messages.length, 0);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test('AcpClient fails Gemini prompts quickly when stderr reports rate limiting', async () => {
|
|
619
|
+
const writer = createWriter();
|
|
620
|
+
const client = new AcpClient({
|
|
621
|
+
agentKey: 'gemini',
|
|
622
|
+
writer,
|
|
623
|
+
projectPath: process.cwd()
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
client.sessionId = 'session-gemini-rate-limit';
|
|
627
|
+
client.connection = {
|
|
628
|
+
prompt: () => new Promise(() => {})
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const promptPromise = client.sendPrompt('Reply with exactly GEMINI_OK.');
|
|
632
|
+
client.handleAgentDiagnostic('Attempt 1 failed with status 429. reason: rateLimitExceeded. status: RESOURCE_EXHAUSTED');
|
|
633
|
+
|
|
634
|
+
await assert.rejects(
|
|
635
|
+
promptPromise,
|
|
636
|
+
/rate limited \(429 RESOURCE_EXHAUSTED\)/i
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
const conversationEvents = getConversationEvents(writer);
|
|
640
|
+
const stateChange = conversationEvents.find((event) => event.kind === 'session_state_changed' && event.payload?.state === 'errored');
|
|
641
|
+
const statusPayloads = writer.messages.filter((payload) => payload.type === 'session-status');
|
|
642
|
+
const rawErrorPayload = writer.messages.find((payload) => payload.type === 'error');
|
|
643
|
+
|
|
644
|
+
assert.ok(stateChange);
|
|
645
|
+
assert.equal(
|
|
646
|
+
rawErrorPayload?.error,
|
|
647
|
+
'Gemini request failed because the logged-in account is currently rate limited (429 RESOURCE_EXHAUSTED). Please wait and try again later.'
|
|
648
|
+
);
|
|
649
|
+
assert.deepEqual(
|
|
650
|
+
statusPayloads.map((payload) => payload.isProcessing),
|
|
651
|
+
[true, false]
|
|
652
|
+
);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test('AcpClient sends prompt resource links as ACP prompt blocks and user content blocks', async () => {
|
|
656
|
+
const writer = createWriter();
|
|
657
|
+
const client = new AcpClient({
|
|
658
|
+
agentKey: 'claude',
|
|
659
|
+
writer,
|
|
660
|
+
projectPath: process.cwd()
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
client.sessionId = 'session-prompt-blocks';
|
|
664
|
+
let promptParams = null;
|
|
665
|
+
client.connection = {
|
|
666
|
+
prompt: async (params) => {
|
|
667
|
+
promptParams = params;
|
|
668
|
+
return { stopReason: 'end_turn' };
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
await client.sendPrompt('Review this file.', {
|
|
673
|
+
resourceLinks: [
|
|
674
|
+
{
|
|
675
|
+
uri: '/tmp/spec.md',
|
|
676
|
+
name: 'spec.md'
|
|
677
|
+
}
|
|
678
|
+
]
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
assert.equal(promptParams.prompt[1].type, 'resource_link');
|
|
682
|
+
assert.equal(promptParams.prompt[1].uri, '/tmp/spec.md');
|
|
683
|
+
|
|
684
|
+
const userEvent = getConversationEvents(writer).find((event) => event.kind === 'user_message');
|
|
685
|
+
assert.ok(userEvent);
|
|
686
|
+
assert.equal(userEvent.payload.contentBlocks[0].type, 'resource_link');
|
|
687
|
+
assert.equal(userEvent.payload.contentBlocks[0].uri, '/tmp/spec.md');
|
|
688
|
+
});
|