@townco/ui 0.1.82 → 0.1.93
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/use-chat-input.js +13 -6
- package/dist/core/hooks/use-chat-messages.d.ts +17 -0
- package/dist/core/hooks/use-chat-messages.js +294 -10
- package/dist/core/schemas/chat.d.ts +20 -0
- package/dist/core/schemas/chat.js +4 -0
- package/dist/core/schemas/index.d.ts +1 -0
- package/dist/core/schemas/index.js +1 -0
- package/dist/core/schemas/source.d.ts +22 -0
- package/dist/core/schemas/source.js +45 -0
- package/dist/core/store/chat-store.d.ts +4 -0
- package/dist/core/store/chat-store.js +54 -0
- package/dist/gui/components/Actions.d.ts +15 -0
- package/dist/gui/components/Actions.js +22 -0
- package/dist/gui/components/ChatInput.d.ts +9 -1
- package/dist/gui/components/ChatInput.js +24 -6
- package/dist/gui/components/ChatInputCommandMenu.d.ts +1 -0
- package/dist/gui/components/ChatInputCommandMenu.js +22 -5
- package/dist/gui/components/ChatInputParameters.d.ts +13 -0
- package/dist/gui/components/ChatInputParameters.js +67 -0
- package/dist/gui/components/ChatLayout.d.ts +2 -0
- package/dist/gui/components/ChatLayout.js +183 -61
- package/dist/gui/components/ChatPanelTabContent.d.ts +7 -0
- package/dist/gui/components/ChatPanelTabContent.js +17 -7
- package/dist/gui/components/ChatView.js +105 -15
- package/dist/gui/components/CitationChip.d.ts +15 -0
- package/dist/gui/components/CitationChip.js +72 -0
- package/dist/gui/components/EditableUserMessage.d.ts +18 -0
- package/dist/gui/components/EditableUserMessage.js +109 -0
- package/dist/gui/components/MessageActions.d.ts +16 -0
- package/dist/gui/components/MessageActions.js +97 -0
- package/dist/gui/components/MessageContent.js +22 -7
- package/dist/gui/components/Response.d.ts +3 -0
- package/dist/gui/components/Response.js +30 -3
- package/dist/gui/components/Sidebar.js +1 -1
- package/dist/gui/components/TodoSubline.js +1 -1
- package/dist/gui/components/WorkProgress.js +7 -0
- package/dist/gui/components/index.d.ts +6 -1
- package/dist/gui/components/index.js +6 -1
- package/dist/gui/hooks/index.d.ts +1 -0
- package/dist/gui/hooks/index.js +1 -0
- package/dist/gui/hooks/use-favicon.d.ts +6 -0
- package/dist/gui/hooks/use-favicon.js +47 -0
- package/dist/gui/hooks/use-scroll-to-bottom.d.ts +14 -0
- package/dist/gui/hooks/use-scroll-to-bottom.js +317 -1
- package/dist/gui/index.d.ts +1 -1
- package/dist/gui/index.js +1 -1
- package/dist/gui/lib/motion.js +6 -6
- package/dist/gui/lib/remark-citations.d.ts +28 -0
- package/dist/gui/lib/remark-citations.js +70 -0
- package/dist/sdk/client/acp-client.d.ts +38 -1
- package/dist/sdk/client/acp-client.js +67 -3
- package/dist/sdk/schemas/message.d.ts +40 -0
- package/dist/sdk/schemas/message.js +20 -0
- package/dist/sdk/transports/http.d.ts +24 -1
- package/dist/sdk/transports/http.js +189 -1
- package/dist/sdk/transports/stdio.d.ts +1 -0
- package/dist/sdk/transports/stdio.js +39 -0
- package/dist/sdk/transports/types.d.ts +46 -1
- package/dist/sdk/transports/websocket.d.ts +1 -0
- package/dist/sdk/transports/websocket.js +4 -0
- package/dist/tui/components/ChatView.js +3 -4
- package/package.json +5 -3
- package/src/styles/global.css +71 -0
package/dist/gui/lib/motion.js
CHANGED
|
@@ -219,11 +219,11 @@ export function getDuration(shouldReduceMotion, duration = motionDuration.normal
|
|
|
219
219
|
// Sidebar Animations (AppSidebar)
|
|
220
220
|
// ============================================================================
|
|
221
221
|
export const sidebarTransition = {
|
|
222
|
-
duration: 0.
|
|
222
|
+
duration: 0.25,
|
|
223
223
|
ease: motionEasing.smooth,
|
|
224
224
|
};
|
|
225
225
|
export const sidebarMobileTransition = {
|
|
226
|
-
duration: 0.
|
|
226
|
+
duration: 0.15,
|
|
227
227
|
ease: motionEasing.smooth,
|
|
228
228
|
};
|
|
229
229
|
// Desktop: Slide animation (fixed width, no reflow)
|
|
@@ -244,9 +244,9 @@ export const sidebarContentVariants = {
|
|
|
244
244
|
animate: { opacity: 1, x: 0 },
|
|
245
245
|
};
|
|
246
246
|
export const sidebarContentTransition = {
|
|
247
|
-
duration: 0.
|
|
247
|
+
duration: 0.25,
|
|
248
248
|
ease: motionEasing.smooth,
|
|
249
|
-
delay: 0.
|
|
249
|
+
delay: 0.1,
|
|
250
250
|
};
|
|
251
251
|
// Backdrop animation for mobile overlay
|
|
252
252
|
export const backdropVariants = {
|
|
@@ -275,12 +275,12 @@ export const asideContentVariants = {
|
|
|
275
275
|
animate: { opacity: 1, x: 0 },
|
|
276
276
|
};
|
|
277
277
|
export const asideContentTransition = {
|
|
278
|
-
duration: 0.
|
|
278
|
+
duration: 0.25,
|
|
279
279
|
ease: motionEasing.smooth,
|
|
280
280
|
delay: 0.1,
|
|
281
281
|
};
|
|
282
282
|
// Mobile transition (faster, matches sidebar)
|
|
283
283
|
export const asideMobileTransition = {
|
|
284
|
-
duration: 0.
|
|
284
|
+
duration: 0.15,
|
|
285
285
|
ease: motionEasing.smooth,
|
|
286
286
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Root } from "mdast";
|
|
2
|
+
/**
|
|
3
|
+
* Citation node type for the MDAST
|
|
4
|
+
* Uses 'span' as hName since Streamdown only supports standard HTML elements
|
|
5
|
+
*/
|
|
6
|
+
export interface CitationNode {
|
|
7
|
+
type: "citation";
|
|
8
|
+
data: {
|
|
9
|
+
hName: "span";
|
|
10
|
+
hProperties: {
|
|
11
|
+
"data-citation-id": string;
|
|
12
|
+
className: "citation-marker";
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
sourceId: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Remark plugin that transforms citation syntax into citation nodes.
|
|
19
|
+
*
|
|
20
|
+
* Finds patterns like [[1]], [[2]], [[12]] (double brackets) or
|
|
21
|
+
* [1], [2], [12] (single brackets) in text and converts them
|
|
22
|
+
* to custom citation elements that can be rendered with a custom component.
|
|
23
|
+
*
|
|
24
|
+
* Single-bracket citations are only matched when NOT followed by:
|
|
25
|
+
* - `(` - would be a markdown link [text](url)
|
|
26
|
+
* - `:` - would be a markdown reference definition [1]: url
|
|
27
|
+
*/
|
|
28
|
+
export declare function remarkCitations(): (tree: Root) => void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { visit } from "unist-util-visit";
|
|
2
|
+
/**
|
|
3
|
+
* Remark plugin that transforms citation syntax into citation nodes.
|
|
4
|
+
*
|
|
5
|
+
* Finds patterns like [[1]], [[2]], [[12]] (double brackets) or
|
|
6
|
+
* [1], [2], [12] (single brackets) in text and converts them
|
|
7
|
+
* to custom citation elements that can be rendered with a custom component.
|
|
8
|
+
*
|
|
9
|
+
* Single-bracket citations are only matched when NOT followed by:
|
|
10
|
+
* - `(` - would be a markdown link [text](url)
|
|
11
|
+
* - `:` - would be a markdown reference definition [1]: url
|
|
12
|
+
*/
|
|
13
|
+
export function remarkCitations() {
|
|
14
|
+
return (tree) => {
|
|
15
|
+
visit(tree, "text", (node, index, parent) => {
|
|
16
|
+
if (!parent || index === undefined)
|
|
17
|
+
return;
|
|
18
|
+
// Match both [[N]] (double brackets) and [N] (single brackets)
|
|
19
|
+
// Single brackets use negative lookahead to avoid matching markdown links/refs
|
|
20
|
+
const citationRegex = /\[\[(\d+)\]\]|\[(\d+)\](?![:(])/g;
|
|
21
|
+
const text = node.value;
|
|
22
|
+
// Check if there are any citations in this text
|
|
23
|
+
if (!citationRegex.test(text))
|
|
24
|
+
return;
|
|
25
|
+
// Reset regex state
|
|
26
|
+
citationRegex.lastIndex = 0;
|
|
27
|
+
// Split text by citations and create mixed content
|
|
28
|
+
const parts = [];
|
|
29
|
+
let lastIndex = 0;
|
|
30
|
+
let match;
|
|
31
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: Standard regex exec loop pattern
|
|
32
|
+
while ((match = citationRegex.exec(text)) !== null) {
|
|
33
|
+
// Add text before the citation
|
|
34
|
+
if (match.index > lastIndex) {
|
|
35
|
+
parts.push({
|
|
36
|
+
type: "text",
|
|
37
|
+
value: text.slice(lastIndex, match.index),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// Add the citation node
|
|
41
|
+
// sourceId is in match[1] for [[N]] format, or match[2] for [N] format
|
|
42
|
+
const sourceId = match[1] ?? match[2] ?? "";
|
|
43
|
+
parts.push({
|
|
44
|
+
type: "citation",
|
|
45
|
+
data: {
|
|
46
|
+
hName: "span",
|
|
47
|
+
hProperties: {
|
|
48
|
+
"data-citation-id": sourceId,
|
|
49
|
+
className: "citation-marker",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
sourceId,
|
|
53
|
+
});
|
|
54
|
+
lastIndex = match.index + match[0].length;
|
|
55
|
+
}
|
|
56
|
+
// Add remaining text after last citation
|
|
57
|
+
if (lastIndex < text.length) {
|
|
58
|
+
parts.push({
|
|
59
|
+
type: "text",
|
|
60
|
+
value: text.slice(lastIndex),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Replace the original text node with the new nodes
|
|
64
|
+
if (parts.length > 0) {
|
|
65
|
+
// biome-ignore lint/suspicious/noExplicitAny: Parent children array needs dynamic assignment
|
|
66
|
+
parent.children.splice(index, 1, ...parts);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -54,6 +54,10 @@ export declare class AcpClient {
|
|
|
54
54
|
listSessions(): Promise<SessionSummary[]>;
|
|
55
55
|
/**
|
|
56
56
|
* Send a message in the current session
|
|
57
|
+
* @param content - The message text content
|
|
58
|
+
* @param sessionId - Optional session ID (defaults to current session)
|
|
59
|
+
* @param attachments - Optional image attachments
|
|
60
|
+
* @param promptParameters - Optional per-message parameters to influence agent behavior
|
|
57
61
|
*/
|
|
58
62
|
sendMessage(content: string, sessionId?: string, attachments?: Array<{
|
|
59
63
|
name: string;
|
|
@@ -61,11 +65,33 @@ export declare class AcpClient {
|
|
|
61
65
|
size: number;
|
|
62
66
|
mimeType: string;
|
|
63
67
|
data: string;
|
|
64
|
-
}>): Promise<void>;
|
|
68
|
+
}>, promptParameters?: Record<string, string>): Promise<void>;
|
|
65
69
|
/**
|
|
66
70
|
* Receive messages from the agent (streaming)
|
|
67
71
|
*/
|
|
68
72
|
receiveMessages(): AsyncIterableIterator<MessageChunk>;
|
|
73
|
+
/**
|
|
74
|
+
* Cancel the current agent turn
|
|
75
|
+
* Stops streaming, tool execution, and sub-agent processes
|
|
76
|
+
*/
|
|
77
|
+
cancel(sessionId?: string): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Edit and resend a message from a specific point in the conversation.
|
|
80
|
+
* This truncates the session history to the specified message index
|
|
81
|
+
* and sends the new content.
|
|
82
|
+
*
|
|
83
|
+
* @param messageIndex - The index of the user message to replace (0-based)
|
|
84
|
+
* @param content - The new text content
|
|
85
|
+
* @param sessionId - Optional session ID (defaults to current session)
|
|
86
|
+
* @param attachments - Optional image attachments
|
|
87
|
+
*/
|
|
88
|
+
editAndResend(messageIndex: number, content: string, sessionId?: string, attachments?: Array<{
|
|
89
|
+
name: string;
|
|
90
|
+
path: string;
|
|
91
|
+
size: number;
|
|
92
|
+
mimeType: string;
|
|
93
|
+
data: string;
|
|
94
|
+
}>): Promise<void>;
|
|
69
95
|
/**
|
|
70
96
|
* Get a session by ID
|
|
71
97
|
*/
|
|
@@ -94,6 +120,7 @@ export declare class AcpClient {
|
|
|
94
120
|
* - mcps: List of MCP servers connected to the agent
|
|
95
121
|
* - subagents: List of subagents available via Task tool
|
|
96
122
|
* - uiConfig: UI configuration for interface appearance
|
|
123
|
+
* - promptParameters: Configurable parameters users can select per-message
|
|
97
124
|
*/
|
|
98
125
|
getAgentInfo(): {
|
|
99
126
|
name?: string;
|
|
@@ -118,6 +145,16 @@ export declare class AcpClient {
|
|
|
118
145
|
name: string;
|
|
119
146
|
description: string;
|
|
120
147
|
}>;
|
|
148
|
+
promptParameters?: Array<{
|
|
149
|
+
id: string;
|
|
150
|
+
label: string;
|
|
151
|
+
description?: string;
|
|
152
|
+
options: Array<{
|
|
153
|
+
id: string;
|
|
154
|
+
label: string;
|
|
155
|
+
}>;
|
|
156
|
+
defaultOptionId?: string;
|
|
157
|
+
}>;
|
|
121
158
|
};
|
|
122
159
|
/**
|
|
123
160
|
* Create transport based on explicit configuration
|
|
@@ -142,8 +142,12 @@ export class AcpClient {
|
|
|
142
142
|
}
|
|
143
143
|
/**
|
|
144
144
|
* Send a message in the current session
|
|
145
|
+
* @param content - The message text content
|
|
146
|
+
* @param sessionId - Optional session ID (defaults to current session)
|
|
147
|
+
* @param attachments - Optional image attachments
|
|
148
|
+
* @param promptParameters - Optional per-message parameters to influence agent behavior
|
|
145
149
|
*/
|
|
146
|
-
async sendMessage(content, sessionId, attachments) {
|
|
150
|
+
async sendMessage(content, sessionId, attachments, promptParameters) {
|
|
147
151
|
const targetSessionId = sessionId || this.currentSessionId;
|
|
148
152
|
if (!targetSessionId) {
|
|
149
153
|
throw new Error("No active session. Start a session first.");
|
|
@@ -188,8 +192,8 @@ export class AcpClient {
|
|
|
188
192
|
// Add to session
|
|
189
193
|
session.messages.push(message);
|
|
190
194
|
this.updateSessionStatus(targetSessionId, "active");
|
|
191
|
-
// Send through transport
|
|
192
|
-
await this.transport.send(message);
|
|
195
|
+
// Send through transport with optional promptParameters
|
|
196
|
+
await this.transport.send(message, promptParameters ? { promptParameters } : undefined);
|
|
193
197
|
}
|
|
194
198
|
/**
|
|
195
199
|
* Receive messages from the agent (streaming)
|
|
@@ -200,6 +204,65 @@ export class AcpClient {
|
|
|
200
204
|
}
|
|
201
205
|
yield* this.transport.receive();
|
|
202
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Cancel the current agent turn
|
|
209
|
+
* Stops streaming, tool execution, and sub-agent processes
|
|
210
|
+
*/
|
|
211
|
+
async cancel(sessionId) {
|
|
212
|
+
const targetSessionId = sessionId || this.currentSessionId;
|
|
213
|
+
if (!targetSessionId) {
|
|
214
|
+
logger.warn("Cannot cancel: no session ID");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
logger.info("Cancelling session", { sessionId: targetSessionId });
|
|
218
|
+
await this.transport.cancel(targetSessionId);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Edit and resend a message from a specific point in the conversation.
|
|
222
|
+
* This truncates the session history to the specified message index
|
|
223
|
+
* and sends the new content.
|
|
224
|
+
*
|
|
225
|
+
* @param messageIndex - The index of the user message to replace (0-based)
|
|
226
|
+
* @param content - The new text content
|
|
227
|
+
* @param sessionId - Optional session ID (defaults to current session)
|
|
228
|
+
* @param attachments - Optional image attachments
|
|
229
|
+
*/
|
|
230
|
+
async editAndResend(messageIndex, content, sessionId, attachments) {
|
|
231
|
+
const targetSessionId = sessionId || this.currentSessionId;
|
|
232
|
+
if (!targetSessionId) {
|
|
233
|
+
throw new Error("No active session. Start a session first.");
|
|
234
|
+
}
|
|
235
|
+
if (!this.transport.isConnected()) {
|
|
236
|
+
throw new Error("Transport not connected");
|
|
237
|
+
}
|
|
238
|
+
if (!this.transport.editAndResend) {
|
|
239
|
+
throw new Error("Transport does not support edit and resend");
|
|
240
|
+
}
|
|
241
|
+
// Build prompt content blocks (images first, then text)
|
|
242
|
+
const prompt = [];
|
|
243
|
+
// Add image attachments
|
|
244
|
+
if (attachments && attachments.length > 0) {
|
|
245
|
+
for (const attachment of attachments) {
|
|
246
|
+
if (attachment.mimeType.startsWith("image/")) {
|
|
247
|
+
prompt.push({
|
|
248
|
+
type: "image",
|
|
249
|
+
data: attachment.data,
|
|
250
|
+
mimeType: attachment.mimeType,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Add text content
|
|
256
|
+
prompt.push({
|
|
257
|
+
type: "text",
|
|
258
|
+
text: content,
|
|
259
|
+
});
|
|
260
|
+
logger.info("Edit and resend", {
|
|
261
|
+
sessionId: targetSessionId,
|
|
262
|
+
messageIndex,
|
|
263
|
+
});
|
|
264
|
+
await this.transport.editAndResend(targetSessionId, messageIndex, prompt);
|
|
265
|
+
}
|
|
203
266
|
/**
|
|
204
267
|
* Get a session by ID
|
|
205
268
|
*/
|
|
@@ -246,6 +309,7 @@ export class AcpClient {
|
|
|
246
309
|
* - mcps: List of MCP servers connected to the agent
|
|
247
310
|
* - subagents: List of subagents available via Task tool
|
|
248
311
|
* - uiConfig: UI configuration for interface appearance
|
|
312
|
+
* - promptParameters: Configurable parameters users can select per-message
|
|
249
313
|
*/
|
|
250
314
|
getAgentInfo() {
|
|
251
315
|
return this.transport.getAgentInfo?.() || {};
|
|
@@ -406,6 +406,35 @@ export declare const HookNotificationChunk: z.ZodObject<{
|
|
|
406
406
|
messageId: z.ZodOptional<z.ZodString>;
|
|
407
407
|
}, z.core.$strip>;
|
|
408
408
|
export type HookNotificationChunk = z.infer<typeof HookNotificationChunk>;
|
|
409
|
+
/**
|
|
410
|
+
* Citation source schema (for inline citations from tool calls)
|
|
411
|
+
*/
|
|
412
|
+
export declare const CitationSource: z.ZodObject<{
|
|
413
|
+
id: z.ZodString;
|
|
414
|
+
url: z.ZodString;
|
|
415
|
+
title: z.ZodString;
|
|
416
|
+
snippet: z.ZodOptional<z.ZodString>;
|
|
417
|
+
favicon: z.ZodOptional<z.ZodString>;
|
|
418
|
+
toolCallId: z.ZodString;
|
|
419
|
+
sourceName: z.ZodOptional<z.ZodString>;
|
|
420
|
+
}, z.core.$strip>;
|
|
421
|
+
export type CitationSource = z.infer<typeof CitationSource>;
|
|
422
|
+
/**
|
|
423
|
+
* Sources chunk - for streaming citation sources from tool calls
|
|
424
|
+
*/
|
|
425
|
+
export declare const SourcesChunk: z.ZodObject<{
|
|
426
|
+
type: z.ZodLiteral<"sources">;
|
|
427
|
+
sources: z.ZodArray<z.ZodObject<{
|
|
428
|
+
id: z.ZodString;
|
|
429
|
+
url: z.ZodString;
|
|
430
|
+
title: z.ZodString;
|
|
431
|
+
snippet: z.ZodOptional<z.ZodString>;
|
|
432
|
+
favicon: z.ZodOptional<z.ZodString>;
|
|
433
|
+
toolCallId: z.ZodString;
|
|
434
|
+
sourceName: z.ZodOptional<z.ZodString>;
|
|
435
|
+
}, z.core.$strip>>;
|
|
436
|
+
}, z.core.$strip>;
|
|
437
|
+
export type SourcesChunk = z.infer<typeof SourcesChunk>;
|
|
409
438
|
/**
|
|
410
439
|
* Message chunk - discriminated union of all chunk types
|
|
411
440
|
*/
|
|
@@ -520,5 +549,16 @@ export declare const MessageChunk: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
520
549
|
toolCallId: z.ZodOptional<z.ZodString>;
|
|
521
550
|
}, z.core.$strip>], "type">;
|
|
522
551
|
messageId: z.ZodOptional<z.ZodString>;
|
|
552
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
553
|
+
type: z.ZodLiteral<"sources">;
|
|
554
|
+
sources: z.ZodArray<z.ZodObject<{
|
|
555
|
+
id: z.ZodString;
|
|
556
|
+
url: z.ZodString;
|
|
557
|
+
title: z.ZodString;
|
|
558
|
+
snippet: z.ZodOptional<z.ZodString>;
|
|
559
|
+
favicon: z.ZodOptional<z.ZodString>;
|
|
560
|
+
toolCallId: z.ZodString;
|
|
561
|
+
sourceName: z.ZodOptional<z.ZodString>;
|
|
562
|
+
}, z.core.$strip>>;
|
|
523
563
|
}, z.core.$strip>], "type">;
|
|
524
564
|
export type MessageChunk = z.infer<typeof MessageChunk>;
|
|
@@ -209,6 +209,25 @@ export const HookNotificationChunk = z.object({
|
|
|
209
209
|
notification: HookNotification,
|
|
210
210
|
messageId: z.string().optional(),
|
|
211
211
|
});
|
|
212
|
+
/**
|
|
213
|
+
* Citation source schema (for inline citations from tool calls)
|
|
214
|
+
*/
|
|
215
|
+
export const CitationSource = z.object({
|
|
216
|
+
id: z.string(),
|
|
217
|
+
url: z.string().url(),
|
|
218
|
+
title: z.string(),
|
|
219
|
+
snippet: z.string().optional(),
|
|
220
|
+
favicon: z.string().optional(),
|
|
221
|
+
toolCallId: z.string(),
|
|
222
|
+
sourceName: z.string().optional(),
|
|
223
|
+
});
|
|
224
|
+
/**
|
|
225
|
+
* Sources chunk - for streaming citation sources from tool calls
|
|
226
|
+
*/
|
|
227
|
+
export const SourcesChunk = z.object({
|
|
228
|
+
type: z.literal("sources"),
|
|
229
|
+
sources: z.array(CitationSource),
|
|
230
|
+
});
|
|
212
231
|
/**
|
|
213
232
|
* Message chunk - discriminated union of all chunk types
|
|
214
233
|
*/
|
|
@@ -217,4 +236,5 @@ export const MessageChunk = z.discriminatedUnion("type", [
|
|
|
217
236
|
ToolCallChunk,
|
|
218
237
|
ToolCallUpdateChunk,
|
|
219
238
|
HookNotificationChunk,
|
|
239
|
+
SourcesChunk,
|
|
220
240
|
]);
|
|
@@ -34,7 +34,19 @@ export declare class HttpTransport implements Transport {
|
|
|
34
34
|
*/
|
|
35
35
|
listSessions(): Promise<SessionSummary[]>;
|
|
36
36
|
disconnect(): Promise<void>;
|
|
37
|
-
send(message: Message
|
|
37
|
+
send(message: Message, options?: {
|
|
38
|
+
promptParameters?: Record<string, string>;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
cancel(sessionId: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Edit and resend a message from a specific point in the conversation.
|
|
43
|
+
* This truncates the session history and sends the new prompt.
|
|
44
|
+
*/
|
|
45
|
+
editAndResend(sessionId: string, messageIndex: number, prompt: Array<{
|
|
46
|
+
type: string;
|
|
47
|
+
text?: string;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}>): Promise<void>;
|
|
38
50
|
receive(): AsyncIterableIterator<MessageChunk>;
|
|
39
51
|
isConnected(): boolean;
|
|
40
52
|
onSessionUpdate(callback: (update: SessionUpdate) => void): () => void;
|
|
@@ -66,7 +78,18 @@ export declare class HttpTransport implements Transport {
|
|
|
66
78
|
name: string;
|
|
67
79
|
description: string;
|
|
68
80
|
}>;
|
|
81
|
+
promptParameters?: Array<{
|
|
82
|
+
id: string;
|
|
83
|
+
label: string;
|
|
84
|
+
description?: string;
|
|
85
|
+
options: Array<{
|
|
86
|
+
id: string;
|
|
87
|
+
label: string;
|
|
88
|
+
}>;
|
|
89
|
+
defaultOptionId?: string;
|
|
90
|
+
}>;
|
|
69
91
|
};
|
|
92
|
+
private sendNotification;
|
|
70
93
|
private sendRpcRequest;
|
|
71
94
|
private connectSSE;
|
|
72
95
|
private handleSSEDisconnect;
|
|
@@ -86,6 +86,11 @@ export class HttpTransport {
|
|
|
86
86
|
typeof meta.uiConfig === "object"
|
|
87
87
|
? meta.uiConfig
|
|
88
88
|
: undefined;
|
|
89
|
+
const promptParameters = metaIsObject &&
|
|
90
|
+
"promptParameters" in meta &&
|
|
91
|
+
Array.isArray(meta.promptParameters)
|
|
92
|
+
? meta.promptParameters
|
|
93
|
+
: undefined;
|
|
89
94
|
this.agentInfo = {
|
|
90
95
|
name: initResponse.agentInfo.name,
|
|
91
96
|
// title is the ACP field for human-readable display name
|
|
@@ -100,6 +105,7 @@ export class HttpTransport {
|
|
|
100
105
|
...(tools ? { tools } : {}),
|
|
101
106
|
...(mcps ? { mcps } : {}),
|
|
102
107
|
...(subagents ? { subagents } : {}),
|
|
108
|
+
...(promptParameters ? { promptParameters } : {}),
|
|
103
109
|
};
|
|
104
110
|
}
|
|
105
111
|
logger.info("ACP connection initialized", { initResponse });
|
|
@@ -181,6 +187,11 @@ export class HttpTransport {
|
|
|
181
187
|
typeof meta.uiConfig === "object"
|
|
182
188
|
? meta.uiConfig
|
|
183
189
|
: undefined;
|
|
190
|
+
const promptParameters = metaIsObject &&
|
|
191
|
+
"promptParameters" in meta &&
|
|
192
|
+
Array.isArray(meta.promptParameters)
|
|
193
|
+
? meta.promptParameters
|
|
194
|
+
: undefined;
|
|
184
195
|
this.agentInfo = {
|
|
185
196
|
name: initResponse.agentInfo.name,
|
|
186
197
|
// title is the ACP field for human-readable display name
|
|
@@ -195,6 +206,7 @@ export class HttpTransport {
|
|
|
195
206
|
...(tools ? { tools } : {}),
|
|
196
207
|
...(mcps ? { mcps } : {}),
|
|
197
208
|
...(subagents ? { subagents } : {}),
|
|
209
|
+
...(promptParameters ? { promptParameters } : {}),
|
|
198
210
|
};
|
|
199
211
|
}
|
|
200
212
|
// Check if loadSession is supported
|
|
@@ -294,7 +306,7 @@ export class HttpTransport {
|
|
|
294
306
|
throw err;
|
|
295
307
|
}
|
|
296
308
|
}
|
|
297
|
-
async send(message) {
|
|
309
|
+
async send(message, options) {
|
|
298
310
|
if (!this.connected || !this.currentSessionId) {
|
|
299
311
|
throw new Error("Transport not connected");
|
|
300
312
|
}
|
|
@@ -346,6 +358,12 @@ export class HttpTransport {
|
|
|
346
358
|
sessionId: this.currentSessionId,
|
|
347
359
|
prompt: promptBlocks,
|
|
348
360
|
};
|
|
361
|
+
// Add promptParameters to _meta if provided
|
|
362
|
+
if (options?.promptParameters) {
|
|
363
|
+
promptRequest._meta = {
|
|
364
|
+
promptParameters: options.promptParameters,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
349
367
|
// Send the prompt - this will trigger streaming responses via SSE
|
|
350
368
|
const promptResponse = await this.sendRpcRequest("session/prompt", promptRequest);
|
|
351
369
|
logger.debug("Prompt sent", { promptResponse });
|
|
@@ -379,6 +397,116 @@ export class HttpTransport {
|
|
|
379
397
|
throw err;
|
|
380
398
|
}
|
|
381
399
|
}
|
|
400
|
+
async cancel(sessionId) {
|
|
401
|
+
if (!this.connected) {
|
|
402
|
+
logger.warn("Cannot cancel: transport not connected");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const targetSessionId = sessionId || this.currentSessionId;
|
|
406
|
+
if (!targetSessionId) {
|
|
407
|
+
logger.warn("Cannot cancel: no session ID");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
logger.info("Cancelling session", { sessionId: targetSessionId });
|
|
411
|
+
// Mark stream as complete FIRST to stop processing new messages
|
|
412
|
+
this.streamComplete = true;
|
|
413
|
+
// Clear any queued messages to prevent them from being processed
|
|
414
|
+
this.messageQueue.length = 0;
|
|
415
|
+
// Send cancel notification to the agent (notification = no id, no response expected)
|
|
416
|
+
await this.sendNotification("session/cancel", {
|
|
417
|
+
sessionId: targetSessionId,
|
|
418
|
+
});
|
|
419
|
+
// Resolve any pending chunk resolvers with a completion marker
|
|
420
|
+
const completionChunk = {
|
|
421
|
+
type: "content",
|
|
422
|
+
id: targetSessionId,
|
|
423
|
+
role: "assistant",
|
|
424
|
+
contentDelta: { type: "text", text: "" },
|
|
425
|
+
isComplete: true,
|
|
426
|
+
};
|
|
427
|
+
// Drain all pending resolvers
|
|
428
|
+
while (this.chunkResolvers.length > 0) {
|
|
429
|
+
const resolver = this.chunkResolvers.shift();
|
|
430
|
+
if (resolver) {
|
|
431
|
+
resolver(completionChunk);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Edit and resend a message from a specific point in the conversation.
|
|
437
|
+
* This truncates the session history and sends the new prompt.
|
|
438
|
+
*/
|
|
439
|
+
async editAndResend(sessionId, messageIndex, prompt) {
|
|
440
|
+
if (!this.connected) {
|
|
441
|
+
throw new Error("Transport not connected");
|
|
442
|
+
}
|
|
443
|
+
const targetSessionId = sessionId || this.currentSessionId;
|
|
444
|
+
if (!targetSessionId) {
|
|
445
|
+
throw new Error("No session ID available");
|
|
446
|
+
}
|
|
447
|
+
// Exit replay mode when user sends their first message
|
|
448
|
+
if (this.isInReplayMode) {
|
|
449
|
+
logger.info("Exiting replay mode - user edited a message");
|
|
450
|
+
this.isInReplayMode = false;
|
|
451
|
+
}
|
|
452
|
+
// Reset stream state for new message
|
|
453
|
+
this.streamComplete = false;
|
|
454
|
+
this.messageQueue = [];
|
|
455
|
+
logger.info("Edit and resend", {
|
|
456
|
+
sessionId: targetSessionId,
|
|
457
|
+
messageIndex,
|
|
458
|
+
});
|
|
459
|
+
const headers = {
|
|
460
|
+
"Content-Type": "application/json",
|
|
461
|
+
...this.options.headers,
|
|
462
|
+
};
|
|
463
|
+
const timeoutMs = this.options.timeout ?? 10 * 60 * 1000;
|
|
464
|
+
const controller = new AbortController();
|
|
465
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
466
|
+
try {
|
|
467
|
+
const response = await fetch(`${this.options.baseUrl}/sessions/${targetSessionId}/edit-and-resend`, {
|
|
468
|
+
method: "POST",
|
|
469
|
+
headers,
|
|
470
|
+
body: JSON.stringify({ messageIndex, prompt }),
|
|
471
|
+
signal: controller.signal,
|
|
472
|
+
});
|
|
473
|
+
clearTimeout(timeoutId);
|
|
474
|
+
if (!response.ok) {
|
|
475
|
+
const errorText = await response.text();
|
|
476
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
477
|
+
}
|
|
478
|
+
// Mark stream as complete after edit-and-resend finishes
|
|
479
|
+
this.streamComplete = true;
|
|
480
|
+
// Send completion chunk
|
|
481
|
+
const resolver = this.chunkResolvers.shift();
|
|
482
|
+
if (resolver) {
|
|
483
|
+
resolver({
|
|
484
|
+
type: "content",
|
|
485
|
+
id: targetSessionId,
|
|
486
|
+
role: "assistant",
|
|
487
|
+
contentDelta: { type: "text", text: "" },
|
|
488
|
+
isComplete: true,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
this.messageQueue.push({
|
|
493
|
+
type: "content",
|
|
494
|
+
id: targetSessionId,
|
|
495
|
+
role: "assistant",
|
|
496
|
+
contentDelta: { type: "text", text: "" },
|
|
497
|
+
isComplete: true,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
clearTimeout(timeoutId);
|
|
503
|
+
this.streamComplete = true;
|
|
504
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
505
|
+
throw new Error(`Request timeout after ${timeoutMs}ms`);
|
|
506
|
+
}
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
382
510
|
async *receive() {
|
|
383
511
|
// Mark that we're actively receiving messages (prevents duplicate session updates)
|
|
384
512
|
this.isReceivingMessages = true;
|
|
@@ -448,6 +576,39 @@ export class HttpTransport {
|
|
|
448
576
|
getAgentInfo() {
|
|
449
577
|
return this.agentInfo || {};
|
|
450
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Send an ACP notification to the server (no response expected)
|
|
581
|
+
*/
|
|
582
|
+
async sendNotification(method, params) {
|
|
583
|
+
// Construct ACP notification message (no id = notification)
|
|
584
|
+
const notification = {
|
|
585
|
+
jsonrpc: "2.0",
|
|
586
|
+
method,
|
|
587
|
+
params,
|
|
588
|
+
};
|
|
589
|
+
logger.debug("Sending notification", { method, params });
|
|
590
|
+
const headers = {
|
|
591
|
+
"Content-Type": "application/json",
|
|
592
|
+
...this.options.headers,
|
|
593
|
+
};
|
|
594
|
+
try {
|
|
595
|
+
const response = await fetch(`${this.options.baseUrl}/rpc`, {
|
|
596
|
+
method: "POST",
|
|
597
|
+
headers,
|
|
598
|
+
body: JSON.stringify(notification),
|
|
599
|
+
});
|
|
600
|
+
if (!response.ok) {
|
|
601
|
+
const errorText = await response.text();
|
|
602
|
+
logger.error("Notification failed", {
|
|
603
|
+
status: response.status,
|
|
604
|
+
errorText,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
logger.error("Error sending notification", { error });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
451
612
|
/**
|
|
452
613
|
* Send an ACP RPC request to the server
|
|
453
614
|
*/
|
|
@@ -661,6 +822,11 @@ export class HttpTransport {
|
|
|
661
822
|
* Handle a session notification from the agent
|
|
662
823
|
*/
|
|
663
824
|
handleSessionNotification(params) {
|
|
825
|
+
// Skip processing if stream has been cancelled/completed
|
|
826
|
+
if (this.streamComplete) {
|
|
827
|
+
logger.debug("Skipping session notification - stream complete/cancelled");
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
664
830
|
logger.debug("handleSessionNotification called", { params });
|
|
665
831
|
// Extract content from the update
|
|
666
832
|
const paramsExtended = params;
|
|
@@ -1207,6 +1373,28 @@ export class HttpTransport {
|
|
|
1207
1373
|
sessionUpdate,
|
|
1208
1374
|
});
|
|
1209
1375
|
}
|
|
1376
|
+
else if (update &&
|
|
1377
|
+
"sessionUpdate" in update &&
|
|
1378
|
+
update.sessionUpdate === "sources") {
|
|
1379
|
+
// Sources notification - citation sources from tool calls
|
|
1380
|
+
const sourcesUpdate = update;
|
|
1381
|
+
logger.debug("Received sources notification", {
|
|
1382
|
+
sourcesCount: sourcesUpdate.sources.length,
|
|
1383
|
+
});
|
|
1384
|
+
// Create a sources chunk for the message queue
|
|
1385
|
+
const sourcesChunk = {
|
|
1386
|
+
type: "sources",
|
|
1387
|
+
sources: sourcesUpdate.sources,
|
|
1388
|
+
};
|
|
1389
|
+
// Queue for ordered processing
|
|
1390
|
+
const resolver = this.chunkResolvers.shift();
|
|
1391
|
+
if (resolver) {
|
|
1392
|
+
resolver(sourcesChunk);
|
|
1393
|
+
}
|
|
1394
|
+
else {
|
|
1395
|
+
this.messageQueue.push(sourcesChunk);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1210
1398
|
else if (update?.sessionUpdate === "agent_message_chunk") {
|
|
1211
1399
|
// Check if this is a replay (not live streaming)
|
|
1212
1400
|
const isReplay = update._meta &&
|