@runtypelabs/persona 3.16.0 → 3.18.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 +142 -0
- package/dist/animations/glyph-cycle.cjs +279 -0
- package/dist/animations/glyph-cycle.d.cts +5 -0
- package/dist/animations/glyph-cycle.d.ts +5 -0
- package/dist/animations/glyph-cycle.js +252 -0
- package/dist/animations/types-cwY5HaFD.d.cts +307 -0
- package/dist/animations/types-cwY5HaFD.d.ts +307 -0
- package/dist/animations/wipe.cjs +107 -0
- package/dist/animations/wipe.d.cts +5 -0
- package/dist/animations/wipe.d.ts +5 -0
- package/dist/animations/wipe.js +80 -0
- package/dist/index.cjs +49 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +504 -1
- package/dist/index.d.ts +504 -1
- package/dist/index.global.js +143 -88
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +49 -48
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +85 -0
- package/dist/testing.d.cts +39 -0
- package/dist/testing.d.ts +39 -0
- package/dist/testing.js +56 -0
- package/dist/theme-editor.cjs +2095 -207
- package/dist/theme-editor.d.cts +432 -2
- package/dist/theme-editor.d.ts +432 -2
- package/dist/theme-editor.js +2093 -207
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +14 -0
- package/dist/theme-reference.d.ts +14 -0
- package/dist/widget.css +565 -0
- package/package.json +20 -3
- package/src/animations/glyph-cycle.ts +332 -0
- package/src/animations/wipe.ts +66 -0
- package/src/client.test.ts +275 -0
- package/src/client.ts +99 -0
- package/src/components/ask-user-question-bubble.test.ts +583 -0
- package/src/components/ask-user-question-bubble.ts +924 -0
- package/src/components/composer-builder.ts +61 -10
- package/src/components/message-bubble.test.ts +181 -2
- package/src/components/message-bubble.ts +209 -14
- package/src/components/messages.ts +33 -1
- package/src/components/panel.ts +45 -5
- package/src/defaults.ts +37 -0
- package/src/index-global.ts +31 -0
- package/src/index.ts +34 -1
- package/src/plugins/types.ts +57 -0
- package/src/session.test.ts +276 -1
- package/src/session.ts +247 -3
- package/src/styles/widget.css +565 -0
- package/src/testing/index.ts +11 -0
- package/src/testing/mock-stream.test.ts +80 -0
- package/src/testing/mock-stream.ts +94 -0
- package/src/testing.ts +2 -0
- package/src/theme-editor/index.ts +4 -0
- package/src/theme-editor/preview-utils.test.ts +60 -0
- package/src/theme-editor/preview-utils.ts +129 -0
- package/src/theme-editor/sections.test.ts +19 -0
- package/src/theme-editor/sections.ts +84 -1
- package/src/types/theme.ts +15 -0
- package/src/types.ts +360 -0
- package/src/ui.ask-user-question-plugin.test.ts +649 -0
- package/src/ui.stop-button.test.ts +165 -0
- package/src/ui.ts +706 -11
- package/src/utils/message-fingerprint.ts +2 -0
- package/src/utils/morph.ts +7 -0
- package/src/utils/storage.ts +10 -2
- package/src/utils/stream-animation.test.ts +417 -0
- package/src/utils/stream-animation.ts +449 -0
- package/src/utils/theme.test.ts +36 -0
- package/src/utils/tokens.ts +23 -0
package/src/session.ts
CHANGED
|
@@ -81,9 +81,22 @@ export class AgentWidgetSession {
|
|
|
81
81
|
this.messages = this.sortMessages(this.messages);
|
|
82
82
|
this.client = new AgentWidgetClient(config);
|
|
83
83
|
|
|
84
|
+
// Hydrate artifacts from config (mirrors `initialMessages`). Restored
|
|
85
|
+
// records are forced to `status: "complete"` — a mid-stream artifact should
|
|
86
|
+
// never reappear after a refresh with its skeleton still showing.
|
|
87
|
+
for (const rec of config.initialArtifacts ?? []) {
|
|
88
|
+
this.artifacts.set(rec.id, { ...rec, status: "complete" });
|
|
89
|
+
}
|
|
90
|
+
if (config.initialSelectedArtifactId != null) {
|
|
91
|
+
this.selectedArtifactId = config.initialSelectedArtifactId;
|
|
92
|
+
}
|
|
93
|
+
|
|
84
94
|
if (this.messages.length) {
|
|
85
95
|
this.callbacks.onMessagesChanged([...this.messages]);
|
|
86
96
|
}
|
|
97
|
+
if (this.artifacts.size > 0) {
|
|
98
|
+
this.emitArtifactsState();
|
|
99
|
+
}
|
|
87
100
|
this.callbacks.onStatusChanged(this.status);
|
|
88
101
|
}
|
|
89
102
|
|
|
@@ -968,9 +981,202 @@ export class AgentWidgetSession {
|
|
|
968
981
|
}
|
|
969
982
|
}
|
|
970
983
|
|
|
984
|
+
/**
|
|
985
|
+
* Resolve a paused `ask_user_question` LOCAL tool call.
|
|
986
|
+
*
|
|
987
|
+
* When the server emits `step_await` for `ask_user_question`, the widget
|
|
988
|
+
* renders the answer-pill sheet and calls this method once the user
|
|
989
|
+
* picks. Steps:
|
|
990
|
+
* 1. POST the answer to `/resume` via `client.resumeFlow`.
|
|
991
|
+
* 2. Pipe the resulting SSE stream through `connectStream()` so the
|
|
992
|
+
* paused agent execution continues.
|
|
993
|
+
* 3. Append a user-visible bubble with the answer text so the
|
|
994
|
+
* transcript reads naturally.
|
|
995
|
+
*/
|
|
996
|
+
/**
|
|
997
|
+
* Persist in-progress answers and the current page index for a multi-question
|
|
998
|
+
* `ask_user_question` payload, so a refresh resumes on the same page with
|
|
999
|
+
* prior answers intact. Called by ui.ts on every Back/Next/pick interaction.
|
|
1000
|
+
*/
|
|
1001
|
+
public persistAskUserQuestionProgress(
|
|
1002
|
+
toolMessage: AgentWidgetMessage,
|
|
1003
|
+
progress: {
|
|
1004
|
+
answers: Record<string, string | string[]>;
|
|
1005
|
+
currentIndex: number;
|
|
1006
|
+
}
|
|
1007
|
+
): void {
|
|
1008
|
+
const current = this.messages.find((m) => m.id === toolMessage.id);
|
|
1009
|
+
if (!current) return;
|
|
1010
|
+
this.upsertMessage({
|
|
1011
|
+
...current,
|
|
1012
|
+
agentMetadata: {
|
|
1013
|
+
...current.agentMetadata,
|
|
1014
|
+
askUserQuestionAnswers: progress.answers,
|
|
1015
|
+
askUserQuestionIndex: progress.currentIndex,
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Flip an `ask_user_question` tool message from awaiting → answered so
|
|
1022
|
+
* render passes stop re-mounting its answer-pill sheet. Idempotent.
|
|
1023
|
+
* When `answers` is provided, persists the full structured answer Record
|
|
1024
|
+
* atomically with the answered flag — guarding against later events that
|
|
1025
|
+
* could re-emit the tool message and clobber the per-pick persisted
|
|
1026
|
+
* answers via top-level merge.
|
|
1027
|
+
*/
|
|
1028
|
+
public markAskUserQuestionResolved(
|
|
1029
|
+
toolMessage: AgentWidgetMessage,
|
|
1030
|
+
answers?: Record<string, string | string[]>
|
|
1031
|
+
): void {
|
|
1032
|
+
const current = this.messages.find((m) => m.id === toolMessage.id);
|
|
1033
|
+
if (!current) return;
|
|
1034
|
+
this.upsertMessage({
|
|
1035
|
+
...current,
|
|
1036
|
+
agentMetadata: {
|
|
1037
|
+
...current.agentMetadata,
|
|
1038
|
+
awaitingLocalTool: false,
|
|
1039
|
+
askUserQuestionAnswered: true,
|
|
1040
|
+
...(answers ? { askUserQuestionAnswers: answers } : {}),
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
public async resolveAskUserQuestion(
|
|
1046
|
+
toolMessage: AgentWidgetMessage,
|
|
1047
|
+
answer: string | Record<string, string | string[]>
|
|
1048
|
+
): Promise<void> {
|
|
1049
|
+
// Idempotent — guards against rapid double-clicks on answer pills before
|
|
1050
|
+
// the re-render swaps the card to its collapsed/answered state.
|
|
1051
|
+
const live = this.messages.find((m) => m.id === toolMessage.id);
|
|
1052
|
+
if (live?.agentMetadata?.askUserQuestionAnswered === true) return;
|
|
1053
|
+
|
|
1054
|
+
const executionId = toolMessage.agentMetadata?.executionId;
|
|
1055
|
+
const toolName = toolMessage.toolCall?.name;
|
|
1056
|
+
if (!executionId || !toolName) {
|
|
1057
|
+
this.callbacks.onError?.(
|
|
1058
|
+
new Error(
|
|
1059
|
+
"resolveAskUserQuestion: message is missing executionId or toolCall.name"
|
|
1060
|
+
)
|
|
1061
|
+
);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Flip answered flag first so the next render skips the sheet re-mount,
|
|
1066
|
+
// avoiding the race between removeAskUserQuestionSheet's 180ms slide-out
|
|
1067
|
+
// timer and the renders that fire as the resume stream lands. Pass the
|
|
1068
|
+
// structured answer Record (when present) so it's atomically persisted
|
|
1069
|
+
// alongside the flag — the answered-state review card depends on
|
|
1070
|
+
// `agentMetadata.askUserQuestionAnswers` being populated at render time.
|
|
1071
|
+
//
|
|
1072
|
+
// For single-question payloads, callers (built-in pick handler, plugins)
|
|
1073
|
+
// resolve with a plain string. Derive a `{ [questionText]: answer }` Record
|
|
1074
|
+
// from the toolCall args so the answered-card render path is consistent
|
|
1075
|
+
// with grouped flows.
|
|
1076
|
+
let structuredAnswers: Record<string, string | string[]> | undefined =
|
|
1077
|
+
typeof answer === "string" ? undefined : answer;
|
|
1078
|
+
if (structuredAnswers === undefined && typeof answer === "string") {
|
|
1079
|
+
const args = toolMessage.toolCall?.args as
|
|
1080
|
+
| { questions?: Array<{ question?: unknown }> }
|
|
1081
|
+
| undefined;
|
|
1082
|
+
const questions = Array.isArray(args?.questions) ? args!.questions : [];
|
|
1083
|
+
if (questions.length === 1) {
|
|
1084
|
+
const qText = typeof questions[0]?.question === "string"
|
|
1085
|
+
? (questions[0].question as string)
|
|
1086
|
+
: "";
|
|
1087
|
+
if (qText) structuredAnswers = { [qText]: answer };
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
this.markAskUserQuestionResolved(toolMessage, structuredAnswers);
|
|
1091
|
+
|
|
1092
|
+
// Inject Q→A pair messages — one assistant bubble per question, one user
|
|
1093
|
+
// bubble per answer — so the transcript reads like a normal conversation.
|
|
1094
|
+
// The original ask_user_question tool message is suppressed by the
|
|
1095
|
+
// renderer once `askUserQuestionAnswered` is true. Skipped questions get
|
|
1096
|
+
// a muted italic `*Skipped*` user bubble (rendered through the standard
|
|
1097
|
+
// markdown pipeline).
|
|
1098
|
+
const toolCallId = toolMessage.toolCall!.id;
|
|
1099
|
+
const args = toolMessage.toolCall?.args as
|
|
1100
|
+
| { questions?: Array<{ question?: unknown; header?: unknown }> }
|
|
1101
|
+
| undefined;
|
|
1102
|
+
const questions = Array.isArray(args?.questions) ? args!.questions : [];
|
|
1103
|
+
if (questions.length === 0) {
|
|
1104
|
+
const fallback =
|
|
1105
|
+
typeof answer === "string"
|
|
1106
|
+
? answer
|
|
1107
|
+
: Object.entries(answer)
|
|
1108
|
+
.map(
|
|
1109
|
+
([q, v]) => `${q}: ${Array.isArray(v) ? v.join(", ") : v}`
|
|
1110
|
+
)
|
|
1111
|
+
.join(" | ");
|
|
1112
|
+
this.appendMessage({
|
|
1113
|
+
id: `ask-user-answer-${toolCallId}`,
|
|
1114
|
+
role: "user",
|
|
1115
|
+
content: fallback,
|
|
1116
|
+
createdAt: new Date().toISOString(),
|
|
1117
|
+
streaming: false,
|
|
1118
|
+
sequence: this.nextSequence(),
|
|
1119
|
+
});
|
|
1120
|
+
} else {
|
|
1121
|
+
const stored = structuredAnswers ?? {};
|
|
1122
|
+
questions.forEach((p, i) => {
|
|
1123
|
+
const qText = typeof p?.question === "string" ? p.question : "";
|
|
1124
|
+
if (!qText) return;
|
|
1125
|
+
const ans = stored[qText];
|
|
1126
|
+
const answerStr = Array.isArray(ans)
|
|
1127
|
+
? ans.join(", ")
|
|
1128
|
+
: typeof ans === "string"
|
|
1129
|
+
? ans
|
|
1130
|
+
: "";
|
|
1131
|
+
this.appendMessage({
|
|
1132
|
+
id: `ask-user-q-${toolCallId}-${i}`,
|
|
1133
|
+
role: "assistant",
|
|
1134
|
+
content: qText,
|
|
1135
|
+
createdAt: new Date().toISOString(),
|
|
1136
|
+
streaming: false,
|
|
1137
|
+
sequence: this.nextSequence(),
|
|
1138
|
+
});
|
|
1139
|
+
this.appendMessage({
|
|
1140
|
+
id: `ask-user-a-${toolCallId}-${i}`,
|
|
1141
|
+
role: "user",
|
|
1142
|
+
content: answerStr || "*Skipped*",
|
|
1143
|
+
createdAt: new Date().toISOString(),
|
|
1144
|
+
streaming: false,
|
|
1145
|
+
sequence: this.nextSequence(),
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
try {
|
|
1151
|
+
const response = await this.client.resumeFlow(executionId, {
|
|
1152
|
+
[toolName]: answer,
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
if (!response.ok) {
|
|
1156
|
+
const errorData = await response.json().catch(() => null);
|
|
1157
|
+
throw new Error(
|
|
1158
|
+
errorData?.error ?? `Resume failed: ${response.status}`
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (response.body) {
|
|
1163
|
+
await this.connectStream(response.body);
|
|
1164
|
+
}
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
this.callbacks.onError?.(
|
|
1167
|
+
error instanceof Error ? error : new Error(String(error))
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
971
1172
|
public cancel() {
|
|
972
1173
|
this.abortController?.abort();
|
|
973
1174
|
this.abortController = null;
|
|
1175
|
+
// Stop any in-progress audio too — when the user hits "stop", they want
|
|
1176
|
+
// the assistant to actually stop talking, not just stop generating tokens.
|
|
1177
|
+
// Both helpers are safe no-ops when audio isn't configured.
|
|
1178
|
+
this.stopSpeaking();
|
|
1179
|
+
this.stopVoicePlayback();
|
|
974
1180
|
this.setStreaming(false);
|
|
975
1181
|
this.setStatus("idle");
|
|
976
1182
|
}
|
|
@@ -1118,6 +1324,18 @@ export class AgentWidgetSession {
|
|
|
1118
1324
|
this.callbacks.onMessagesChanged([...this.messages]);
|
|
1119
1325
|
}
|
|
1120
1326
|
|
|
1327
|
+
public hydrateArtifacts(
|
|
1328
|
+
artifacts: PersonaArtifactRecord[],
|
|
1329
|
+
selectedId: string | null = null
|
|
1330
|
+
) {
|
|
1331
|
+
this.artifacts.clear();
|
|
1332
|
+
for (const rec of artifacts) {
|
|
1333
|
+
this.artifacts.set(rec.id, { ...rec, status: "complete" });
|
|
1334
|
+
}
|
|
1335
|
+
this.selectedArtifactId = selectedId;
|
|
1336
|
+
this.emitArtifactsState();
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1121
1339
|
private handleEvent = (event: AgentWidgetEvent) => {
|
|
1122
1340
|
if (event.type === "message") {
|
|
1123
1341
|
this.upsertMessage(event.message);
|
|
@@ -1312,9 +1530,35 @@ export class AgentWidgetSession {
|
|
|
1312
1530
|
return;
|
|
1313
1531
|
}
|
|
1314
1532
|
|
|
1315
|
-
this.messages = this.messages.map((existing, idx) =>
|
|
1316
|
-
idx
|
|
1317
|
-
|
|
1533
|
+
this.messages = this.messages.map((existing, idx) => {
|
|
1534
|
+
if (idx !== index) return existing;
|
|
1535
|
+
const merged = { ...existing, ...withSequence };
|
|
1536
|
+
// Preserve `ask_user_question` answered state across re-emissions.
|
|
1537
|
+
// Top-level merge would otherwise replace `agentMetadata` wholesale —
|
|
1538
|
+
// post-resume events (e.g. `tool_complete` re-emitted from a stale
|
|
1539
|
+
// client-side cache) would wipe `askUserQuestionAnswered` and
|
|
1540
|
+
// `askUserQuestionAnswers`, causing the answered review card to
|
|
1541
|
+
// lose its answers and revert to "(skipped)" placeholders.
|
|
1542
|
+
if (
|
|
1543
|
+
existing.agentMetadata?.askUserQuestionAnswered === true &&
|
|
1544
|
+
withSequence.agentMetadata
|
|
1545
|
+
) {
|
|
1546
|
+
merged.agentMetadata = {
|
|
1547
|
+
...withSequence.agentMetadata,
|
|
1548
|
+
askUserQuestionAnswered: true,
|
|
1549
|
+
...(existing.agentMetadata.askUserQuestionAnswers
|
|
1550
|
+
? {
|
|
1551
|
+
askUserQuestionAnswers:
|
|
1552
|
+
existing.agentMetadata.askUserQuestionAnswers,
|
|
1553
|
+
}
|
|
1554
|
+
: {}),
|
|
1555
|
+
// Keep awaiting flag false once resolved — never let a stale
|
|
1556
|
+
// re-emit flip us back to awaiting.
|
|
1557
|
+
awaitingLocalTool: false,
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
return merged;
|
|
1561
|
+
});
|
|
1318
1562
|
this.messages = this.sortMessages(this.messages);
|
|
1319
1563
|
this.callbacks.onMessagesChanged([...this.messages]);
|
|
1320
1564
|
}
|