@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.
Files changed (71) hide show
  1. package/README.md +142 -0
  2. package/dist/animations/glyph-cycle.cjs +279 -0
  3. package/dist/animations/glyph-cycle.d.cts +5 -0
  4. package/dist/animations/glyph-cycle.d.ts +5 -0
  5. package/dist/animations/glyph-cycle.js +252 -0
  6. package/dist/animations/types-cwY5HaFD.d.cts +307 -0
  7. package/dist/animations/types-cwY5HaFD.d.ts +307 -0
  8. package/dist/animations/wipe.cjs +107 -0
  9. package/dist/animations/wipe.d.cts +5 -0
  10. package/dist/animations/wipe.d.ts +5 -0
  11. package/dist/animations/wipe.js +80 -0
  12. package/dist/index.cjs +49 -48
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +504 -1
  15. package/dist/index.d.ts +504 -1
  16. package/dist/index.global.js +143 -88
  17. package/dist/index.global.js.map +1 -1
  18. package/dist/index.js +49 -48
  19. package/dist/index.js.map +1 -1
  20. package/dist/testing.cjs +85 -0
  21. package/dist/testing.d.cts +39 -0
  22. package/dist/testing.d.ts +39 -0
  23. package/dist/testing.js +56 -0
  24. package/dist/theme-editor.cjs +2095 -207
  25. package/dist/theme-editor.d.cts +432 -2
  26. package/dist/theme-editor.d.ts +432 -2
  27. package/dist/theme-editor.js +2093 -207
  28. package/dist/theme-reference.cjs +1 -1
  29. package/dist/theme-reference.d.cts +14 -0
  30. package/dist/theme-reference.d.ts +14 -0
  31. package/dist/widget.css +565 -0
  32. package/package.json +20 -3
  33. package/src/animations/glyph-cycle.ts +332 -0
  34. package/src/animations/wipe.ts +66 -0
  35. package/src/client.test.ts +275 -0
  36. package/src/client.ts +99 -0
  37. package/src/components/ask-user-question-bubble.test.ts +583 -0
  38. package/src/components/ask-user-question-bubble.ts +924 -0
  39. package/src/components/composer-builder.ts +61 -10
  40. package/src/components/message-bubble.test.ts +181 -2
  41. package/src/components/message-bubble.ts +209 -14
  42. package/src/components/messages.ts +33 -1
  43. package/src/components/panel.ts +45 -5
  44. package/src/defaults.ts +37 -0
  45. package/src/index-global.ts +31 -0
  46. package/src/index.ts +34 -1
  47. package/src/plugins/types.ts +57 -0
  48. package/src/session.test.ts +276 -1
  49. package/src/session.ts +247 -3
  50. package/src/styles/widget.css +565 -0
  51. package/src/testing/index.ts +11 -0
  52. package/src/testing/mock-stream.test.ts +80 -0
  53. package/src/testing/mock-stream.ts +94 -0
  54. package/src/testing.ts +2 -0
  55. package/src/theme-editor/index.ts +4 -0
  56. package/src/theme-editor/preview-utils.test.ts +60 -0
  57. package/src/theme-editor/preview-utils.ts +129 -0
  58. package/src/theme-editor/sections.test.ts +19 -0
  59. package/src/theme-editor/sections.ts +84 -1
  60. package/src/types/theme.ts +15 -0
  61. package/src/types.ts +360 -0
  62. package/src/ui.ask-user-question-plugin.test.ts +649 -0
  63. package/src/ui.stop-button.test.ts +165 -0
  64. package/src/ui.ts +706 -11
  65. package/src/utils/message-fingerprint.ts +2 -0
  66. package/src/utils/morph.ts +7 -0
  67. package/src/utils/storage.ts +10 -2
  68. package/src/utils/stream-animation.test.ts +417 -0
  69. package/src/utils/stream-animation.ts +449 -0
  70. package/src/utils/theme.test.ts +36 -0
  71. 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 === index ? { ...existing, ...withSequence } : existing
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
  }