@runtypelabs/persona 3.31.1 → 3.34.0
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/README.md +17 -10
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-quh7NmYD.d.cts → types-CthJFfNx.d.cts} +7 -0
- package/dist/animations/{types-quh7NmYD.d.ts → types-CthJFfNx.d.ts} +7 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/index.cjs +43 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +320 -8
- package/dist/index.d.ts +320 -8
- package/dist/index.global.js +193 -192
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +43 -43
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +117 -117
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +110 -2
- package/dist/smart-dom-reader.d.ts +110 -2
- package/dist/theme-editor.cjs +30 -30
- package/dist/theme-editor.d.cts +124 -5
- package/dist/theme-editor.d.ts +124 -5
- package/dist/theme-editor.js +30 -30
- package/package.json +3 -3
- package/src/ask-user-question-tool.test.ts +148 -0
- package/src/ask-user-question-tool.ts +138 -0
- package/src/client.ts +43 -9
- package/src/codegen.test.ts +0 -14
- package/src/components/messages.ts +10 -0
- package/src/components/suggestions.ts +49 -6
- package/src/index-core.ts +15 -0
- package/src/runtime/host-layout.test.ts +206 -9
- package/src/runtime/host-layout.ts +121 -8
- package/src/runtime/init.test.ts +2 -2
- package/src/session.ts +188 -32
- package/src/session.webmcp.test.ts +48 -0
- package/src/suggest-replies-tool.test.ts +445 -0
- package/src/suggest-replies-tool.ts +152 -0
- package/src/theme-editor/index.ts +2 -0
- package/src/theme-editor/webmcp/index.ts +2 -0
- package/src/theme-editor/webmcp/types.ts +16 -3
- package/src/types.ts +108 -2
- package/src/ui.docked.test.ts +2 -2
- package/src/ui.suggest-replies.test.ts +237 -0
- package/src/ui.ts +57 -13
- package/src/utils/dock.test.ts +23 -1
- package/src/utils/dock.ts +2 -0
- package/src/voice/voice.test.ts +0 -51
- package/src/install-config.test.ts +0 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.34.0",
|
|
4
4
|
"description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
],
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@mcp-b/webmcp-polyfill": "^3.0.0",
|
|
63
|
-
"dompurify": "^3.
|
|
63
|
+
"dompurify": "^3.4.10",
|
|
64
64
|
"idiomorph": "^0.7.4",
|
|
65
|
-
"lucide": "^
|
|
65
|
+
"lucide": "^1.18.0",
|
|
66
66
|
"marked": "^12.0.2",
|
|
67
67
|
"partial-json": "^0.1.7",
|
|
68
68
|
"zod": "^3.22.4"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
ASK_USER_QUESTION_CLIENT_TOOL,
|
|
4
|
+
ASK_USER_QUESTION_PARAMETERS_SCHEMA,
|
|
5
|
+
builtInClientToolsForDispatch,
|
|
6
|
+
} from "./ask-user-question-tool";
|
|
7
|
+
import {
|
|
8
|
+
ASK_USER_QUESTION_MAX,
|
|
9
|
+
ASK_USER_QUESTION_TOOL_NAME,
|
|
10
|
+
} from "./components/ask-user-question-bubble";
|
|
11
|
+
import { AgentWidgetClient } from "./client";
|
|
12
|
+
import { computeClientToolsFingerprint } from "./webmcp-bridge";
|
|
13
|
+
import type { AgentWidgetConfig } from "./types";
|
|
14
|
+
|
|
15
|
+
describe("ASK_USER_QUESTION_CLIENT_TOOL definition", () => {
|
|
16
|
+
it("matches the renderer's tool name and origin/annotation contract", () => {
|
|
17
|
+
expect(ASK_USER_QUESTION_CLIENT_TOOL.name).toBe(ASK_USER_QUESTION_TOOL_NAME);
|
|
18
|
+
// `'sdk'` keeps the bare name on the wire (the server only prefixes
|
|
19
|
+
// `origin: 'webmcp'` tools) so the step_await routes to the answer sheet,
|
|
20
|
+
// not the WebMCP bridge. `'local'` is NOT a valid server-side origin.
|
|
21
|
+
expect(ASK_USER_QUESTION_CLIENT_TOOL.origin).toBe("sdk");
|
|
22
|
+
expect(ASK_USER_QUESTION_CLIENT_TOOL.annotations?.readOnlyHint).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("bounds questions to the renderer cap and options to 2-4", () => {
|
|
26
|
+
const questions = ASK_USER_QUESTION_PARAMETERS_SCHEMA.properties.questions;
|
|
27
|
+
expect(questions.minItems).toBe(1);
|
|
28
|
+
expect(questions.maxItems).toBe(ASK_USER_QUESTION_MAX);
|
|
29
|
+
const options = questions.items.properties.options;
|
|
30
|
+
expect(options.minItems).toBe(2);
|
|
31
|
+
expect(options.maxItems).toBe(4);
|
|
32
|
+
expect(questions.items.required).toEqual(["question", "options"]);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("builtInClientToolsForDispatch", () => {
|
|
37
|
+
it("returns nothing by default (expose is opt-in)", () => {
|
|
38
|
+
expect(builtInClientToolsForDispatch(undefined)).toEqual([]);
|
|
39
|
+
expect(builtInClientToolsForDispatch({} as AgentWidgetConfig)).toEqual([]);
|
|
40
|
+
expect(
|
|
41
|
+
builtInClientToolsForDispatch({
|
|
42
|
+
features: { askUserQuestion: {} },
|
|
43
|
+
} as AgentWidgetConfig)
|
|
44
|
+
).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns the tool when expose is true", () => {
|
|
48
|
+
expect(
|
|
49
|
+
builtInClientToolsForDispatch({
|
|
50
|
+
features: { askUserQuestion: { expose: true } },
|
|
51
|
+
} as AgentWidgetConfig)
|
|
52
|
+
).toEqual([ASK_USER_QUESTION_CLIENT_TOOL]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("ignores expose when the answer sheet is disabled", () => {
|
|
56
|
+
// Offering the agent a question tool the widget can't render an answer
|
|
57
|
+
// UI for would park the execution on a generic tool bubble forever.
|
|
58
|
+
expect(
|
|
59
|
+
builtInClientToolsForDispatch({
|
|
60
|
+
features: { askUserQuestion: { expose: true, enabled: false } },
|
|
61
|
+
} as AgentWidgetConfig)
|
|
62
|
+
).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("AgentWidgetClient - built-in ask_user_question exposure", () => {
|
|
67
|
+
const captureDispatchBody = () => {
|
|
68
|
+
const captured: { body: string | null } = { body: null };
|
|
69
|
+
global.fetch = vi
|
|
70
|
+
.fn()
|
|
71
|
+
.mockImplementation(async (_url: string, options: { body: string }) => {
|
|
72
|
+
captured.body = options.body;
|
|
73
|
+
const encoder = new TextEncoder();
|
|
74
|
+
const stream = new ReadableStream({
|
|
75
|
+
start(controller) {
|
|
76
|
+
controller.enqueue(encoder.encode('data: {"type":"done"}\n\n'));
|
|
77
|
+
controller.close();
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
return new Response(stream, {
|
|
81
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
return captured;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const userMessage = () => ({
|
|
88
|
+
id: "u1",
|
|
89
|
+
role: "user" as const,
|
|
90
|
+
content: "hi",
|
|
91
|
+
createdAt: new Date().toISOString(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("ships ask_user_question on clientTools when expose is on (no WebMCP)", async () => {
|
|
95
|
+
const captured = captureDispatchBody();
|
|
96
|
+
const client = new AgentWidgetClient({
|
|
97
|
+
apiUrl: "http://localhost:8000",
|
|
98
|
+
features: { askUserQuestion: { expose: true } },
|
|
99
|
+
});
|
|
100
|
+
await client.dispatch({ messages: [userMessage()] }, () => undefined);
|
|
101
|
+
const parsed = JSON.parse(captured.body!);
|
|
102
|
+
expect(parsed.clientTools).toEqual([ASK_USER_QUESTION_CLIENT_TOOL]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("omits clientTools entirely when expose is off and no WebMCP tools exist", async () => {
|
|
106
|
+
const captured = captureDispatchBody();
|
|
107
|
+
const client = new AgentWidgetClient({
|
|
108
|
+
apiUrl: "http://localhost:8000",
|
|
109
|
+
});
|
|
110
|
+
await client.dispatch({ messages: [userMessage()] }, () => undefined);
|
|
111
|
+
const parsed = JSON.parse(captured.body!);
|
|
112
|
+
expect(parsed.clientTools).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("leads the WebMCP snapshot when both are present", async () => {
|
|
116
|
+
const captured = captureDispatchBody();
|
|
117
|
+
const client = new AgentWidgetClient({
|
|
118
|
+
apiUrl: "http://localhost:8000",
|
|
119
|
+
features: { askUserQuestion: { expose: true } },
|
|
120
|
+
});
|
|
121
|
+
(
|
|
122
|
+
client as unknown as {
|
|
123
|
+
webMcpBridge: { snapshotForDispatch: () => unknown[] } | null;
|
|
124
|
+
}
|
|
125
|
+
).webMcpBridge = {
|
|
126
|
+
snapshotForDispatch: () => [
|
|
127
|
+
{ name: "search", description: "s", origin: "webmcp" },
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
await client.dispatch({ messages: [userMessage()] }, () => undefined);
|
|
131
|
+
const parsed = JSON.parse(captured.body!);
|
|
132
|
+
expect(parsed.clientTools).toEqual([
|
|
133
|
+
ASK_USER_QUESTION_CLIENT_TOOL,
|
|
134
|
+
{ name: "search", description: "s", origin: "webmcp" },
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("changes the clientTools fingerprint when toggled (diff-only resend)", () => {
|
|
139
|
+
// In client-token mode the widget only resends the full clientTools[]
|
|
140
|
+
// when the fingerprint changes — toggling expose must change it so the
|
|
141
|
+
// server actually learns about (or forgets) the built-in tool.
|
|
142
|
+
const webMcpOnly = [{ name: "search", description: "s" }];
|
|
143
|
+
const withBuiltIn = [ASK_USER_QUESTION_CLIENT_TOOL, ...webMcpOnly];
|
|
144
|
+
expect(computeClientToolsFingerprint(withBuiltIn)).not.toBe(
|
|
145
|
+
computeClientToolsFingerprint(webMcpOnly)
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in `ask_user_question` client tool.
|
|
3
|
+
*
|
|
4
|
+
* The widget can advertise this tool to the agent on every dispatch via
|
|
5
|
+
* `clientTools[]` (set `features.askUserQuestion.expose: true`) — the same
|
|
6
|
+
* wire surface WebMCP page tools ride. The server registers it as a LOCAL
|
|
7
|
+
* tool under its bare name (`origin: 'sdk'` tools are not `webmcp:`-prefixed),
|
|
8
|
+
* so when the model calls it the execution pauses with a `step_await`
|
|
9
|
+
* (`awaitReason: "local_tool_required"`), the widget's answer-pill sheet
|
|
10
|
+
* renders, and `session.resolveAskUserQuestion()` resumes the execution with
|
|
11
|
+
* the structured answers.
|
|
12
|
+
*
|
|
13
|
+
* This replaces the previous integrator burden of hand-declaring the tool in
|
|
14
|
+
* a flow's `runtimeTools` and keeping that schema in sync with the renderer.
|
|
15
|
+
* Flows that already declare `ask_user_question` server-side should leave
|
|
16
|
+
* `expose` off — the model would otherwise see the tool twice.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
ASK_USER_QUESTION_MAX,
|
|
21
|
+
ASK_USER_QUESTION_TOOL_NAME,
|
|
22
|
+
} from "./components/ask-user-question-bubble";
|
|
23
|
+
import {
|
|
24
|
+
SUGGEST_REPLIES_CLIENT_TOOL,
|
|
25
|
+
shouldExposeSuggestReplies,
|
|
26
|
+
} from "./suggest-replies-tool";
|
|
27
|
+
import type { AgentWidgetConfig, ClientToolDefinition } from "./types";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* JSON Schema for the tool's parameters. Mirrors {@link AskUserQuestionPayload}
|
|
31
|
+
* — the shape `parseAskUserQuestionPayload` hydrates the answer sheet from —
|
|
32
|
+
* so the schema the model is held to and the schema the renderer expects can
|
|
33
|
+
* never drift.
|
|
34
|
+
*/
|
|
35
|
+
export const ASK_USER_QUESTION_PARAMETERS_SCHEMA = {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
questions: {
|
|
39
|
+
type: "array",
|
|
40
|
+
minItems: 1,
|
|
41
|
+
maxItems: ASK_USER_QUESTION_MAX,
|
|
42
|
+
description: "Questions to ask the user. Prefer a single question.",
|
|
43
|
+
items: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
question: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "The complete question, ending with a question mark.",
|
|
49
|
+
},
|
|
50
|
+
header: {
|
|
51
|
+
type: "string",
|
|
52
|
+
maxLength: 12,
|
|
53
|
+
description: 'Short topic label, e.g. "Auth method".',
|
|
54
|
+
},
|
|
55
|
+
options: {
|
|
56
|
+
type: "array",
|
|
57
|
+
minItems: 2,
|
|
58
|
+
maxItems: 4,
|
|
59
|
+
description:
|
|
60
|
+
'2-4 distinct choices. Do NOT add an "Other" option — free text is automatic.',
|
|
61
|
+
items: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
label: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Concise choice text (1-5 words).",
|
|
67
|
+
},
|
|
68
|
+
description: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "What the option means or implies.",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: ["label"],
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
multiSelect: {
|
|
78
|
+
type: "boolean",
|
|
79
|
+
description: "Allow selecting multiple options. Default false.",
|
|
80
|
+
},
|
|
81
|
+
allowFreeText: {
|
|
82
|
+
type: "boolean",
|
|
83
|
+
description: "Show a free-text input. Default true.",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: ["question", "options"],
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
required: ["questions"],
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The `ClientToolDefinition` shipped on `dispatch.clientTools[]` when
|
|
97
|
+
* `features.askUserQuestion.expose` is on. Exported so integrators who prefer
|
|
98
|
+
* declaring the tool server-side (a flow's `runtimeTools`) can reuse the same
|
|
99
|
+
* description and schema instead of hand-writing them.
|
|
100
|
+
*/
|
|
101
|
+
export const ASK_USER_QUESTION_CLIENT_TOOL: ClientToolDefinition = {
|
|
102
|
+
name: ASK_USER_QUESTION_TOOL_NAME,
|
|
103
|
+
description:
|
|
104
|
+
"Ask the user multiple-choice questions and wait for their answers. Use " +
|
|
105
|
+
"only when blocked on a decision that is the user's to make — a " +
|
|
106
|
+
"preference, a choice between valid approaches, or information you " +
|
|
107
|
+
"cannot infer. Each question offers 2-4 options plus an automatic " +
|
|
108
|
+
"free-text input. The result maps each question to its answer (an array " +
|
|
109
|
+
"when multiSelect); a question absent from the result was skipped.",
|
|
110
|
+
parametersSchema: ASK_USER_QUESTION_PARAMETERS_SCHEMA,
|
|
111
|
+
origin: "sdk",
|
|
112
|
+
annotations: { readOnlyHint: true },
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Built-in client tools to append to a dispatch's `clientTools[]` for the
|
|
117
|
+
* given widget config: `ask_user_question` and `suggest_replies`; future
|
|
118
|
+
* built-in local tools join here.
|
|
119
|
+
*
|
|
120
|
+
* Each tool is gated on BOTH of its feature flags: `expose` opts the tool
|
|
121
|
+
* into the agent's catalog, and `enabled !== false` guarantees the widget can
|
|
122
|
+
* actually render UI (and, for fire-and-forget tools, auto-resume) for it —
|
|
123
|
+
* exposing a tool with its feature disabled would park the execution on a
|
|
124
|
+
* generic tool bubble with no way to advance.
|
|
125
|
+
*/
|
|
126
|
+
export const builtInClientToolsForDispatch = (
|
|
127
|
+
config: AgentWidgetConfig | undefined,
|
|
128
|
+
): ClientToolDefinition[] => {
|
|
129
|
+
const tools: ClientToolDefinition[] = [];
|
|
130
|
+
const ask = config?.features?.askUserQuestion;
|
|
131
|
+
if (ask?.expose === true && ask.enabled !== false) {
|
|
132
|
+
tools.push(ASK_USER_QUESTION_CLIENT_TOOL);
|
|
133
|
+
}
|
|
134
|
+
if (shouldExposeSuggestReplies(config)) {
|
|
135
|
+
tools.push(SUGGEST_REPLIES_CLIENT_TOOL);
|
|
136
|
+
}
|
|
137
|
+
return tools;
|
|
138
|
+
};
|
package/src/client.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
WebMcpConfirmHandler
|
|
25
25
|
} from "./types";
|
|
26
26
|
import { WebMcpBridge, computeClientToolsFingerprint, isWebMcpToolName } from "./webmcp-bridge";
|
|
27
|
+
import { builtInClientToolsForDispatch } from "./ask-user-question-tool";
|
|
27
28
|
import {
|
|
28
29
|
extractTextFromJson,
|
|
29
30
|
createPlainTextParser,
|
|
@@ -201,6 +202,28 @@ export class AgentWidgetClient {
|
|
|
201
202
|
config.webmcp?.enabled === true ? new WebMcpBridge(config.webmcp) : null;
|
|
202
203
|
}
|
|
203
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Refresh config in place WITHOUT tearing down the live connection or the
|
|
207
|
+
* WebMCP bridge. `AgentWidgetSession.updateConfig` calls this when only
|
|
208
|
+
* connection-irrelevant fields changed (theme, copy, layout, suggestions, …),
|
|
209
|
+
* so a UI update that lands mid-turn — e.g. a `webmcp:*` tool restyling the
|
|
210
|
+
* widget while the agent's turn is still streaming — doesn't abandon the
|
|
211
|
+
* in-flight stream/resume. Connection or request-shaping changes (apiUrl,
|
|
212
|
+
* clientToken, webmcp, headers, parser, …) take the full client rebuild path
|
|
213
|
+
* in the session instead, which is the only place the bridge is recreated.
|
|
214
|
+
*
|
|
215
|
+
* Only the live-read `config` is refreshed (e.g. `iterationDisplay`); the
|
|
216
|
+
* constructor-derived request-shaping fields (apiUrl, headers, parser,
|
|
217
|
+
* contextProviders, middleware, …) are left untouched because the session
|
|
218
|
+
* routes any change to those down the full-rebuild path instead — so they are
|
|
219
|
+
* guaranteed unchanged here. The `webMcpBridge` instance and its
|
|
220
|
+
* installed-polyfill memo are deliberately preserved, which keeps any
|
|
221
|
+
* in-flight resolve alive.
|
|
222
|
+
*/
|
|
223
|
+
public updateConfig(next: AgentWidgetConfig): void {
|
|
224
|
+
this.config = next;
|
|
225
|
+
}
|
|
226
|
+
|
|
204
227
|
/**
|
|
205
228
|
* Set callback for capturing raw SSE events
|
|
206
229
|
*/
|
|
@@ -1035,11 +1058,18 @@ export class AgentWidgetClient {
|
|
|
1035
1058
|
}
|
|
1036
1059
|
};
|
|
1037
1060
|
|
|
1038
|
-
//
|
|
1039
|
-
//
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1061
|
+
// Client tools: built-in widget tools (ask_user_question, when exposed)
|
|
1062
|
+
// plus the per-turn WebMCP page-registry snapshot. Name collisions are
|
|
1063
|
+
// impossible — WebMCP entries are `webmcp:`-prefixed server-side while
|
|
1064
|
+
// `sdk`-origin built-ins keep bare names. Both kinds ride the same
|
|
1065
|
+
// diff-only fingerprint path in client-token mode. Kept to a single await
|
|
1066
|
+
// so dispatch microtask timing is unchanged.
|
|
1067
|
+
const clientTools = [
|
|
1068
|
+
...builtInClientToolsForDispatch(this.config),
|
|
1069
|
+
...((await this.webMcpBridge?.snapshotForDispatch()) ?? []),
|
|
1070
|
+
];
|
|
1071
|
+
if (clientTools.length > 0) {
|
|
1072
|
+
payload.clientTools = clientTools;
|
|
1043
1073
|
}
|
|
1044
1074
|
|
|
1045
1075
|
// Add context from providers
|
|
@@ -1096,10 +1126,14 @@ export class AgentWidgetClient {
|
|
|
1096
1126
|
...(this.config.flowId && { flowId: this.config.flowId })
|
|
1097
1127
|
};
|
|
1098
1128
|
|
|
1099
|
-
//
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1129
|
+
// Client tools: same built-in + WebMCP merge as buildAgentPayload
|
|
1130
|
+
// (flow-dispatch path).
|
|
1131
|
+
const clientTools = [
|
|
1132
|
+
...builtInClientToolsForDispatch(this.config),
|
|
1133
|
+
...((await this.webMcpBridge?.snapshotForDispatch()) ?? []),
|
|
1134
|
+
];
|
|
1135
|
+
if (clientTools.length > 0) {
|
|
1136
|
+
payload.clientTools = clientTools;
|
|
1103
1137
|
}
|
|
1104
1138
|
|
|
1105
1139
|
if (this.contextProviders.length) {
|
package/src/codegen.test.ts
CHANGED
|
@@ -17,20 +17,6 @@ describe("codegen subpath", () => {
|
|
|
17
17
|
expect(fromSubpath).toBe(fromInternal);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
it("produces identical output via the subpath for every format", () => {
|
|
21
|
-
const formats = [
|
|
22
|
-
"esm",
|
|
23
|
-
"script-installer",
|
|
24
|
-
"script-manual",
|
|
25
|
-
"script-advanced",
|
|
26
|
-
"react-component",
|
|
27
|
-
"react-advanced",
|
|
28
|
-
] as const;
|
|
29
|
-
for (const format of formats) {
|
|
30
|
-
expect(fromSubpath(config, format)).toBe(fromInternal(config, format));
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
|
|
34
20
|
it("pins the installer CDN url to the package version (not @latest)", () => {
|
|
35
21
|
const code = fromSubpath(config, "script-installer");
|
|
36
22
|
expect(code).toContain(`@runtypelabs/persona@${VERSION}/dist/install.global.js`);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isAskUserQuestionMessage,
|
|
10
10
|
removeAskUserQuestionSheet,
|
|
11
11
|
} from "./ask-user-question-bubble";
|
|
12
|
+
import { isSuggestRepliesMessage } from "../suggest-replies-tool";
|
|
12
13
|
|
|
13
14
|
export const renderMessages = (
|
|
14
15
|
container: HTMLElement,
|
|
@@ -40,6 +41,15 @@ export const renderMessages = (
|
|
|
40
41
|
ensureAskUserQuestionSheet(message, config, composerOverlay ?? null);
|
|
41
42
|
}
|
|
42
43
|
return;
|
|
44
|
+
} else if (
|
|
45
|
+
isSuggestRepliesMessage(message) &&
|
|
46
|
+
config?.features?.suggestReplies?.enabled !== false
|
|
47
|
+
) {
|
|
48
|
+
// No transcript bubble — the chips above the composer are the only UI.
|
|
49
|
+
// When the feature is disabled the message falls through to the generic
|
|
50
|
+
// tool bubble below (and is never auto-resumed), making the parked
|
|
51
|
+
// execution visible instead of silently swallowed.
|
|
52
|
+
return;
|
|
43
53
|
} else if (message.variant === "tool" && message.toolCall) {
|
|
44
54
|
if (!showToolCalls) return;
|
|
45
55
|
bubble = createToolBubble(message, config);
|
|
@@ -9,29 +9,50 @@ export interface SuggestionButtons {
|
|
|
9
9
|
session: AgentWidgetSession,
|
|
10
10
|
textarea: HTMLTextAreaElement,
|
|
11
11
|
messages?: AgentWidgetMessage[],
|
|
12
|
-
config?: AgentWidgetSuggestionChipsConfig
|
|
12
|
+
config?: AgentWidgetSuggestionChipsConfig,
|
|
13
|
+
opts?: SuggestionRenderOptions
|
|
13
14
|
) => void;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
export interface SuggestionRenderOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Chips pushed by the agent's `suggest_replies` tool rather than the
|
|
20
|
+
* static `suggestionChips` config. Skips the before-first-user-message
|
|
21
|
+
* gate (the caller already applied the latest-turn visibility rule) and
|
|
22
|
+
* dispatches `persona:suggestReplies:*` DOM events.
|
|
23
|
+
*/
|
|
24
|
+
agentPushed?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
export const createSuggestions = (container: HTMLElement): SuggestionButtons => {
|
|
17
28
|
const suggestionButtons: HTMLButtonElement[] = [];
|
|
29
|
+
// render() runs on every message change; only announce agent-pushed chips
|
|
30
|
+
// when the visible set actually changes, not on each re-render pass.
|
|
31
|
+
let lastAgentShownKey: string | null = null;
|
|
18
32
|
|
|
19
33
|
const render = (
|
|
20
34
|
chips: string[] | undefined,
|
|
21
35
|
session: AgentWidgetSession,
|
|
22
36
|
textarea: HTMLTextAreaElement,
|
|
23
37
|
messages?: AgentWidgetMessage[],
|
|
24
|
-
chipsConfig?: AgentWidgetSuggestionChipsConfig
|
|
38
|
+
chipsConfig?: AgentWidgetSuggestionChipsConfig,
|
|
39
|
+
opts?: SuggestionRenderOptions
|
|
25
40
|
) => {
|
|
26
41
|
container.innerHTML = "";
|
|
27
42
|
suggestionButtons.length = 0;
|
|
43
|
+
const agentPushed = opts?.agentPushed === true;
|
|
44
|
+
if (!agentPushed) lastAgentShownKey = null;
|
|
28
45
|
if (!chips || !chips.length) return;
|
|
29
46
|
|
|
30
|
-
// Hide suggestions after the first user message is sent
|
|
47
|
+
// Hide config suggestions after the first user message is sent.
|
|
48
|
+
// Agent-pushed chips skip this gate — their visibility is the caller's
|
|
49
|
+
// latest-turn rule (last suggest_replies call with no user message after).
|
|
31
50
|
// Use provided messages or get from session
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
if (!agentPushed) {
|
|
52
|
+
const messagesToCheck = messages ?? (session ? session.getMessages() : []);
|
|
53
|
+
const hasUserMessage = messagesToCheck.some((msg) => msg.role === "user");
|
|
54
|
+
if (hasUserMessage) return;
|
|
55
|
+
}
|
|
35
56
|
|
|
36
57
|
const fragment = document.createDocumentFragment();
|
|
37
58
|
const streaming = session ? session.isStreaming() : false;
|
|
@@ -79,12 +100,34 @@ export const createSuggestions = (container: HTMLElement): SuggestionButtons =>
|
|
|
79
100
|
btn.addEventListener("click", () => {
|
|
80
101
|
if (!session || session.isStreaming()) return;
|
|
81
102
|
textarea.value = "";
|
|
103
|
+
if (agentPushed) {
|
|
104
|
+
container.dispatchEvent(
|
|
105
|
+
new CustomEvent("persona:suggestReplies:selected", {
|
|
106
|
+
detail: { suggestion: chip },
|
|
107
|
+
bubbles: true,
|
|
108
|
+
composed: true,
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
}
|
|
82
112
|
session.sendMessage(chip);
|
|
83
113
|
});
|
|
84
114
|
fragment.appendChild(btn);
|
|
85
115
|
suggestionButtons.push(btn);
|
|
86
116
|
});
|
|
87
117
|
container.appendChild(fragment);
|
|
118
|
+
if (agentPushed) {
|
|
119
|
+
const shownKey = JSON.stringify(chips);
|
|
120
|
+
if (shownKey !== lastAgentShownKey) {
|
|
121
|
+
lastAgentShownKey = shownKey;
|
|
122
|
+
container.dispatchEvent(
|
|
123
|
+
new CustomEvent("persona:suggestReplies:shown", {
|
|
124
|
+
detail: { suggestions: [...chips] },
|
|
125
|
+
bubbles: true,
|
|
126
|
+
composed: true,
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
88
131
|
};
|
|
89
132
|
|
|
90
133
|
return {
|
package/src/index-core.ts
CHANGED
|
@@ -134,6 +134,21 @@ export {
|
|
|
134
134
|
parseAskUserQuestionPayload
|
|
135
135
|
} from "./components/ask-user-question-bubble";
|
|
136
136
|
|
|
137
|
+
export {
|
|
138
|
+
ASK_USER_QUESTION_CLIENT_TOOL,
|
|
139
|
+
ASK_USER_QUESTION_PARAMETERS_SCHEMA,
|
|
140
|
+
builtInClientToolsForDispatch
|
|
141
|
+
} from "./ask-user-question-tool";
|
|
142
|
+
|
|
143
|
+
export {
|
|
144
|
+
SUGGEST_REPLIES_TOOL_NAME,
|
|
145
|
+
SUGGEST_REPLIES_CLIENT_TOOL,
|
|
146
|
+
SUGGEST_REPLIES_PARAMETERS_SCHEMA,
|
|
147
|
+
isSuggestRepliesMessage,
|
|
148
|
+
parseSuggestRepliesPayload,
|
|
149
|
+
latestAgentSuggestions
|
|
150
|
+
} from "./suggest-replies-tool";
|
|
151
|
+
|
|
137
152
|
export { initAgentWidgetFn as initAgentWidget };
|
|
138
153
|
export {
|
|
139
154
|
createWidgetHostLayout,
|