@runtypelabs/persona 4.13.0 → 4.15.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 +4 -4
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-DveIaNx6.d.cts → types-BpWGm7_6.d.cts} +17 -0
- package/dist/animations/{types-DveIaNx6.d.ts → types-BpWGm7_6.d.ts} +17 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-QJ4LV2KW.js +1 -0
- package/dist/codegen.cjs +12 -11
- package/dist/codegen.js +14 -13
- package/dist/{context-mentions-ONG7A7M6.js → context-mentions-BTSTIBFC.js} +1 -1
- package/dist/context-mentions.cjs +11 -8
- package/dist/context-mentions.d.cts +339 -22
- package/dist/context-mentions.d.ts +339 -22
- package/dist/context-mentions.js +11 -8
- package/dist/index.cjs +68 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +483 -48
- package/dist/index.d.ts +483 -48
- package/dist/index.global.js +61 -58
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -69
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +5 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +339 -22
- package/dist/smart-dom-reader.d.ts +339 -22
- package/dist/theme-editor-preview.cjs +67 -67
- package/dist/theme-editor-preview.d.cts +355 -23
- package/dist/theme-editor-preview.d.ts +355 -23
- package/dist/theme-editor-preview.js +75 -75
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +376 -24
- package/dist/theme-editor.d.ts +376 -24
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +38 -0
- package/dist/theme-reference.d.ts +38 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/artifacts-session.test.ts +106 -0
- package/src/ask-user-question-tool.ts +10 -7
- package/src/client.test.ts +2 -2
- package/src/client.ts +4 -1
- package/src/components/artifact-card.ts +13 -2
- package/src/components/artifact-inline.ts +11 -2
- package/src/components/artifact-preview.ts +1 -1
- package/src/components/composer-builder.test.ts +2 -0
- package/src/components/composer-builder.ts +1 -1
- package/src/components/composer-parts.test.ts +105 -4
- package/src/components/composer-parts.ts +54 -16
- package/src/components/message-bubble.test.ts +7 -1
- package/src/components/message-bubble.ts +11 -8
- package/src/components/messages.ts +5 -2
- package/src/components/panel.ts +31 -5
- package/src/components/pill-composer-builder.test.ts +18 -0
- package/src/components/suggestions.test.ts +233 -0
- package/src/components/suggestions.ts +463 -81
- package/src/components/widget-view.ts +4 -0
- package/src/defaults.ts +5 -3
- package/src/generated/runtype-openapi-contract.ts +17 -0
- package/src/index-core.ts +52 -2
- package/src/launcher-global.ts +1 -1
- package/src/plugins/types.ts +71 -3
- package/src/presets.ts +3 -3
- package/src/session.ts +55 -12
- package/src/styles/widget-styles.test.ts +162 -0
- package/src/styles/widget.css +440 -1
- package/src/suggest-replies-tool.test.ts +233 -2
- package/src/suggest-replies-tool.ts +115 -39
- package/src/theme-editor/index.ts +5 -0
- package/src/theme-editor/presets.test.ts +43 -0
- package/src/theme-editor/presets.ts +101 -87
- package/src/theme-editor/preview-utils.test.ts +34 -0
- package/src/theme-editor/preview-utils.ts +77 -2
- package/src/theme-editor/sections.test.ts +72 -7
- package/src/theme-editor/sections.ts +113 -13
- package/src/theme-editor/state-preferences.test.ts +15 -0
- package/src/theme-editor/state.ts +46 -0
- package/src/theme-editor/types.ts +15 -1
- package/src/theme-editor/webmcp/tools.test.ts +13 -0
- package/src/theme-editor/webmcp/tools.ts +5 -0
- package/src/theme-reference.ts +9 -2
- package/src/types/theme.ts +32 -0
- package/src/types.ts +291 -25
- package/src/ui.artifact-pane-gating.test.ts +5 -5
- package/src/ui.config-preferences.test.ts +166 -0
- package/src/ui.detached-panel.test.ts +1 -1
- package/src/ui.suggest-replies.test.ts +1066 -1
- package/src/ui.ts +311 -45
- package/src/ui.typing-indicator.test.ts +135 -0
- package/src/utils/artifact-display.test.ts +180 -16
- package/src/utils/artifact-display.ts +155 -14
- package/src/utils/artifact-file.test.ts +1 -1
- package/src/utils/code-generators.test.ts +40 -0
- package/src/utils/code-generators.ts +22 -0
- package/src/utils/config-merge.test.ts +84 -0
- package/src/utils/config-merge.ts +6 -1
- package/src/utils/feature-preferences.test.ts +326 -0
- package/src/utils/feature-preferences.ts +498 -0
- package/src/utils/icons.test.ts +26 -0
- package/src/utils/icons.ts +8 -0
- package/src/utils/theme.test.ts +171 -0
- package/src/utils/tokens.ts +71 -7
- package/dist/chunk-MMUPR2JW.js +0 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
SUGGEST_REPLIES_TOOL_NAME,
|
|
7
7
|
latestAgentSuggestions,
|
|
8
8
|
parseSuggestRepliesPayload,
|
|
9
|
+
resolveFollowUpsFeature,
|
|
9
10
|
} from "./suggest-replies-tool";
|
|
10
11
|
import {
|
|
11
12
|
ASK_USER_QUESTION_CLIENT_TOOL,
|
|
@@ -26,14 +27,27 @@ describe("SUGGEST_REPLIES_CLIENT_TOOL definition", () => {
|
|
|
26
27
|
expect(SUGGEST_REPLIES_CLIENT_TOOL.annotations?.readOnlyHint).toBe(true);
|
|
27
28
|
});
|
|
28
29
|
|
|
29
|
-
it("bounds suggestions to
|
|
30
|
+
it("bounds suggestions to semantic-only object items", () => {
|
|
30
31
|
const suggestions =
|
|
31
32
|
SUGGEST_REPLIES_PARAMETERS_SCHEMA.properties.suggestions;
|
|
32
33
|
expect(suggestions.minItems).toBe(1);
|
|
33
34
|
expect(suggestions.maxItems).toBe(SUGGEST_REPLIES_MAX);
|
|
34
|
-
expect(suggestions.items.
|
|
35
|
+
expect(suggestions.items.type).toBe("object");
|
|
36
|
+
expect(suggestions.items.required).toEqual(["label"]);
|
|
37
|
+
expect(suggestions.items.additionalProperties).toBe(false);
|
|
35
38
|
expect(SUGGEST_REPLIES_PARAMETERS_SCHEMA.required).toEqual(["suggestions"]);
|
|
36
39
|
});
|
|
40
|
+
|
|
41
|
+
it("advertises no oneOf union and no presentation fields", () => {
|
|
42
|
+
const items = SUGGEST_REPLIES_PARAMETERS_SCHEMA.properties.suggestions
|
|
43
|
+
.items as Record<string, unknown>;
|
|
44
|
+
expect(items.oneOf).toBeUndefined();
|
|
45
|
+
expect(Object.keys(items.properties as Record<string, unknown>)).toEqual([
|
|
46
|
+
"label",
|
|
47
|
+
"prompt",
|
|
48
|
+
"description",
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
37
51
|
});
|
|
38
52
|
|
|
39
53
|
describe("parseSuggestRepliesPayload", () => {
|
|
@@ -55,6 +69,44 @@ describe("parseSuggestRepliesPayload", () => {
|
|
|
55
69
|
).toEqual(["A", "B"]);
|
|
56
70
|
});
|
|
57
71
|
|
|
72
|
+
it("parses rich items while dropping malformed objects", () => {
|
|
73
|
+
expect(
|
|
74
|
+
parseSuggestRepliesPayload({
|
|
75
|
+
suggestions: [
|
|
76
|
+
{
|
|
77
|
+
label: " Pricing ",
|
|
78
|
+
prompt: " Tell me about pricing ",
|
|
79
|
+
description: " Compare plans ",
|
|
80
|
+
},
|
|
81
|
+
{ label: " " },
|
|
82
|
+
{ nope: true },
|
|
83
|
+
],
|
|
84
|
+
}),
|
|
85
|
+
).toEqual([
|
|
86
|
+
{
|
|
87
|
+
label: "Pricing",
|
|
88
|
+
prompt: "Tell me about pricing",
|
|
89
|
+
description: "Compare plans",
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("strips presentation fields a model may send anyway", () => {
|
|
95
|
+
expect(
|
|
96
|
+
parseSuggestRepliesPayload({
|
|
97
|
+
suggestions: [
|
|
98
|
+
{
|
|
99
|
+
id: "pricing",
|
|
100
|
+
label: "Pricing",
|
|
101
|
+
icon: "dollar-sign",
|
|
102
|
+
behavior: "fill",
|
|
103
|
+
emphasis: "primary",
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
}),
|
|
107
|
+
).toEqual([{ label: "Pricing" }]);
|
|
108
|
+
});
|
|
109
|
+
|
|
58
110
|
it("truncates past the cap with a console warning", () => {
|
|
59
111
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
60
112
|
const result = parseSuggestRepliesPayload({
|
|
@@ -145,6 +197,172 @@ describe("latestAgentSuggestions", () => {
|
|
|
145
197
|
});
|
|
146
198
|
});
|
|
147
199
|
|
|
200
|
+
describe("resolveFollowUpsFeature", () => {
|
|
201
|
+
const cfg = (config: Partial<AgentWidgetConfig>): AgentWidgetConfig =>
|
|
202
|
+
config as AgentWidgetConfig;
|
|
203
|
+
|
|
204
|
+
it("defaults to enabled and not exposed", () => {
|
|
205
|
+
expect(resolveFollowUpsFeature(undefined)).toEqual({
|
|
206
|
+
enabled: true,
|
|
207
|
+
expose: false,
|
|
208
|
+
});
|
|
209
|
+
expect(resolveFollowUpsFeature(cfg({}))).toEqual({
|
|
210
|
+
enabled: true,
|
|
211
|
+
expose: false,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("reads each key from suggestions.followUps", () => {
|
|
216
|
+
expect(
|
|
217
|
+
resolveFollowUpsFeature(
|
|
218
|
+
cfg({ suggestions: { followUps: { expose: true } } }),
|
|
219
|
+
),
|
|
220
|
+
).toEqual({ enabled: true, expose: true });
|
|
221
|
+
expect(
|
|
222
|
+
resolveFollowUpsFeature(
|
|
223
|
+
cfg({ suggestions: { followUps: { enabled: false } } }),
|
|
224
|
+
),
|
|
225
|
+
).toEqual({ enabled: false, expose: false });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("falls back per key to the deprecated features.suggestReplies alias", () => {
|
|
229
|
+
expect(
|
|
230
|
+
resolveFollowUpsFeature(
|
|
231
|
+
cfg({
|
|
232
|
+
features: { suggestReplies: { enabled: true, expose: true } },
|
|
233
|
+
suggestions: { followUps: { variant: "card" } },
|
|
234
|
+
}),
|
|
235
|
+
),
|
|
236
|
+
).toEqual({ enabled: true, expose: true });
|
|
237
|
+
// New home supplies `expose`, alias still supplies `enabled`.
|
|
238
|
+
expect(
|
|
239
|
+
resolveFollowUpsFeature(
|
|
240
|
+
cfg({
|
|
241
|
+
features: { suggestReplies: { enabled: false } },
|
|
242
|
+
suggestions: { followUps: { expose: true } },
|
|
243
|
+
}),
|
|
244
|
+
),
|
|
245
|
+
).toEqual({ enabled: false, expose: false });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("does not silently re-enable when only presentation keys are added", () => {
|
|
249
|
+
expect(
|
|
250
|
+
resolveFollowUpsFeature(
|
|
251
|
+
cfg({
|
|
252
|
+
features: { suggestReplies: { enabled: false } },
|
|
253
|
+
suggestions: { followUps: { variant: "card" } },
|
|
254
|
+
}),
|
|
255
|
+
),
|
|
256
|
+
).toEqual({ enabled: false, expose: false });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("lets suggestions.followUps win on conflict", () => {
|
|
260
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
261
|
+
expect(
|
|
262
|
+
resolveFollowUpsFeature(
|
|
263
|
+
cfg({
|
|
264
|
+
features: { suggestReplies: { enabled: false, expose: false } },
|
|
265
|
+
suggestions: { followUps: { enabled: true, expose: true } },
|
|
266
|
+
}),
|
|
267
|
+
),
|
|
268
|
+
).toEqual({ enabled: true, expose: true });
|
|
269
|
+
expect(
|
|
270
|
+
resolveFollowUpsFeature(
|
|
271
|
+
cfg({
|
|
272
|
+
features: { suggestReplies: { enabled: true, expose: true } },
|
|
273
|
+
suggestions: { followUps: { enabled: false } },
|
|
274
|
+
}),
|
|
275
|
+
),
|
|
276
|
+
).toEqual({ enabled: false, expose: false });
|
|
277
|
+
warn.mockRestore();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("forces expose off when enabled resolves false", () => {
|
|
281
|
+
expect(
|
|
282
|
+
resolveFollowUpsFeature(
|
|
283
|
+
cfg({
|
|
284
|
+
suggestions: { followUps: { enabled: false, expose: true } },
|
|
285
|
+
}),
|
|
286
|
+
),
|
|
287
|
+
).toEqual({ enabled: false, expose: false });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("resolveFollowUpsFeature conflict warnings", () => {
|
|
293
|
+
const cfg = (config: Partial<AgentWidgetConfig>): AgentWidgetConfig =>
|
|
294
|
+
config as AgentWidgetConfig;
|
|
295
|
+
|
|
296
|
+
// The dedupe Set is module-level, so a shared conflict signature would let
|
|
297
|
+
// one test mask another. Each case gets a fresh module.
|
|
298
|
+
const freshResolve = async () => {
|
|
299
|
+
vi.resetModules();
|
|
300
|
+
return (await import("./suggest-replies-tool")).resolveFollowUpsFeature;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
it("warns once per key without debug", async () => {
|
|
304
|
+
const resolve = await freshResolve();
|
|
305
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
306
|
+
|
|
307
|
+
const production = cfg({
|
|
308
|
+
features: { suggestReplies: { enabled: true, expose: false } },
|
|
309
|
+
suggestions: { followUps: { enabled: false, expose: true } },
|
|
310
|
+
});
|
|
311
|
+
resolve(production);
|
|
312
|
+
resolve(production);
|
|
313
|
+
expect(warn).toHaveBeenCalledTimes(2);
|
|
314
|
+
expect(warn.mock.calls.map((call) => String(call[0]))).toEqual([
|
|
315
|
+
expect.stringContaining("suggestions.followUps.enabled"),
|
|
316
|
+
expect.stringContaining("suggestions.followUps.expose"),
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
warn.mockRestore();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("warns with debug on too", async () => {
|
|
323
|
+
const resolve = await freshResolve();
|
|
324
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
325
|
+
|
|
326
|
+
const debug = cfg({
|
|
327
|
+
debug: true,
|
|
328
|
+
features: { suggestReplies: { enabled: true, expose: false } },
|
|
329
|
+
suggestions: { followUps: { enabled: false, expose: true } },
|
|
330
|
+
});
|
|
331
|
+
resolve(debug);
|
|
332
|
+
resolve(debug);
|
|
333
|
+
expect(warn).toHaveBeenCalledTimes(2);
|
|
334
|
+
|
|
335
|
+
warn.mockRestore();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("stays silent when a rebuilt config object repeats the same conflict", async () => {
|
|
339
|
+
const resolve = await freshResolve();
|
|
340
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
341
|
+
const conflicting = () =>
|
|
342
|
+
cfg({
|
|
343
|
+
features: { suggestReplies: { enabled: true } },
|
|
344
|
+
suggestions: { followUps: { enabled: false } },
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// `controller.update()` rebuilds the config object on every call.
|
|
348
|
+
resolve(conflicting());
|
|
349
|
+
resolve(conflicting());
|
|
350
|
+
resolve(conflicting());
|
|
351
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
352
|
+
|
|
353
|
+
// A different conflict is still a distinct developer error.
|
|
354
|
+
resolve(
|
|
355
|
+
cfg({
|
|
356
|
+
features: { suggestReplies: { expose: true } },
|
|
357
|
+
suggestions: { followUps: { expose: false } },
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
expect(warn).toHaveBeenCalledTimes(2);
|
|
361
|
+
|
|
362
|
+
warn.mockRestore();
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
148
366
|
describe("builtInClientToolsForDispatch - suggest_replies gating", () => {
|
|
149
367
|
it("returns nothing by default (expose is opt-in)", () => {
|
|
150
368
|
expect(builtInClientToolsForDispatch(undefined)).toEqual([]);
|
|
@@ -173,6 +391,19 @@ describe("builtInClientToolsForDispatch - suggest_replies gating", () => {
|
|
|
173
391
|
).toEqual([]);
|
|
174
392
|
});
|
|
175
393
|
|
|
394
|
+
it("reads expose from suggestions.followUps too", () => {
|
|
395
|
+
expect(
|
|
396
|
+
builtInClientToolsForDispatch({
|
|
397
|
+
suggestions: { followUps: { expose: true } },
|
|
398
|
+
} as AgentWidgetConfig),
|
|
399
|
+
).toEqual([SUGGEST_REPLIES_CLIENT_TOOL]);
|
|
400
|
+
expect(
|
|
401
|
+
builtInClientToolsForDispatch({
|
|
402
|
+
suggestions: { followUps: { expose: true, enabled: false } },
|
|
403
|
+
} as AgentWidgetConfig),
|
|
404
|
+
).toEqual([]);
|
|
405
|
+
});
|
|
406
|
+
|
|
176
407
|
it("composes with ask_user_question, ask first", () => {
|
|
177
408
|
expect(
|
|
178
409
|
builtInClientToolsForDispatch({
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
* Built-in `suggest_replies` client tool.
|
|
3
3
|
*
|
|
4
4
|
* The widget can advertise this tool to the agent on every dispatch via
|
|
5
|
-
* `clientTools[]` (set `
|
|
5
|
+
* `clientTools[]` (set `suggestions.followUps.expose: true`): the same
|
|
6
6
|
* wire surface as `ask_user_question` and WebMCP page tools. When the model
|
|
7
|
-
* calls it, the execution pauses with a `step_await`
|
|
8
|
-
*
|
|
9
|
-
* widget resolves it FIRE-AND-FORGET: it renders the
|
|
10
|
-
*
|
|
11
|
-
* canned "shown" result, so the
|
|
12
|
-
* user. Tapping a chip sends its text verbatim as the user's next message.
|
|
7
|
+
* calls it, the execution pauses with a `step_await` (a local-tool pause
|
|
8
|
+
* carries no `awaitReason`; that discriminator is internal). Unlike
|
|
9
|
+
* `ask_user_question`, the widget resolves it FIRE-AND-FORGET: it renders the
|
|
10
|
+
* configured follow-up surface and immediately resumes the execution with a
|
|
11
|
+
* canned "shown" result, so the turn completes without waiting on the user.
|
|
13
12
|
*
|
|
14
13
|
* Chip visibility is DERIVED state, not imperative show/hide: the chips of
|
|
15
14
|
* the last `suggest_replies` tool message with no user message after it are
|
|
@@ -21,6 +20,7 @@
|
|
|
21
20
|
import type {
|
|
22
21
|
AgentWidgetConfig,
|
|
23
22
|
AgentWidgetMessage,
|
|
23
|
+
AgentWidgetSuggestion,
|
|
24
24
|
ClientToolDefinition,
|
|
25
25
|
} from "./types";
|
|
26
26
|
|
|
@@ -30,9 +30,10 @@ export const SUGGEST_REPLIES_TOOL_NAME = "suggest_replies";
|
|
|
30
30
|
export const SUGGEST_REPLIES_MAX = 4;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* JSON Schema for the tool's parameters.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* JSON Schema for the tool's parameters. The model owns semantics only: id,
|
|
34
|
+
* icon, behavior, and emphasis are presentation, and hosts set them with the
|
|
35
|
+
* `transformSuggestions` plugin hook. A single object type is used because
|
|
36
|
+
* strict structured-output modes reject or flatten `oneOf`.
|
|
36
37
|
*/
|
|
37
38
|
export const SUGGEST_REPLIES_PARAMETERS_SCHEMA = {
|
|
38
39
|
type: "object",
|
|
@@ -43,7 +44,16 @@ export const SUGGEST_REPLIES_PARAMETERS_SCHEMA = {
|
|
|
43
44
|
maxItems: SUGGEST_REPLIES_MAX,
|
|
44
45
|
description:
|
|
45
46
|
"1-4 short, distinct follow-up replies, phrased in the user's voice.",
|
|
46
|
-
items: {
|
|
47
|
+
items: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
label: { type: "string", minLength: 1, maxLength: 80 },
|
|
51
|
+
prompt: { type: "string", minLength: 1, maxLength: 500 },
|
|
52
|
+
description: { type: "string", minLength: 1, maxLength: 160 },
|
|
53
|
+
},
|
|
54
|
+
required: ["label"],
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
},
|
|
47
57
|
},
|
|
48
58
|
},
|
|
49
59
|
required: ["suggestions"],
|
|
@@ -52,7 +62,7 @@ export const SUGGEST_REPLIES_PARAMETERS_SCHEMA = {
|
|
|
52
62
|
|
|
53
63
|
/**
|
|
54
64
|
* The `ClientToolDefinition` shipped on `dispatch.clientTools[]` when
|
|
55
|
-
* `
|
|
65
|
+
* `suggestions.followUps.expose` is on. Exported so integrators who prefer
|
|
56
66
|
* declaring the tool server-side (a flow's `runtimeTools`) can reuse the same
|
|
57
67
|
* description and schema instead of hand-writing them.
|
|
58
68
|
*/
|
|
@@ -60,12 +70,15 @@ export const SUGGEST_REPLIES_CLIENT_TOOL: ClientToolDefinition = {
|
|
|
60
70
|
name: SUGGEST_REPLIES_TOOL_NAME,
|
|
61
71
|
description:
|
|
62
72
|
"Offer the user tappable quick-reply suggestions for their next message. " +
|
|
63
|
-
"Call at most once per turn, as the
|
|
64
|
-
"complete.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
"
|
|
73
|
+
"Call at most once per turn, as the last action after your reply text is " +
|
|
74
|
+
"complete. Give each suggestion a `label` with the short visible text, an " +
|
|
75
|
+
"optional `prompt` with the fuller text placed as the user's message " +
|
|
76
|
+
"(defaults to the label), and an optional `description` with one line of " +
|
|
77
|
+
"supporting copy. Suggestions are sent as the user's next message, so " +
|
|
78
|
+
'phrase prompts in the user\'s voice (e.g. "Tell me more about pricing"). ' +
|
|
79
|
+
"Keep them short and distinct. The result only confirms the suggestions " +
|
|
80
|
+
"were shown: do not add further commentary after calling this tool; end " +
|
|
81
|
+
"your turn.",
|
|
69
82
|
parametersSchema: SUGGEST_REPLIES_PARAMETERS_SCHEMA,
|
|
70
83
|
origin: "sdk",
|
|
71
84
|
annotations: { readOnlyHint: true },
|
|
@@ -90,11 +103,15 @@ export const isSuggestRepliesMessage = (
|
|
|
90
103
|
message.toolCall?.name === SUGGEST_REPLIES_TOOL_NAME;
|
|
91
104
|
|
|
92
105
|
/**
|
|
93
|
-
* Tolerant parse of a `suggest_replies` tool call's args into
|
|
94
|
-
*
|
|
95
|
-
*
|
|
106
|
+
* Tolerant parse of a `suggest_replies` tool call's args into suggestion
|
|
107
|
+
* items. Plain strings stay strings for older flows that emit
|
|
108
|
+
* `suggestions: string[]`; objects keep only label, prompt, and description,
|
|
109
|
+
* so a model that ignores `additionalProperties` cannot set presentation.
|
|
110
|
+
* Hosts add id, icon, behavior, and emphasis via `transformSuggestions`.
|
|
96
111
|
*/
|
|
97
|
-
export const parseSuggestRepliesPayload = (
|
|
112
|
+
export const parseSuggestRepliesPayload = (
|
|
113
|
+
args: unknown
|
|
114
|
+
): AgentWidgetSuggestion[] => {
|
|
98
115
|
let parsed: unknown = args;
|
|
99
116
|
if (typeof parsed === "string") {
|
|
100
117
|
try {
|
|
@@ -106,17 +123,32 @@ export const parseSuggestRepliesPayload = (args: unknown): string[] => {
|
|
|
106
123
|
const raw = (parsed as { suggestions?: unknown } | null | undefined)
|
|
107
124
|
?.suggestions;
|
|
108
125
|
if (!Array.isArray(raw)) return [];
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
const suggestions = raw.flatMap<AgentWidgetSuggestion>((item) => {
|
|
127
|
+
if (typeof item === "string") {
|
|
128
|
+
const value = item.trim();
|
|
129
|
+
return value ? [value] : [];
|
|
130
|
+
}
|
|
131
|
+
if (!item || typeof item !== "object") return [];
|
|
132
|
+
const value = item as Record<string, unknown>;
|
|
133
|
+
if (typeof value.label !== "string" || !value.label.trim()) return [];
|
|
134
|
+
const suggestion: Exclude<AgentWidgetSuggestion, string> = {
|
|
135
|
+
label: value.label.trim(),
|
|
136
|
+
};
|
|
137
|
+
if (typeof value.prompt === "string" && value.prompt.trim()) {
|
|
138
|
+
suggestion.prompt = value.prompt.trim();
|
|
139
|
+
}
|
|
140
|
+
if (typeof value.description === "string" && value.description.trim()) {
|
|
141
|
+
suggestion.description = value.description.trim();
|
|
142
|
+
}
|
|
143
|
+
return [suggestion];
|
|
144
|
+
});
|
|
145
|
+
if (suggestions.length > SUGGEST_REPLIES_MAX) {
|
|
114
146
|
console.warn(
|
|
115
|
-
`[persona] suggest_replies: ${
|
|
147
|
+
`[persona] suggest_replies: ${suggestions.length} suggestions exceeds the cap of ${SUGGEST_REPLIES_MAX}; extra suggestions dropped.`,
|
|
116
148
|
);
|
|
117
|
-
return
|
|
149
|
+
return suggestions.slice(0, SUGGEST_REPLIES_MAX);
|
|
118
150
|
}
|
|
119
|
-
return
|
|
151
|
+
return suggestions;
|
|
120
152
|
};
|
|
121
153
|
|
|
122
154
|
/**
|
|
@@ -127,7 +159,7 @@ export const parseSuggestRepliesPayload = (args: unknown): string[] => {
|
|
|
127
159
|
*/
|
|
128
160
|
export const latestAgentSuggestions = (
|
|
129
161
|
messages: AgentWidgetMessage[],
|
|
130
|
-
):
|
|
162
|
+
): AgentWidgetSuggestion[] | null => {
|
|
131
163
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
132
164
|
const message = messages[i];
|
|
133
165
|
if (message.role === "user") return null;
|
|
@@ -138,15 +170,59 @@ export const latestAgentSuggestions = (
|
|
|
138
170
|
return null;
|
|
139
171
|
};
|
|
140
172
|
|
|
173
|
+
export type ResolvedFollowUpsFeature = {
|
|
174
|
+
/** Widget renders follow-up chips and auto-resumes the tool call. */
|
|
175
|
+
enabled: boolean;
|
|
176
|
+
/** Tool ships on `dispatch.clientTools[]`. */
|
|
177
|
+
expose: boolean;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Keyed on the conflict itself, not the config object: `controller.update()`
|
|
181
|
+
// rebuilds the config on every call, so an object-keyed dedupe re-warns
|
|
182
|
+
// forever. Trade-off: two widgets with the identical conflict warn once total
|
|
183
|
+
// per page load.
|
|
184
|
+
const conflictWarnings = new Set<string>();
|
|
185
|
+
|
|
186
|
+
/** Always warns, debug or not: a config conflict is a developer error. */
|
|
187
|
+
const warnConflict = (
|
|
188
|
+
key: "enabled" | "expose",
|
|
189
|
+
legacyValue: boolean,
|
|
190
|
+
newValue: boolean,
|
|
191
|
+
): void => {
|
|
192
|
+
const signature = `${key}:${legacyValue}:${newValue}`;
|
|
193
|
+
if (conflictWarnings.has(signature)) return;
|
|
194
|
+
conflictWarnings.add(signature);
|
|
195
|
+
console.warn(
|
|
196
|
+
`[persona] suggestions.followUps.${key} and features.suggestReplies.${key} disagree; suggestions.followUps.${key} wins.`,
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
141
200
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
201
|
+
* Single source of truth for the follow-ups capability flags. Resolution is
|
|
202
|
+
* per key so a `features.suggestReplies` embed that adds presentation-only
|
|
203
|
+
* `suggestions.followUps` keys is not silently re-enabled. `expose` is forced
|
|
204
|
+
* off when `enabled` resolves false: advertising the tool without the widget
|
|
205
|
+
* auto-resolving it parks the execution on a generic tool bubble.
|
|
146
206
|
*/
|
|
147
|
-
export const
|
|
207
|
+
export const resolveFollowUpsFeature = (
|
|
148
208
|
config: AgentWidgetConfig | undefined,
|
|
149
|
-
):
|
|
150
|
-
const
|
|
151
|
-
|
|
209
|
+
): ResolvedFollowUpsFeature => {
|
|
210
|
+
const followUps = config?.suggestions?.followUps;
|
|
211
|
+
const legacy = config?.features?.suggestReplies;
|
|
212
|
+
const warnIfConflicting = (key: "enabled" | "expose"): void => {
|
|
213
|
+
const newValue = followUps?.[key];
|
|
214
|
+
const legacyValue = legacy?.[key];
|
|
215
|
+
if (newValue === undefined || legacyValue === undefined) return;
|
|
216
|
+
if (newValue === legacyValue) return;
|
|
217
|
+
warnConflict(key, legacyValue, newValue);
|
|
218
|
+
};
|
|
219
|
+
if (config) {
|
|
220
|
+
warnIfConflicting("enabled");
|
|
221
|
+
warnIfConflicting("expose");
|
|
222
|
+
}
|
|
223
|
+
const enabled = followUps?.enabled ?? legacy?.enabled ?? true;
|
|
224
|
+
const expose = enabled
|
|
225
|
+
? followUps?.expose ?? legacy?.expose ?? false
|
|
226
|
+
: false;
|
|
227
|
+
return { enabled, expose };
|
|
152
228
|
};
|
|
@@ -43,6 +43,8 @@ export {
|
|
|
43
43
|
COMPONENT_COLOR_SECTIONS,
|
|
44
44
|
CONFIGURE_SECTIONS,
|
|
45
45
|
CONFIGURE_SUB_GROUPS,
|
|
46
|
+
STREAM_AND_ARTIFACTS_SUB_GROUP,
|
|
47
|
+
ARTIFACT_DISPLAY_PREFERENCE_SECTION,
|
|
46
48
|
ALL_TABS,
|
|
47
49
|
scopeSection,
|
|
48
50
|
findSection,
|
|
@@ -90,6 +92,9 @@ export type {
|
|
|
90
92
|
TranscriptStreamFrame,
|
|
91
93
|
BuildTranscriptStreamFramesOptions,
|
|
92
94
|
} from './preview-utils';
|
|
95
|
+
// Type-only: the full preview implementation stays in
|
|
96
|
+
// `@runtypelabs/persona/theme-editor/preview`.
|
|
97
|
+
export type { CompareMode, PreviewShellMode } from './preview';
|
|
93
98
|
|
|
94
99
|
// Role mappings (Interface Roles editor)
|
|
95
100
|
export {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { AgentWidgetConfig } from '../types';
|
|
3
|
+
import { getActiveTheme, themeToCssVariables } from '../utils/theme';
|
|
4
|
+
import { BUILT_IN_PRESETS, getThemeEditorPreset } from './presets';
|
|
5
|
+
|
|
6
|
+
describe('theme editor presets', () => {
|
|
7
|
+
it('offers the paired Persona default before the single-scheme presets', () => {
|
|
8
|
+
const preset = getThemeEditorPreset('persona-default');
|
|
9
|
+
|
|
10
|
+
expect(BUILT_IN_PRESETS[0]).toBe(preset);
|
|
11
|
+
expect(preset).toMatchObject({
|
|
12
|
+
name: 'Persona Default',
|
|
13
|
+
tags: ['light', 'dark', 'adaptive'],
|
|
14
|
+
preview: { surface: '#ffffff' },
|
|
15
|
+
darkPreview: { surface: '#1f2937' },
|
|
16
|
+
});
|
|
17
|
+
expect(preset?.darkTheme).toBeDefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('resolves the paired default to distinct light and dark surfaces', () => {
|
|
21
|
+
const preset = getThemeEditorPreset('persona-default')!;
|
|
22
|
+
const config = {
|
|
23
|
+
theme: preset.theme,
|
|
24
|
+
darkTheme: preset.darkTheme,
|
|
25
|
+
} as AgentWidgetConfig;
|
|
26
|
+
|
|
27
|
+
const light = themeToCssVariables(getActiveTheme({ ...config, colorScheme: 'light' }));
|
|
28
|
+
const dark = themeToCssVariables(getActiveTheme({ ...config, colorScheme: 'dark' }));
|
|
29
|
+
|
|
30
|
+
expect(light).toMatchObject({
|
|
31
|
+
'--persona-surface': '#f9fafb',
|
|
32
|
+
'--persona-background': '#f9fafb',
|
|
33
|
+
'--persona-text': '#111827',
|
|
34
|
+
'--persona-border': '#e5e7eb',
|
|
35
|
+
});
|
|
36
|
+
expect(dark).toMatchObject({
|
|
37
|
+
'--persona-surface': '#1f2937',
|
|
38
|
+
'--persona-background': '#111827',
|
|
39
|
+
'--persona-text': '#f3f4f6',
|
|
40
|
+
'--persona-border': '#374151',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|