@townco/ui 0.1.15 → 0.1.16
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/core/hooks/index.d.ts +1 -0
- package/dist/core/hooks/index.js +1 -0
- package/dist/core/hooks/use-chat-messages.d.ts +50 -11
- package/dist/core/hooks/use-chat-session.d.ts +5 -5
- package/dist/core/hooks/use-tool-calls.d.ts +52 -0
- package/dist/core/hooks/use-tool-calls.js +61 -0
- package/dist/core/schemas/chat.d.ts +166 -83
- package/dist/core/schemas/chat.js +27 -27
- package/dist/core/schemas/index.d.ts +1 -0
- package/dist/core/schemas/index.js +1 -0
- package/dist/core/schemas/tool-call.d.ts +174 -0
- package/dist/core/schemas/tool-call.js +130 -0
- package/dist/core/store/chat-store.d.ts +28 -28
- package/dist/core/store/chat-store.js +123 -59
- package/dist/gui/components/ChatLayout.js +11 -10
- package/dist/gui/components/MessageContent.js +4 -1
- package/dist/gui/components/ToolCall.d.ts +8 -0
- package/dist/gui/components/ToolCall.js +100 -0
- package/dist/gui/components/ToolCallList.d.ts +9 -0
- package/dist/gui/components/ToolCallList.js +22 -0
- package/dist/gui/components/index.d.ts +2 -0
- package/dist/gui/components/index.js +2 -0
- package/dist/gui/components/resizable.d.ts +7 -0
- package/dist/gui/components/resizable.js +7 -0
- package/dist/sdk/schemas/session.d.ts +390 -220
- package/dist/sdk/schemas/session.js +74 -29
- package/dist/sdk/transports/http.js +705 -472
- package/dist/sdk/transports/stdio.js +187 -32
- package/dist/tui/components/ChatView.js +19 -51
- package/dist/tui/components/MessageList.d.ts +2 -4
- package/dist/tui/components/MessageList.js +13 -37
- package/dist/tui/components/ToolCall.d.ts +9 -0
- package/dist/tui/components/ToolCall.js +41 -0
- package/dist/tui/components/ToolCallList.d.ts +8 -0
- package/dist/tui/components/ToolCallList.js +17 -0
- package/dist/tui/components/index.d.ts +2 -0
- package/dist/tui/components/index.js +2 -0
- package/package.json +4 -2
|
@@ -4,476 +4,709 @@ import * as acp from "@agentclientprotocol/sdk";
|
|
|
4
4
|
* Uses POST /rpc for client->agent messages and GET /events (SSE) for agent->client
|
|
5
5
|
*/
|
|
6
6
|
export class HttpTransport {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
7
|
+
connected = false;
|
|
8
|
+
sessionUpdateCallbacks = new Set();
|
|
9
|
+
errorCallbacks = new Set();
|
|
10
|
+
messageQueue = [];
|
|
11
|
+
currentSessionId = null;
|
|
12
|
+
chunkResolvers = [];
|
|
13
|
+
streamComplete = false;
|
|
14
|
+
sseAbortController = null;
|
|
15
|
+
reconnectAttempts = 0;
|
|
16
|
+
maxReconnectAttempts = 5;
|
|
17
|
+
reconnectDelay = 1000; // Start with 1 second
|
|
18
|
+
reconnecting = false;
|
|
19
|
+
abortController = null;
|
|
20
|
+
options;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
// Ensure baseUrl doesn't end with a slash
|
|
23
|
+
this.options = { ...options, baseUrl: options.baseUrl.replace(/\/$/, "") };
|
|
24
|
+
}
|
|
25
|
+
async connect() {
|
|
26
|
+
if (this.connected) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
this.abortController = new AbortController();
|
|
31
|
+
// Step 1: Initialize the ACP connection
|
|
32
|
+
const initRequest = {
|
|
33
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
34
|
+
clientCapabilities: {
|
|
35
|
+
fs: {
|
|
36
|
+
readTextFile: true,
|
|
37
|
+
writeTextFile: true,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const initResponse = await this.sendRpcRequest("initialize", initRequest);
|
|
42
|
+
console.log("ACP connection initialized:", initResponse);
|
|
43
|
+
// Step 2: Create a new session
|
|
44
|
+
const sessionRequest = {
|
|
45
|
+
cwd: "/",
|
|
46
|
+
mcpServers: [],
|
|
47
|
+
};
|
|
48
|
+
const sessionResponse = await this.sendRpcRequest("session/new", sessionRequest);
|
|
49
|
+
this.currentSessionId = sessionResponse.sessionId;
|
|
50
|
+
console.log("Session created:", this.currentSessionId);
|
|
51
|
+
// Step 3: Open SSE connection for receiving messages
|
|
52
|
+
await this.connectSSE();
|
|
53
|
+
this.connected = true;
|
|
54
|
+
this.reconnectAttempts = 0; // Reset on successful connection
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.connected = false;
|
|
58
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
59
|
+
this.notifyError(err);
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async disconnect() {
|
|
64
|
+
if (!this.connected) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
// Abort any ongoing requests
|
|
69
|
+
if (this.abortController) {
|
|
70
|
+
this.abortController.abort();
|
|
71
|
+
this.abortController = null;
|
|
72
|
+
}
|
|
73
|
+
// Abort SSE connection
|
|
74
|
+
if (this.sseAbortController) {
|
|
75
|
+
this.sseAbortController.abort();
|
|
76
|
+
this.sseAbortController = null;
|
|
77
|
+
}
|
|
78
|
+
// Clear state
|
|
79
|
+
this.connected = false;
|
|
80
|
+
this.currentSessionId = null;
|
|
81
|
+
this.messageQueue = [];
|
|
82
|
+
this.chunkResolvers = [];
|
|
83
|
+
this.streamComplete = false;
|
|
84
|
+
this.reconnecting = false;
|
|
85
|
+
this.reconnectAttempts = 0;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
89
|
+
this.notifyError(err);
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async send(message) {
|
|
94
|
+
if (!this.connected || !this.currentSessionId) {
|
|
95
|
+
throw new Error("Transport not connected");
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
// Reset stream state for new message
|
|
99
|
+
this.streamComplete = false;
|
|
100
|
+
this.messageQueue = [];
|
|
101
|
+
// Convert our message format to ACP prompt format
|
|
102
|
+
const textContent = message.content
|
|
103
|
+
.filter((c) => c.type === "text")
|
|
104
|
+
.map((c) => c.text)
|
|
105
|
+
.join("\n");
|
|
106
|
+
// Create ACP prompt request
|
|
107
|
+
const promptRequest = {
|
|
108
|
+
sessionId: this.currentSessionId,
|
|
109
|
+
prompt: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: textContent,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
// Send the prompt - this will trigger streaming responses via SSE
|
|
117
|
+
const promptResponse = await this.sendRpcRequest("session/prompt", promptRequest);
|
|
118
|
+
console.log("Prompt sent:", promptResponse);
|
|
119
|
+
// Mark stream as complete after prompt finishes
|
|
120
|
+
this.streamComplete = true;
|
|
121
|
+
// Send completion chunk
|
|
122
|
+
const resolver = this.chunkResolvers.shift();
|
|
123
|
+
if (resolver) {
|
|
124
|
+
resolver({
|
|
125
|
+
id: this.currentSessionId || "unknown",
|
|
126
|
+
role: "assistant",
|
|
127
|
+
contentDelta: { type: "text", text: "" },
|
|
128
|
+
isComplete: true,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.messageQueue.push({
|
|
133
|
+
id: this.currentSessionId || "unknown",
|
|
134
|
+
role: "assistant",
|
|
135
|
+
contentDelta: { type: "text", text: "" },
|
|
136
|
+
isComplete: true,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
this.streamComplete = true;
|
|
142
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
143
|
+
this.notifyError(err);
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async *receive() {
|
|
148
|
+
// Keep yielding chunks until stream is complete
|
|
149
|
+
while (!this.streamComplete) {
|
|
150
|
+
// Check if there are queued messages
|
|
151
|
+
if (this.messageQueue.length > 0) {
|
|
152
|
+
const chunk = this.messageQueue.shift();
|
|
153
|
+
if (chunk) {
|
|
154
|
+
yield chunk;
|
|
155
|
+
if (chunk.isComplete) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Wait for next chunk to arrive
|
|
162
|
+
const chunk = await new Promise((resolve) => {
|
|
163
|
+
this.chunkResolvers.push(resolve);
|
|
164
|
+
});
|
|
165
|
+
if (chunk.isComplete) {
|
|
166
|
+
yield chunk;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
yield chunk;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Yield any remaining queued messages
|
|
175
|
+
while (this.messageQueue.length > 0) {
|
|
176
|
+
const chunk = this.messageQueue.shift();
|
|
177
|
+
if (chunk) {
|
|
178
|
+
yield chunk;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Mark the stream as complete
|
|
182
|
+
yield {
|
|
183
|
+
id: this.currentSessionId || "unknown",
|
|
184
|
+
role: "assistant",
|
|
185
|
+
contentDelta: { type: "text", text: "" },
|
|
186
|
+
isComplete: true,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
isConnected() {
|
|
190
|
+
return this.connected;
|
|
191
|
+
}
|
|
192
|
+
onSessionUpdate(callback) {
|
|
193
|
+
this.sessionUpdateCallbacks.add(callback);
|
|
194
|
+
return () => {
|
|
195
|
+
this.sessionUpdateCallbacks.delete(callback);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
onError(callback) {
|
|
199
|
+
this.errorCallbacks.add(callback);
|
|
200
|
+
return () => {
|
|
201
|
+
this.errorCallbacks.delete(callback);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Send an ACP RPC request to the server
|
|
206
|
+
*/
|
|
207
|
+
async sendRpcRequest(method, params) {
|
|
208
|
+
const requestId = this.generateRequestId();
|
|
209
|
+
// Construct ACP request message
|
|
210
|
+
const request = {
|
|
211
|
+
jsonrpc: "2.0",
|
|
212
|
+
id: requestId,
|
|
213
|
+
method,
|
|
214
|
+
params,
|
|
215
|
+
};
|
|
216
|
+
const headers = {
|
|
217
|
+
"Content-Type": "application/json",
|
|
218
|
+
...this.options.headers,
|
|
219
|
+
};
|
|
220
|
+
const timeout = this.options.timeout || 30000;
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
223
|
+
try {
|
|
224
|
+
const response = await fetch(`${this.options.baseUrl}/rpc`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers,
|
|
227
|
+
body: JSON.stringify(request),
|
|
228
|
+
signal: controller.signal,
|
|
229
|
+
});
|
|
230
|
+
clearTimeout(timeoutId);
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
const errorText = await response.text();
|
|
233
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
234
|
+
}
|
|
235
|
+
const result = await response.json();
|
|
236
|
+
// Check for JSON-RPC error
|
|
237
|
+
if (result.error) {
|
|
238
|
+
throw new Error(`ACP error: ${result.error.message || JSON.stringify(result.error)}`);
|
|
239
|
+
}
|
|
240
|
+
return result.result || result;
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
clearTimeout(timeoutId);
|
|
244
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
245
|
+
throw new Error(`Request timeout after ${timeout}ms`);
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Connect to the SSE event stream
|
|
252
|
+
* Uses fetch-based SSE to support custom headers (X-Session-ID)
|
|
253
|
+
*/
|
|
254
|
+
async connectSSE() {
|
|
255
|
+
if (!this.currentSessionId) {
|
|
256
|
+
throw new Error("Cannot connect SSE without a session ID");
|
|
257
|
+
}
|
|
258
|
+
const url = `${this.options.baseUrl}/events`;
|
|
259
|
+
const headers = {
|
|
260
|
+
"X-Session-ID": this.currentSessionId,
|
|
261
|
+
...this.options.headers,
|
|
262
|
+
};
|
|
263
|
+
// Create a new abort controller for this SSE connection
|
|
264
|
+
this.sseAbortController = new AbortController();
|
|
265
|
+
try {
|
|
266
|
+
const response = await fetch(url, {
|
|
267
|
+
method: "GET",
|
|
268
|
+
headers,
|
|
269
|
+
signal: this.sseAbortController.signal,
|
|
270
|
+
});
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
throw new Error(`SSE connection failed: HTTP ${response.status}`);
|
|
273
|
+
}
|
|
274
|
+
if (!response.body) {
|
|
275
|
+
throw new Error("Response body is null");
|
|
276
|
+
}
|
|
277
|
+
console.log("SSE connection opened");
|
|
278
|
+
this.reconnectAttempts = 0;
|
|
279
|
+
this.reconnectDelay = 1000;
|
|
280
|
+
// Read the SSE stream
|
|
281
|
+
const reader = response.body.getReader();
|
|
282
|
+
const decoder = new TextDecoder();
|
|
283
|
+
let buffer = "";
|
|
284
|
+
// Process the stream in the background
|
|
285
|
+
(async () => {
|
|
286
|
+
try {
|
|
287
|
+
while (true) {
|
|
288
|
+
const { done, value } = await reader.read();
|
|
289
|
+
if (done) {
|
|
290
|
+
console.log("SSE stream closed by server");
|
|
291
|
+
if (this.connected) {
|
|
292
|
+
await this.handleSSEDisconnect();
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
// Decode the chunk and add to buffer
|
|
297
|
+
buffer += decoder.decode(value, { stream: true });
|
|
298
|
+
// Process complete SSE messages
|
|
299
|
+
const lines = buffer.split("\n");
|
|
300
|
+
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
|
301
|
+
let currentEvent = { event: "message", data: "" };
|
|
302
|
+
for (const line of lines) {
|
|
303
|
+
if (line.startsWith("event:")) {
|
|
304
|
+
currentEvent.event = line.substring(6).trim();
|
|
305
|
+
}
|
|
306
|
+
else if (line.startsWith("data:")) {
|
|
307
|
+
currentEvent.data = line.substring(5).trim();
|
|
308
|
+
}
|
|
309
|
+
else if (line === "") {
|
|
310
|
+
// Empty line signals end of event
|
|
311
|
+
if (currentEvent.event === "message" && currentEvent.data) {
|
|
312
|
+
this.handleSSEMessage(currentEvent.data);
|
|
313
|
+
}
|
|
314
|
+
// Reset for next event
|
|
315
|
+
currentEvent = { event: "message", data: "" };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
322
|
+
console.log("SSE stream aborted");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
console.error("Error reading SSE stream:", error);
|
|
326
|
+
if (this.connected && !this.reconnecting) {
|
|
327
|
+
await this.handleSSEDisconnect();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
})();
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
console.error("SSE connection error:", error);
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Handle SSE disconnection with automatic reconnection
|
|
339
|
+
*/
|
|
340
|
+
async handleSSEDisconnect() {
|
|
341
|
+
if (this.reconnecting || !this.connected) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.reconnecting = true;
|
|
345
|
+
// Abort the current SSE connection
|
|
346
|
+
if (this.sseAbortController) {
|
|
347
|
+
this.sseAbortController.abort();
|
|
348
|
+
this.sseAbortController = null;
|
|
349
|
+
}
|
|
350
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
351
|
+
const error = new Error(`SSE reconnection failed after ${this.maxReconnectAttempts} attempts`);
|
|
352
|
+
this.notifyError(error);
|
|
353
|
+
this.connected = false;
|
|
354
|
+
this.reconnecting = false;
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.reconnectAttempts++;
|
|
358
|
+
const delay = Math.min(this.reconnectDelay * 2 ** (this.reconnectAttempts - 1), 32000);
|
|
359
|
+
console.log(`SSE reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
360
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
361
|
+
try {
|
|
362
|
+
await this.connectSSE();
|
|
363
|
+
console.log("SSE reconnected successfully");
|
|
364
|
+
this.reconnecting = false;
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
console.error("SSE reconnection failed:", error);
|
|
368
|
+
this.reconnecting = false;
|
|
369
|
+
// Will try again on next error event
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Handle an incoming SSE message
|
|
374
|
+
*/
|
|
375
|
+
handleSSEMessage(data) {
|
|
376
|
+
try {
|
|
377
|
+
const message = JSON.parse(data);
|
|
378
|
+
console.log("[HTTP Transport] Received SSE message:", message);
|
|
379
|
+
// Validate the message is an ACP agent outgoing message
|
|
380
|
+
const parseResult = acp.agentOutgoingMessageSchema.safeParse(message);
|
|
381
|
+
if (!parseResult.success) {
|
|
382
|
+
console.error("Invalid ACP message from SSE:", parseResult.error.issues);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const acpMessage = parseResult.data;
|
|
386
|
+
console.log("[HTTP Transport] Parsed ACP message, method:", "method" in acpMessage ? acpMessage.method : "(no method)");
|
|
387
|
+
// Check if this is a notification (has method but not a response)
|
|
388
|
+
if ("method" in acpMessage && acpMessage.method === "session/update") {
|
|
389
|
+
console.log("[HTTP Transport] This is a session/update notification");
|
|
390
|
+
// Type narrowing: we know it has method and params
|
|
391
|
+
if ("params" in acpMessage && acpMessage.params) {
|
|
392
|
+
console.log("[HTTP Transport] Calling handleSessionNotification with params:", acpMessage.params);
|
|
393
|
+
this.handleSessionNotification(acpMessage.params);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
console.error("Error parsing SSE message:", error);
|
|
399
|
+
this.notifyError(error instanceof Error ? error : new Error(String(error)));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Handle a session notification from the agent
|
|
404
|
+
*/
|
|
405
|
+
handleSessionNotification(params) {
|
|
406
|
+
console.log("[HTTP Transport] handleSessionNotification called with:", params);
|
|
407
|
+
// Extract content from the update
|
|
408
|
+
const paramsExtended = params;
|
|
409
|
+
const update = paramsExtended.update;
|
|
410
|
+
const sessionId = this.currentSessionId || params.sessionId;
|
|
411
|
+
console.log("[HTTP Transport] update.sessionUpdate type:", update?.sessionUpdate);
|
|
412
|
+
// Handle ACP tool call notifications
|
|
413
|
+
if (update?.sessionUpdate === "tool_call") {
|
|
414
|
+
console.log("[HTTP Transport] tool_call - tokenUsage:", update.tokenUsage);
|
|
415
|
+
// Extract messageId from _meta
|
|
416
|
+
const messageId = update._meta &&
|
|
417
|
+
typeof update._meta === "object" &&
|
|
418
|
+
"messageId" in update._meta
|
|
419
|
+
? String(update._meta.messageId)
|
|
420
|
+
: undefined;
|
|
421
|
+
// Initial tool call notification
|
|
422
|
+
const toolCall = {
|
|
423
|
+
id: update.toolCallId ?? "",
|
|
424
|
+
title: update.title ?? "",
|
|
425
|
+
kind: update.kind || "other",
|
|
426
|
+
status: update.status || "pending",
|
|
427
|
+
locations: update.locations,
|
|
428
|
+
rawInput: update.rawInput,
|
|
429
|
+
tokenUsage: update.tokenUsage,
|
|
430
|
+
content: update.content?.map((c) => {
|
|
431
|
+
// Type guard to safely check properties
|
|
432
|
+
if (typeof c !== "object" || c === null) {
|
|
433
|
+
return { type: "text", text: "" };
|
|
434
|
+
}
|
|
435
|
+
const content = c;
|
|
436
|
+
// Handle ACP nested content format
|
|
437
|
+
if (content.type === "content" &&
|
|
438
|
+
typeof content.content === "object" &&
|
|
439
|
+
content.content !== null) {
|
|
440
|
+
const innerContent = content.content;
|
|
441
|
+
if (innerContent.type === "text") {
|
|
442
|
+
return {
|
|
443
|
+
type: "content",
|
|
444
|
+
content: {
|
|
445
|
+
type: "text",
|
|
446
|
+
text: typeof innerContent.text === "string"
|
|
447
|
+
? innerContent.text
|
|
448
|
+
: "",
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Handle legacy direct formats
|
|
454
|
+
if (content.type === "text")
|
|
455
|
+
return {
|
|
456
|
+
type: "text",
|
|
457
|
+
text: typeof content.text === "string" ? content.text : "",
|
|
458
|
+
};
|
|
459
|
+
if (content.type === "diff")
|
|
460
|
+
return {
|
|
461
|
+
type: "diff",
|
|
462
|
+
path: typeof content.path === "string" ? content.path : "",
|
|
463
|
+
oldText: typeof content.oldText === "string" ? content.oldText : "",
|
|
464
|
+
newText: typeof content.newText === "string" ? content.newText : "",
|
|
465
|
+
line: typeof content.line === "number" ? content.line : null,
|
|
466
|
+
};
|
|
467
|
+
if (content.type === "terminal")
|
|
468
|
+
return {
|
|
469
|
+
type: "terminal",
|
|
470
|
+
terminalId: typeof content.terminalId === "string"
|
|
471
|
+
? content.terminalId
|
|
472
|
+
: "",
|
|
473
|
+
};
|
|
474
|
+
return { type: "text", text: "" };
|
|
475
|
+
}),
|
|
476
|
+
startedAt: Date.now(),
|
|
477
|
+
};
|
|
478
|
+
const sessionUpdate = {
|
|
479
|
+
type: "tool_call",
|
|
480
|
+
sessionId,
|
|
481
|
+
status: "active",
|
|
482
|
+
toolCall: toolCall,
|
|
483
|
+
messageId,
|
|
484
|
+
};
|
|
485
|
+
this.notifySessionUpdate(sessionUpdate);
|
|
486
|
+
}
|
|
487
|
+
else if (update?.sessionUpdate === "tool_call_update") {
|
|
488
|
+
// Extract messageId from _meta
|
|
489
|
+
const messageId = update._meta &&
|
|
490
|
+
typeof update._meta === "object" &&
|
|
491
|
+
"messageId" in update._meta
|
|
492
|
+
? String(update._meta.messageId)
|
|
493
|
+
: undefined;
|
|
494
|
+
// Tool call update notification
|
|
495
|
+
const toolCallUpdate = {
|
|
496
|
+
id: update.toolCallId ?? "",
|
|
497
|
+
status: update.status,
|
|
498
|
+
locations: update.locations,
|
|
499
|
+
rawOutput: update.rawOutput,
|
|
500
|
+
tokenUsage: update.tokenUsage,
|
|
501
|
+
content: update.content?.map((c) => {
|
|
502
|
+
// Type guard to safely check properties
|
|
503
|
+
if (typeof c !== "object" || c === null) {
|
|
504
|
+
return { type: "text", text: "" };
|
|
505
|
+
}
|
|
506
|
+
const content = c;
|
|
507
|
+
// Handle ACP nested content format
|
|
508
|
+
if (content.type === "content" &&
|
|
509
|
+
typeof content.content === "object" &&
|
|
510
|
+
content.content !== null) {
|
|
511
|
+
const innerContent = content.content;
|
|
512
|
+
if (innerContent.type === "text") {
|
|
513
|
+
return {
|
|
514
|
+
type: "content",
|
|
515
|
+
content: {
|
|
516
|
+
type: "text",
|
|
517
|
+
text: typeof innerContent.text === "string"
|
|
518
|
+
? innerContent.text
|
|
519
|
+
: "",
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// Handle legacy direct formats
|
|
525
|
+
if (content.type === "text")
|
|
526
|
+
return {
|
|
527
|
+
type: "text",
|
|
528
|
+
text: typeof content.text === "string" ? content.text : "",
|
|
529
|
+
};
|
|
530
|
+
if (content.type === "diff")
|
|
531
|
+
return {
|
|
532
|
+
type: "diff",
|
|
533
|
+
path: typeof content.path === "string" ? content.path : "",
|
|
534
|
+
oldText: typeof content.oldText === "string" ? content.oldText : "",
|
|
535
|
+
newText: typeof content.newText === "string" ? content.newText : "",
|
|
536
|
+
line: typeof content.line === "number" ? content.line : null,
|
|
537
|
+
};
|
|
538
|
+
if (content.type === "terminal")
|
|
539
|
+
return {
|
|
540
|
+
type: "terminal",
|
|
541
|
+
terminalId: typeof content.terminalId === "string"
|
|
542
|
+
? content.terminalId
|
|
543
|
+
: "",
|
|
544
|
+
};
|
|
545
|
+
return { type: "text", text: "" };
|
|
546
|
+
}),
|
|
547
|
+
error: update.error,
|
|
548
|
+
completedAt: update.status === "completed" || update.status === "failed"
|
|
549
|
+
? Date.now()
|
|
550
|
+
: undefined,
|
|
551
|
+
};
|
|
552
|
+
const sessionUpdate = {
|
|
553
|
+
type: "tool_call_update",
|
|
554
|
+
sessionId,
|
|
555
|
+
status: "active",
|
|
556
|
+
toolCallUpdate: toolCallUpdate,
|
|
557
|
+
messageId,
|
|
558
|
+
};
|
|
559
|
+
console.log("[HTTP Transport] Notifying tool_call_update session update:", sessionUpdate);
|
|
560
|
+
this.notifySessionUpdate(sessionUpdate);
|
|
561
|
+
}
|
|
562
|
+
else if (update &&
|
|
563
|
+
"sessionUpdate" in update &&
|
|
564
|
+
update.sessionUpdate === "tool_output") {
|
|
565
|
+
// Tool output notification (sent separately from tool_call_update)
|
|
566
|
+
// Type guard for tool_output extension
|
|
567
|
+
const toolOutputUpdate = update;
|
|
568
|
+
// Extract messageId from _meta
|
|
569
|
+
const messageId = toolOutputUpdate._meta &&
|
|
570
|
+
typeof toolOutputUpdate._meta === "object" &&
|
|
571
|
+
"messageId" in toolOutputUpdate._meta
|
|
572
|
+
? String(toolOutputUpdate._meta.messageId)
|
|
573
|
+
: undefined;
|
|
574
|
+
const toolOutput = {
|
|
575
|
+
id: toolOutputUpdate.toolCallId ?? "",
|
|
576
|
+
rawOutput: toolOutputUpdate.rawOutput,
|
|
577
|
+
content: toolOutputUpdate.content?.map((c) => {
|
|
578
|
+
// Type guard to safely check properties
|
|
579
|
+
if (typeof c !== "object" || c === null) {
|
|
580
|
+
return { type: "text", text: "" };
|
|
581
|
+
}
|
|
582
|
+
const content = c;
|
|
583
|
+
// Handle ACP nested content format
|
|
584
|
+
if (content.type === "content" &&
|
|
585
|
+
typeof content.content === "object" &&
|
|
586
|
+
content.content !== null) {
|
|
587
|
+
const innerContent = content.content;
|
|
588
|
+
if (innerContent.type === "text") {
|
|
589
|
+
return {
|
|
590
|
+
type: "content",
|
|
591
|
+
content: {
|
|
592
|
+
type: "text",
|
|
593
|
+
text: typeof innerContent.text === "string"
|
|
594
|
+
? innerContent.text
|
|
595
|
+
: "",
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// Handle legacy direct formats
|
|
601
|
+
if (content.type === "text")
|
|
602
|
+
return {
|
|
603
|
+
type: "text",
|
|
604
|
+
text: typeof content.text === "string" ? content.text : "",
|
|
605
|
+
};
|
|
606
|
+
if (content.type === "diff")
|
|
607
|
+
return {
|
|
608
|
+
type: "diff",
|
|
609
|
+
path: typeof content.path === "string" ? content.path : "",
|
|
610
|
+
oldText: typeof content.oldText === "string" ? content.oldText : "",
|
|
611
|
+
newText: typeof content.newText === "string" ? content.newText : "",
|
|
612
|
+
line: typeof content.line === "number" ? content.line : null,
|
|
613
|
+
};
|
|
614
|
+
if (content.type === "terminal")
|
|
615
|
+
return {
|
|
616
|
+
type: "terminal",
|
|
617
|
+
terminalId: typeof content.terminalId === "string"
|
|
618
|
+
? content.terminalId
|
|
619
|
+
: "",
|
|
620
|
+
};
|
|
621
|
+
return { type: "text", text: "" };
|
|
622
|
+
}),
|
|
623
|
+
};
|
|
624
|
+
// Emit as a tool_call_update so it merges with existing tool call
|
|
625
|
+
const sessionUpdate = {
|
|
626
|
+
type: "tool_call_update",
|
|
627
|
+
sessionId,
|
|
628
|
+
status: "active",
|
|
629
|
+
toolCallUpdate: toolOutput,
|
|
630
|
+
messageId,
|
|
631
|
+
};
|
|
632
|
+
console.log("[HTTP Transport] Notifying tool_output as tool_call_update:", sessionUpdate);
|
|
633
|
+
this.notifySessionUpdate(sessionUpdate);
|
|
634
|
+
}
|
|
635
|
+
else if (update?.sessionUpdate === "agent_message_chunk") {
|
|
636
|
+
// Handle agent message chunks
|
|
637
|
+
const sessionUpdate = {
|
|
638
|
+
type: "generic",
|
|
639
|
+
sessionId,
|
|
640
|
+
status: "active",
|
|
641
|
+
};
|
|
642
|
+
// Queue message chunks if present
|
|
643
|
+
// For agent_message_chunk, content is an object, not an array
|
|
644
|
+
const content = update.content;
|
|
645
|
+
if (content && typeof content === "object") {
|
|
646
|
+
const contentObj = content;
|
|
647
|
+
let chunk = null;
|
|
648
|
+
if (contentObj.type === "text" && typeof contentObj.text === "string") {
|
|
649
|
+
chunk = {
|
|
650
|
+
id: params.sessionId,
|
|
651
|
+
role: "assistant",
|
|
652
|
+
contentDelta: { type: "text", text: contentObj.text },
|
|
653
|
+
isComplete: false,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
if (chunk) {
|
|
657
|
+
// Resolve any waiting receive() calls immediately
|
|
658
|
+
const resolver = this.chunkResolvers.shift();
|
|
659
|
+
if (resolver) {
|
|
660
|
+
resolver(chunk);
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
// Only queue if no resolver is waiting
|
|
664
|
+
this.messageQueue.push(chunk);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
this.notifySessionUpdate(sessionUpdate);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
// Handle other session updates
|
|
672
|
+
const sessionUpdate = {
|
|
673
|
+
type: "generic",
|
|
674
|
+
sessionId,
|
|
675
|
+
status: "active",
|
|
676
|
+
};
|
|
677
|
+
this.notifySessionUpdate(sessionUpdate);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Generate a unique request ID
|
|
682
|
+
*/
|
|
683
|
+
generateRequestId() {
|
|
684
|
+
return `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Notify all session update callbacks
|
|
688
|
+
*/
|
|
689
|
+
notifySessionUpdate(update) {
|
|
690
|
+
for (const callback of this.sessionUpdateCallbacks) {
|
|
691
|
+
try {
|
|
692
|
+
callback(update);
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
console.error("Error in session update callback:", error);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Notify all error callbacks
|
|
701
|
+
*/
|
|
702
|
+
notifyError(error) {
|
|
703
|
+
for (const callback of this.errorCallbacks) {
|
|
704
|
+
try {
|
|
705
|
+
callback(error);
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
console.error("Error in error callback:", err);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
479
712
|
}
|