@townco/ui 0.1.0 → 0.1.6

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 (125) hide show
  1. package/dist/core/hooks/use-chat-input.d.ts +17 -17
  2. package/dist/core/hooks/use-chat-input.js +64 -55
  3. package/dist/core/hooks/use-chat-messages.d.ts +11 -11
  4. package/dist/core/hooks/use-chat-messages.js +121 -114
  5. package/dist/core/hooks/use-chat-session.d.ts +5 -5
  6. package/dist/core/hooks/use-chat-session.js +78 -80
  7. package/dist/core/hooks/use-media-query.d.ts +5 -5
  8. package/dist/core/hooks/use-media-query.js +38 -38
  9. package/dist/core/index.d.ts +1 -1
  10. package/dist/core/index.js +1 -1
  11. package/dist/core/schemas/chat.d.ts +83 -56
  12. package/dist/core/schemas/chat.js +27 -25
  13. package/dist/core/store/chat-store.d.ts +28 -22
  14. package/dist/core/store/chat-store.js +59 -50
  15. package/dist/gui/components/Button.d.ts +23 -7
  16. package/dist/gui/components/Button.js +40 -27
  17. package/dist/gui/components/Card.d.ts +26 -7
  18. package/dist/gui/components/Card.js +54 -8
  19. package/dist/gui/components/ChatHeader.d.ts +58 -31
  20. package/dist/gui/components/ChatHeader.js +171 -66
  21. package/dist/gui/components/ChatInput.d.ts +58 -36
  22. package/dist/gui/components/ChatInput.js +191 -121
  23. package/dist/gui/components/ChatInterface.d.ts +9 -6
  24. package/dist/gui/components/ChatInterface.js +162 -90
  25. package/dist/gui/components/ChatLayout.d.ts +71 -41
  26. package/dist/gui/components/ChatLayout.js +214 -87
  27. package/dist/gui/components/ChatPanelTabContent.d.ts +18 -9
  28. package/dist/gui/components/ChatPanelTabContent.js +88 -10
  29. package/dist/gui/components/ChatPreview.d.ts +9 -6
  30. package/dist/gui/components/ChatPreview.js +212 -162
  31. package/dist/gui/components/ChatSecondaryPanel.d.ts +14 -11
  32. package/dist/gui/components/ChatSecondaryPanel.js +115 -38
  33. package/dist/gui/components/ChatSidebar.d.ts +26 -13
  34. package/dist/gui/components/ChatSidebar.js +48 -14
  35. package/dist/gui/components/ChatStatus.d.ts +4 -2
  36. package/dist/gui/components/ChatStatus.js +45 -34
  37. package/dist/gui/components/ChatView.d.ts +5 -3
  38. package/dist/gui/components/ChatView.js +38 -9
  39. package/dist/gui/components/ConfigPanel.d.ts +16 -12
  40. package/dist/gui/components/ConfigPanel.js +218 -41
  41. package/dist/gui/components/Conversation.d.ts +17 -14
  42. package/dist/gui/components/Conversation.js +143 -83
  43. package/dist/gui/components/Dialog.d.ts +57 -11
  44. package/dist/gui/components/Dialog.js +84 -8
  45. package/dist/gui/components/DropdownMenu.d.ts +101 -20
  46. package/dist/gui/components/DropdownMenu.js +161 -14
  47. package/dist/gui/components/HeightTransition.d.ts +12 -7
  48. package/dist/gui/components/HeightTransition.js +88 -77
  49. package/dist/gui/components/Input.d.ts +13 -6
  50. package/dist/gui/components/Input.js +27 -16
  51. package/dist/gui/components/InputBox.d.ts +19 -12
  52. package/dist/gui/components/InputBox.js +86 -14
  53. package/dist/gui/components/Label.d.ts +7 -1
  54. package/dist/gui/components/Label.js +12 -2
  55. package/dist/gui/components/MarkdownRenderer.d.ts +6 -4
  56. package/dist/gui/components/MarkdownRenderer.js +178 -81
  57. package/dist/gui/components/Message.d.ts +25 -18
  58. package/dist/gui/components/Message.js +44 -23
  59. package/dist/gui/components/MessageContent.d.ts +29 -22
  60. package/dist/gui/components/MessageContent.js +157 -85
  61. package/dist/gui/components/PlaygroundLayout.d.ts +9 -5
  62. package/dist/gui/components/PlaygroundLayout.js +43 -12
  63. package/dist/gui/components/Reasoning.d.ts +30 -24
  64. package/dist/gui/components/Reasoning.js +187 -60
  65. package/dist/gui/components/Response.d.ts +11 -9
  66. package/dist/gui/components/Response.js +229 -90
  67. package/dist/gui/components/Select.d.ts +69 -10
  68. package/dist/gui/components/Select.js +118 -12
  69. package/dist/gui/components/Sonner.d.ts +3 -1
  70. package/dist/gui/components/Sonner.js +29 -18
  71. package/dist/gui/components/StatusBar.d.ts +9 -5
  72. package/dist/gui/components/StatusBar.js +56 -9
  73. package/dist/gui/components/Tabs.d.ts +24 -4
  74. package/dist/gui/components/Tabs.js +32 -4
  75. package/dist/gui/components/Task.d.ts +28 -24
  76. package/dist/gui/components/Task.js +164 -31
  77. package/dist/gui/components/Textarea.d.ts +15 -7
  78. package/dist/gui/components/Textarea.js +63 -46
  79. package/dist/gui/components/ThinkingBlock.d.ts +20 -10
  80. package/dist/gui/components/ThinkingBlock.js +134 -35
  81. package/dist/gui/components/TodoList.d.ts +12 -10
  82. package/dist/gui/components/TodoList.js +22 -7
  83. package/dist/gui/components/TodoListItem.d.ts +9 -6
  84. package/dist/gui/components/TodoListItem.js +18 -4
  85. package/dist/gui/components/index.d.ts +59 -8
  86. package/dist/gui/components/index.js +42 -8
  87. package/dist/gui/lib/utils.js +1 -1
  88. package/dist/index.d.ts +1 -1
  89. package/dist/index.js +1 -1
  90. package/dist/index.test.js +0 -1
  91. package/dist/sdk/client/acp-client.d.ts +88 -76
  92. package/dist/sdk/client/acp-client.js +215 -217
  93. package/dist/sdk/index.d.ts +1 -1
  94. package/dist/sdk/index.js +1 -1
  95. package/dist/sdk/schemas/agent.d.ts +111 -64
  96. package/dist/sdk/schemas/agent.js +24 -24
  97. package/dist/sdk/schemas/message.d.ts +245 -147
  98. package/dist/sdk/schemas/message.js +40 -40
  99. package/dist/sdk/schemas/session.d.ts +219 -135
  100. package/dist/sdk/schemas/session.js +27 -27
  101. package/dist/sdk/transports/http.d.ts +55 -55
  102. package/dist/sdk/transports/http.js +472 -469
  103. package/dist/sdk/transports/stdio.d.ts +20 -20
  104. package/dist/sdk/transports/stdio.js +289 -286
  105. package/dist/sdk/transports/types.d.ts +42 -42
  106. package/dist/sdk/transports/websocket.d.ts +12 -12
  107. package/dist/sdk/transports/websocket.js +52 -46
  108. package/dist/tui/components/ChatView.d.ts +4 -2
  109. package/dist/tui/components/ChatView.js +51 -18
  110. package/dist/tui/components/GameOfLife.js +64 -35
  111. package/dist/tui/components/InputBox.d.ts +18 -11
  112. package/dist/tui/components/InputBox.js +70 -10
  113. package/dist/tui/components/MessageList.d.ts +4 -2
  114. package/dist/tui/components/MessageList.js +37 -10
  115. package/dist/tui/components/MultiSelect.d.ts +15 -9
  116. package/dist/tui/components/MultiSelect.js +116 -69
  117. package/dist/tui/components/ReadlineInput.d.ts +12 -6
  118. package/dist/tui/components/ReadlineInput.js +252 -237
  119. package/dist/tui/components/SingleSelect.d.ts +15 -9
  120. package/dist/tui/components/SingleSelect.js +84 -43
  121. package/dist/tui/components/StatusBar.d.ts +11 -6
  122. package/dist/tui/components/StatusBar.js +102 -67
  123. package/dist/tui/index.d.ts +1 -1
  124. package/dist/tui/index.js +1 -1
  125. package/package.json +2 -3
@@ -4,473 +4,476 @@ 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
- 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
- // Validate the message is an ACP agent outgoing message
379
- const parseResult = acp.agentOutgoingMessageSchema.safeParse(message);
380
- if (!parseResult.success) {
381
- console.error("Invalid ACP message from SSE:", parseResult.error.issues);
382
- return;
383
- }
384
- const acpMessage = parseResult.data;
385
- // Check if this is a notification (has method but not a response)
386
- if ("method" in acpMessage && acpMessage.method === "session/update") {
387
- // Type narrowing: we know it has method and params
388
- if ("params" in acpMessage && acpMessage.params) {
389
- this.handleSessionNotification(acpMessage.params);
390
- }
391
- }
392
- }
393
- catch (error) {
394
- console.error("Error parsing SSE message:", error);
395
- this.notifyError(error instanceof Error ? error : new Error(String(error)));
396
- }
397
- }
398
- /**
399
- * Handle a session notification from the agent
400
- */
401
- handleSessionNotification(params) {
402
- // Extract content from the update
403
- const paramsExtended = params;
404
- const update = paramsExtended.update;
405
- const sessionUpdate = {
406
- sessionId: this.currentSessionId || params.sessionId,
407
- status: "active",
408
- };
409
- // Queue message chunks if present
410
- if (update?.content) {
411
- const content = update.content;
412
- let chunk = null;
413
- if (content.type === "text" && content.text) {
414
- chunk = {
415
- id: params.sessionId,
416
- role: "assistant",
417
- contentDelta: { type: "text", text: content.text },
418
- isComplete: false,
419
- };
420
- }
421
- else if (content.type === "tool_call") {
422
- chunk = {
423
- id: params.sessionId,
424
- role: "assistant",
425
- // biome-ignore lint/suspicious/noExplicitAny: hopefully assume that the server is sending the right stuff
426
- contentDelta: content,
427
- isComplete: false,
428
- };
429
- }
430
- if (chunk) {
431
- // Resolve any waiting receive() calls immediately
432
- const resolver = this.chunkResolvers.shift();
433
- if (resolver) {
434
- resolver(chunk);
435
- }
436
- else {
437
- // Only queue if no resolver is waiting
438
- this.messageQueue.push(chunk);
439
- }
440
- }
441
- }
442
- this.notifySessionUpdate(sessionUpdate);
443
- }
444
- /**
445
- * Generate a unique request ID
446
- */
447
- generateRequestId() {
448
- return `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
449
- }
450
- /**
451
- * Notify all session update callbacks
452
- */
453
- notifySessionUpdate(update) {
454
- for (const callback of this.sessionUpdateCallbacks) {
455
- try {
456
- callback(update);
457
- }
458
- catch (error) {
459
- console.error("Error in session update callback:", error);
460
- }
461
- }
462
- }
463
- /**
464
- * Notify all error callbacks
465
- */
466
- notifyError(error) {
467
- for (const callback of this.errorCallbacks) {
468
- try {
469
- callback(error);
470
- }
471
- catch (err) {
472
- console.error("Error in error callback:", err);
473
- }
474
- }
475
- }
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(
49
+ "session/new",
50
+ sessionRequest,
51
+ );
52
+ this.currentSessionId = sessionResponse.sessionId;
53
+ console.log("Session created:", this.currentSessionId);
54
+ // Step 3: Open SSE connection for receiving messages
55
+ await this.connectSSE();
56
+ this.connected = true;
57
+ this.reconnectAttempts = 0; // Reset on successful connection
58
+ } catch (error) {
59
+ this.connected = false;
60
+ const err = error instanceof Error ? error : new Error(String(error));
61
+ this.notifyError(err);
62
+ throw err;
63
+ }
64
+ }
65
+ async disconnect() {
66
+ if (!this.connected) {
67
+ return;
68
+ }
69
+ try {
70
+ // Abort any ongoing requests
71
+ if (this.abortController) {
72
+ this.abortController.abort();
73
+ this.abortController = null;
74
+ }
75
+ // Abort SSE connection
76
+ if (this.sseAbortController) {
77
+ this.sseAbortController.abort();
78
+ this.sseAbortController = null;
79
+ }
80
+ // Clear state
81
+ this.connected = false;
82
+ this.currentSessionId = null;
83
+ this.messageQueue = [];
84
+ this.chunkResolvers = [];
85
+ this.streamComplete = false;
86
+ this.reconnecting = false;
87
+ this.reconnectAttempts = 0;
88
+ } catch (error) {
89
+ const err = error instanceof Error ? error : new Error(String(error));
90
+ this.notifyError(err);
91
+ throw err;
92
+ }
93
+ }
94
+ async send(message) {
95
+ if (!this.connected || !this.currentSessionId) {
96
+ throw new Error("Transport not connected");
97
+ }
98
+ try {
99
+ // Reset stream state for new message
100
+ this.streamComplete = false;
101
+ this.messageQueue = [];
102
+ // Convert our message format to ACP prompt format
103
+ const textContent = message.content
104
+ .filter((c) => c.type === "text")
105
+ .map((c) => c.text)
106
+ .join("\n");
107
+ // Create ACP prompt request
108
+ const promptRequest = {
109
+ sessionId: this.currentSessionId,
110
+ prompt: [
111
+ {
112
+ type: "text",
113
+ text: textContent,
114
+ },
115
+ ],
116
+ };
117
+ // Send the prompt - this will trigger streaming responses via SSE
118
+ const promptResponse = await this.sendRpcRequest(
119
+ "session/prompt",
120
+ promptRequest,
121
+ );
122
+ console.log("Prompt sent:", promptResponse);
123
+ // Mark stream as complete after prompt finishes
124
+ this.streamComplete = true;
125
+ // Send completion chunk
126
+ const resolver = this.chunkResolvers.shift();
127
+ if (resolver) {
128
+ resolver({
129
+ id: this.currentSessionId || "unknown",
130
+ role: "assistant",
131
+ contentDelta: { type: "text", text: "" },
132
+ isComplete: true,
133
+ });
134
+ } else {
135
+ this.messageQueue.push({
136
+ id: this.currentSessionId || "unknown",
137
+ role: "assistant",
138
+ contentDelta: { type: "text", text: "" },
139
+ isComplete: true,
140
+ });
141
+ }
142
+ } catch (error) {
143
+ this.streamComplete = true;
144
+ const err = error instanceof Error ? error : new Error(String(error));
145
+ this.notifyError(err);
146
+ throw err;
147
+ }
148
+ }
149
+ async *receive() {
150
+ // Keep yielding chunks until stream is complete
151
+ while (!this.streamComplete) {
152
+ // Check if there are queued messages
153
+ if (this.messageQueue.length > 0) {
154
+ const chunk = this.messageQueue.shift();
155
+ if (chunk) {
156
+ yield chunk;
157
+ if (chunk.isComplete) {
158
+ return;
159
+ }
160
+ }
161
+ } else {
162
+ // Wait for next chunk to arrive
163
+ const chunk = await new Promise((resolve) => {
164
+ this.chunkResolvers.push(resolve);
165
+ });
166
+ if (chunk.isComplete) {
167
+ yield chunk;
168
+ return;
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(
239
+ `ACP error: ${result.error.message || JSON.stringify(result.error)}`,
240
+ );
241
+ }
242
+ return result.result || result;
243
+ } catch (error) {
244
+ clearTimeout(timeoutId);
245
+ if (error instanceof Error && error.name === "AbortError") {
246
+ throw new Error(`Request timeout after ${timeout}ms`);
247
+ }
248
+ throw error;
249
+ }
250
+ }
251
+ /**
252
+ * Connect to the SSE event stream
253
+ * Uses fetch-based SSE to support custom headers (X-Session-ID)
254
+ */
255
+ async connectSSE() {
256
+ if (!this.currentSessionId) {
257
+ throw new Error("Cannot connect SSE without a session ID");
258
+ }
259
+ const url = `${this.options.baseUrl}/events`;
260
+ const headers = {
261
+ "X-Session-ID": this.currentSessionId,
262
+ ...this.options.headers,
263
+ };
264
+ // Create a new abort controller for this SSE connection
265
+ this.sseAbortController = new AbortController();
266
+ try {
267
+ const response = await fetch(url, {
268
+ method: "GET",
269
+ headers,
270
+ signal: this.sseAbortController.signal,
271
+ });
272
+ if (!response.ok) {
273
+ throw new Error(`SSE connection failed: HTTP ${response.status}`);
274
+ }
275
+ if (!response.body) {
276
+ throw new Error("Response body is null");
277
+ }
278
+ console.log("SSE connection opened");
279
+ this.reconnectAttempts = 0;
280
+ this.reconnectDelay = 1000;
281
+ // Read the SSE stream
282
+ const reader = response.body.getReader();
283
+ const decoder = new TextDecoder();
284
+ let buffer = "";
285
+ // Process the stream in the background
286
+ (async () => {
287
+ try {
288
+ while (true) {
289
+ const { done, value } = await reader.read();
290
+ if (done) {
291
+ console.log("SSE stream closed by server");
292
+ if (this.connected) {
293
+ await this.handleSSEDisconnect();
294
+ }
295
+ break;
296
+ }
297
+ // Decode the chunk and add to buffer
298
+ buffer += decoder.decode(value, { stream: true });
299
+ // Process complete SSE messages
300
+ const lines = buffer.split("\n");
301
+ buffer = lines.pop() || ""; // Keep incomplete line in buffer
302
+ let currentEvent = { event: "message", data: "" };
303
+ for (const line of lines) {
304
+ if (line.startsWith("event:")) {
305
+ currentEvent.event = line.substring(6).trim();
306
+ } else if (line.startsWith("data:")) {
307
+ currentEvent.data = line.substring(5).trim();
308
+ } else if (line === "") {
309
+ // Empty line signals end of event
310
+ if (currentEvent.event === "message" && currentEvent.data) {
311
+ this.handleSSEMessage(currentEvent.data);
312
+ }
313
+ // Reset for next event
314
+ currentEvent = { event: "message", data: "" };
315
+ }
316
+ }
317
+ }
318
+ } catch (error) {
319
+ if (error instanceof Error && error.name === "AbortError") {
320
+ console.log("SSE stream aborted");
321
+ return;
322
+ }
323
+ console.error("Error reading SSE stream:", error);
324
+ if (this.connected && !this.reconnecting) {
325
+ await this.handleSSEDisconnect();
326
+ }
327
+ }
328
+ })();
329
+ } catch (error) {
330
+ console.error("SSE connection error:", error);
331
+ throw error;
332
+ }
333
+ }
334
+ /**
335
+ * Handle SSE disconnection with automatic reconnection
336
+ */
337
+ async handleSSEDisconnect() {
338
+ if (this.reconnecting || !this.connected) {
339
+ return;
340
+ }
341
+ this.reconnecting = true;
342
+ // Abort the current SSE connection
343
+ if (this.sseAbortController) {
344
+ this.sseAbortController.abort();
345
+ this.sseAbortController = null;
346
+ }
347
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
348
+ const error = new Error(
349
+ `SSE reconnection failed after ${this.maxReconnectAttempts} attempts`,
350
+ );
351
+ this.notifyError(error);
352
+ this.connected = false;
353
+ this.reconnecting = false;
354
+ return;
355
+ }
356
+ this.reconnectAttempts++;
357
+ const delay = Math.min(
358
+ this.reconnectDelay * 2 ** (this.reconnectAttempts - 1),
359
+ 32000,
360
+ );
361
+ console.log(
362
+ `SSE reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
363
+ );
364
+ await new Promise((resolve) => setTimeout(resolve, delay));
365
+ try {
366
+ await this.connectSSE();
367
+ console.log("SSE reconnected successfully");
368
+ this.reconnecting = false;
369
+ } catch (error) {
370
+ console.error("SSE reconnection failed:", error);
371
+ this.reconnecting = false;
372
+ // Will try again on next error event
373
+ }
374
+ }
375
+ /**
376
+ * Handle an incoming SSE message
377
+ */
378
+ handleSSEMessage(data) {
379
+ try {
380
+ const message = JSON.parse(data);
381
+ // Validate the message is an ACP agent outgoing message
382
+ const parseResult = acp.agentOutgoingMessageSchema.safeParse(message);
383
+ if (!parseResult.success) {
384
+ console.error(
385
+ "Invalid ACP message from SSE:",
386
+ parseResult.error.issues,
387
+ );
388
+ return;
389
+ }
390
+ const acpMessage = parseResult.data;
391
+ // Check if this is a notification (has method but not a response)
392
+ if ("method" in acpMessage && acpMessage.method === "session/update") {
393
+ // Type narrowing: we know it has method and params
394
+ if ("params" in acpMessage && acpMessage.params) {
395
+ this.handleSessionNotification(acpMessage.params);
396
+ }
397
+ }
398
+ } catch (error) {
399
+ console.error("Error parsing SSE message:", error);
400
+ this.notifyError(
401
+ error instanceof Error ? error : new Error(String(error)),
402
+ );
403
+ }
404
+ }
405
+ /**
406
+ * Handle a session notification from the agent
407
+ */
408
+ handleSessionNotification(params) {
409
+ // Extract content from the update
410
+ const paramsExtended = params;
411
+ const update = paramsExtended.update;
412
+ const sessionUpdate = {
413
+ sessionId: this.currentSessionId || params.sessionId,
414
+ status: "active",
415
+ };
416
+ // Queue message chunks if present
417
+ if (update?.content) {
418
+ const content = update.content;
419
+ let chunk = null;
420
+ if (content.type === "text" && content.text) {
421
+ chunk = {
422
+ id: params.sessionId,
423
+ role: "assistant",
424
+ contentDelta: { type: "text", text: content.text },
425
+ isComplete: false,
426
+ };
427
+ } else if (content.type === "tool_call") {
428
+ chunk = {
429
+ id: params.sessionId,
430
+ role: "assistant",
431
+ // biome-ignore lint/suspicious/noExplicitAny: hopefully assume that the server is sending the right stuff
432
+ contentDelta: content,
433
+ isComplete: false,
434
+ };
435
+ }
436
+ if (chunk) {
437
+ // Resolve any waiting receive() calls immediately
438
+ const resolver = this.chunkResolvers.shift();
439
+ if (resolver) {
440
+ resolver(chunk);
441
+ } else {
442
+ // Only queue if no resolver is waiting
443
+ this.messageQueue.push(chunk);
444
+ }
445
+ }
446
+ }
447
+ this.notifySessionUpdate(sessionUpdate);
448
+ }
449
+ /**
450
+ * Generate a unique request ID
451
+ */
452
+ generateRequestId() {
453
+ return `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
454
+ }
455
+ /**
456
+ * Notify all session update callbacks
457
+ */
458
+ notifySessionUpdate(update) {
459
+ for (const callback of this.sessionUpdateCallbacks) {
460
+ try {
461
+ callback(update);
462
+ } catch (error) {
463
+ console.error("Error in session update callback:", error);
464
+ }
465
+ }
466
+ }
467
+ /**
468
+ * Notify all error callbacks
469
+ */
470
+ notifyError(error) {
471
+ for (const callback of this.errorCallbacks) {
472
+ try {
473
+ callback(error);
474
+ } catch (err) {
475
+ console.error("Error in error callback:", err);
476
+ }
477
+ }
478
+ }
476
479
  }