@runtypelabs/persona 4.17.0 → 4.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/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-BpWGm7_6.d.cts → types-DsmTD8TO.d.cts} +12 -0
- package/dist/animations/{types-BpWGm7_6.d.ts → types-DsmTD8TO.d.ts} +12 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-3I2S3I5P.js +1 -0
- package/dist/chunk-76IELU7F.js +1 -0
- package/dist/chunk-EFJBUH5F.js +1 -0
- package/dist/chunk-PDPVORUF.js +1 -0
- package/dist/chunk-Q735PSZL.js +1 -0
- package/dist/chunk-VK6JOIYJ.js +1 -0
- package/dist/codegen.cjs +4 -4
- package/dist/codegen.js +4 -4
- package/dist/{context-mentions-BTSTIBFC.js → context-mentions-MJL6S6P2.js} +1 -1
- package/dist/context-mentions.cjs +9 -8
- package/dist/context-mentions.d.cts +1224 -14
- package/dist/context-mentions.d.ts +1224 -14
- package/dist/context-mentions.js +9 -8
- package/dist/event-stream-view-4EZ7BGFK.js +1 -0
- package/dist/event-stream-view.cjs +129 -0
- package/dist/event-stream-view.d.cts +8384 -0
- package/dist/event-stream-view.d.ts +8384 -0
- package/dist/event-stream-view.js +129 -0
- package/dist/history-view-BD6OXLNA.js +898 -0
- package/dist/history-view.cjs +898 -0
- package/dist/history-view.d.cts +1461 -0
- package/dist/history-view.d.ts +1461 -0
- package/dist/history-view.js +898 -0
- package/dist/index.cjs +77 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2040 -21
- package/dist/index.d.ts +2040 -21
- package/dist/index.global.js +67 -66
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +77 -77
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +5 -4
- package/dist/launcher.global.js.map +1 -1
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.d.cts +0 -8
- package/dist/plugin-kit.d.ts +0 -8
- package/dist/plugin-kit.js +1 -1
- package/dist/smart-dom-reader.d.cts +1224 -14
- package/dist/smart-dom-reader.d.ts +1224 -14
- package/dist/theme-editor-preview.cjs +970 -73
- package/dist/theme-editor-preview.d.cts +1343 -15
- package/dist/theme-editor-preview.d.ts +1343 -15
- package/dist/theme-editor-preview.js +81 -81
- package/dist/theme-editor.cjs +7 -7
- package/dist/theme-editor.d.cts +1227 -15
- package/dist/theme-editor.d.ts +1227 -15
- package/dist/theme-editor.js +9 -9
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +226 -7
- package/dist/theme-reference.d.ts +226 -7
- package/dist/theme-reference.js +1 -1
- package/dist/webmcp-polyfill.js +2 -2
- package/dist/widget.css +1 -1
- package/package.json +18 -6
- package/src/client-history-rest.test.ts +1034 -0
- package/src/client-visitor-history.test.ts +596 -0
- package/src/client.ts +1040 -24
- package/src/components/artifact-inline.ts +8 -1
- package/src/components/artifact-pane.test.ts +278 -1
- package/src/components/artifact-pane.ts +135 -67
- package/src/components/artifact-preview.test.ts +14 -1
- package/src/components/artifact-preview.ts +9 -0
- package/src/components/header-builder.ts +3 -0
- package/src/components/header-layouts.test.ts +171 -7
- package/src/components/header-layouts.ts +83 -53
- package/src/components/header-parts.test.ts +154 -1
- package/src/components/header-parts.ts +266 -138
- package/src/components/history-confirm-dialog.ts +177 -0
- package/src/components/history-shell-copy.ts +99 -0
- package/src/components/history-view/announcer.ts +40 -0
- package/src/components/history-view/copy.ts +105 -0
- package/src/components/history-view/css.ts +1078 -0
- package/src/components/history-view/grouping.test.ts +142 -0
- package/src/components/history-view/grouping.ts +142 -0
- package/src/components/history-view/icons.ts +73 -0
- package/src/components/history-view/rows.ts +341 -0
- package/src/components/history-view/state.ts +309 -0
- package/src/components/history-view.test.ts +2660 -0
- package/src/components/history-view.ts +1891 -0
- package/src/components/launcher-surface.test.ts +133 -0
- package/src/components/launcher.test.ts +18 -0
- package/src/components/launcher.ts +7 -7
- package/src/components/panel.ts +17 -7
- package/src/components/welcome.ts +72 -2
- package/src/defaults.ts +36 -3
- package/src/event-stream-view-bundle.test.ts +69 -0
- package/src/event-stream-view-entry.ts +19 -0
- package/src/event-stream-view-loader.ts +31 -0
- package/src/event-stream-view.ts +19 -0
- package/src/generated/runtype-openapi-contract.ts +54 -4
- package/src/history-render.ts +325 -0
- package/src/history-view-bundle.test.ts +78 -0
- package/src/history-view-entry.ts +41 -0
- package/src/history-view-loader.test.ts +30 -0
- package/src/history-view-loader.ts +32 -0
- package/src/history-view.ts +28 -0
- package/src/index-core.ts +50 -2
- package/src/index-global.ts +55 -0
- package/src/internal/demo-history-provider.test.ts +402 -0
- package/src/internal/demo-history-provider.ts +689 -0
- package/src/internal/history-provider-registry.ts +25 -0
- package/src/internal/history-provider.ts +150 -0
- package/src/internal/runtype-history-provider.test.ts +313 -0
- package/src/internal/runtype-history-provider.ts +240 -0
- package/src/plugin-kit.test.ts +16 -0
- package/src/plugin-kit.ts +13 -5
- package/src/plugins/types.ts +177 -1
- package/src/session-history.test.ts +1397 -0
- package/src/session.ts +1200 -26
- package/src/styles/widget-styles.test.ts +133 -0
- package/src/styles/widget.css +227 -8
- package/src/theme-editor/preview-utils.test.ts +19 -0
- package/src/theme-editor/preview-utils.ts +15 -1
- package/src/theme-editor/preview.ts +7 -1
- package/src/theme-editor/role-mappings.ts +4 -1
- package/src/theme-editor/sections.test.ts +168 -1
- package/src/theme-editor/sections.ts +136 -7
- package/src/theme-editor/webmcp/summary.ts +16 -2
- package/src/theme-reference.ts +20 -6
- package/src/types/theme.ts +213 -7
- package/src/types.ts +927 -7
- package/src/ui.artifact-rail-resize.test.ts +235 -0
- package/src/ui.attachments-drop.test.ts +1 -0
- package/src/ui.header-update-stability.test.ts +2 -2
- package/src/ui.history-chunk.test.ts +72 -0
- package/src/ui.history-config-provider.test.ts +318 -0
- package/src/ui.history-controller.test.ts +711 -0
- package/src/ui.history-lifecycle.test.ts +456 -0
- package/src/ui.history-render-hooks.test.ts +904 -0
- package/src/ui.history-shell.test.ts +1887 -0
- package/src/ui.rail-overlay.test.ts +573 -0
- package/src/ui.rail-resize.test.ts +284 -0
- package/src/ui.rebuild-composer.test.ts +21 -0
- package/src/ui.render-welcome.test.ts +42 -0
- package/src/ui.shortcuts.test.ts +166 -0
- package/src/ui.ts +3071 -240
- package/src/utils/__fixtures__/client-conversation-history-wire.json +108 -0
- package/src/utils/artifact-file.ts +15 -0
- package/src/utils/artifact-gate.test.ts +4 -0
- package/src/utils/artifact-gate.ts +13 -17
- package/src/utils/artifact-resize.test.ts +28 -0
- package/src/utils/artifact-resize.ts +9 -2
- package/src/utils/buttons.ts +188 -1
- package/src/utils/config-merge.test.ts +91 -0
- package/src/utils/dropdown.ts +10 -1
- package/src/utils/history-messages.test.ts +284 -0
- package/src/utils/history-messages.ts +284 -0
- package/src/utils/icons.ts +3 -0
- package/src/utils/shortcuts.test.ts +288 -0
- package/src/utils/shortcuts.ts +221 -0
- package/src/utils/theme.test.ts +501 -4
- package/src/utils/theme.ts +42 -3
- package/src/utils/tokens.ts +153 -15
- package/src/utils/tooltip-timing.ts +4 -0
- package/src/utils/tooltip.test.ts +249 -7
- package/src/utils/tooltip.ts +146 -4
- package/src/utils/visitor-store.test.ts +425 -0
- package/src/utils/visitor-store.ts +337 -0
- package/dist/chunk-QJ4LV2KW.js +0 -1
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory `HistoryProvider` (D9's second consumer). Keeps the seam honest:
|
|
3
|
+
* anything it cannot express without a Runtype special case is a seam defect.
|
|
4
|
+
*
|
|
5
|
+
* Not public and not in any tsup entry or package export. Demo pages reach it
|
|
6
|
+
* through the `apps/web` Vite alias
|
|
7
|
+
* (`@runtypelabs/persona/internal/demo-history-provider` -> source) and mount it
|
|
8
|
+
* with `setHistoryProviderFactory()`.
|
|
9
|
+
*
|
|
10
|
+
* Advertises `browser` scope only and omits `resetDevice`, so the "forget this
|
|
11
|
+
* device" control stays hidden.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
AgentMessageMetadata,
|
|
16
|
+
AgentWidgetMessage,
|
|
17
|
+
AgentWidgetMessageRole,
|
|
18
|
+
ContentPart,
|
|
19
|
+
HistoryIdentityStatus,
|
|
20
|
+
HistoryScope,
|
|
21
|
+
} from "../types";
|
|
22
|
+
import { createFilePart, createImagePart, createTextPart } from "../utils/content";
|
|
23
|
+
import { isHistoryDisplayUnavailable } from "../utils/history-messages";
|
|
24
|
+
import {
|
|
25
|
+
HistoryProviderError,
|
|
26
|
+
type HistoryConversationSummary,
|
|
27
|
+
type HistoryDeleteAllOptions,
|
|
28
|
+
type HistoryListOptions,
|
|
29
|
+
type HistoryListResult,
|
|
30
|
+
type HistoryOperationContext,
|
|
31
|
+
type HistoryPageOptions,
|
|
32
|
+
type HistoryPageResult,
|
|
33
|
+
type HistoryProvider,
|
|
34
|
+
type HistoryProviderErrorCode,
|
|
35
|
+
type PreparedHistoryActivation,
|
|
36
|
+
} from "./history-provider";
|
|
37
|
+
|
|
38
|
+
/** Every provider call that can be delayed or made to fail. */
|
|
39
|
+
export type DemoHistoryOperation =
|
|
40
|
+
| "list"
|
|
41
|
+
| "getPage"
|
|
42
|
+
| "prepareOpen"
|
|
43
|
+
| "prepareStartNew"
|
|
44
|
+
| "delete"
|
|
45
|
+
| "deleteAll"
|
|
46
|
+
| "update";
|
|
47
|
+
|
|
48
|
+
export interface DemoHistoryErrorInit {
|
|
49
|
+
code: HistoryProviderErrorCode;
|
|
50
|
+
message?: string;
|
|
51
|
+
/** Meaningful only for `rate_limited`. */
|
|
52
|
+
retryAfterSeconds?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface DemoHistoryMessageSeed {
|
|
56
|
+
id?: string;
|
|
57
|
+
role: AgentWidgetMessageRole;
|
|
58
|
+
/** Visitor-visible projection only; the demo has no model channel. */
|
|
59
|
+
content: string;
|
|
60
|
+
createdAt?: string;
|
|
61
|
+
contentParts?: ContentPart[];
|
|
62
|
+
/** Renders the withheld-content transcript gap. */
|
|
63
|
+
displayUnavailable?: true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface DemoHistoryConversationSeed {
|
|
67
|
+
id?: string;
|
|
68
|
+
title: string;
|
|
69
|
+
targetId?: string | null;
|
|
70
|
+
/** Omitted derives a bounded preview from the newest message. */
|
|
71
|
+
preview?: string | null;
|
|
72
|
+
createdAt?: string;
|
|
73
|
+
updatedAt?: string;
|
|
74
|
+
starred?: boolean;
|
|
75
|
+
/** Oldest first, the order the transcript reads in. */
|
|
76
|
+
messages: DemoHistoryMessageSeed[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface DemoHistoryProviderOptions {
|
|
80
|
+
/** Replaces the built-in seed set entirely. */
|
|
81
|
+
conversations?: DemoHistoryConversationSeed[];
|
|
82
|
+
/** List default limit and the fixed message page size. Default 25. */
|
|
83
|
+
pageSize?: number;
|
|
84
|
+
/** Artificial delay applied to every operation. Default 0. */
|
|
85
|
+
latencyMs?: number;
|
|
86
|
+
identityStatus?: HistoryIdentityStatus;
|
|
87
|
+
/** Persistent per-operation failures; clear one with `setFailure(op, null)`. */
|
|
88
|
+
failures?: Partial<Record<DemoHistoryOperation, DemoHistoryErrorInit>>;
|
|
89
|
+
/** Clock for the built-in seed set and minted timestamps. */
|
|
90
|
+
now?: () => number;
|
|
91
|
+
activeConversationId?: string | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface DemoHistoryProvider extends HistoryProvider {
|
|
95
|
+
/** Test/demo-only injection. Notifies subscribers only on a real change. */
|
|
96
|
+
setIdentityStatus(status: HistoryIdentityStatus): void;
|
|
97
|
+
setLatency(ms: number): void;
|
|
98
|
+
/** One-shot, queued per operation; consumed before persistent failures. */
|
|
99
|
+
failNext(operation: DemoHistoryOperation, error: DemoHistoryErrorInit): void;
|
|
100
|
+
/** Persistent until replaced or cleared with `null`. */
|
|
101
|
+
setFailure(
|
|
102
|
+
operation: DemoHistoryOperation,
|
|
103
|
+
error: DemoHistoryErrorInit | null
|
|
104
|
+
): void;
|
|
105
|
+
clearFailures(): void;
|
|
106
|
+
getActiveConversationId(): string | null;
|
|
107
|
+
getConversationRevision(id: string): string | null;
|
|
108
|
+
/** Simulates a transcript mutation: bumps revision, `updatedAt`, and preview. */
|
|
109
|
+
appendMessage(conversationId: string, message: DemoHistoryMessageSeed): void;
|
|
110
|
+
/** Stored ids in list order (newest first), including empty conversations. */
|
|
111
|
+
getConversationIds(): string[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type DemoConversation = {
|
|
115
|
+
id: string;
|
|
116
|
+
title: string;
|
|
117
|
+
targetId: string | null;
|
|
118
|
+
preview: string | null;
|
|
119
|
+
createdAt: string;
|
|
120
|
+
updatedAt: string;
|
|
121
|
+
starred: boolean;
|
|
122
|
+
revision: string;
|
|
123
|
+
/** Oldest first. */
|
|
124
|
+
messages: AgentWidgetMessage[];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Local widening, mirroring `utils/history-messages.ts`. */
|
|
128
|
+
type HistoryAgentMetadata = AgentMessageMetadata & {
|
|
129
|
+
historyDisplayUnavailable?: true;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const DEFAULT_PAGE_SIZE = 25;
|
|
133
|
+
const MAX_PAGE_SIZE = 100;
|
|
134
|
+
const PREVIEW_MAX = 140;
|
|
135
|
+
const LIST_CURSOR_PREFIX = "dhcl_";
|
|
136
|
+
const PAGE_CURSOR_PREFIX = "dhcm_";
|
|
137
|
+
|
|
138
|
+
const DEFAULT_IDENTITY_STATUS: HistoryIdentityStatus = {
|
|
139
|
+
state: "browser_only",
|
|
140
|
+
reason: "no_identity_provider",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const MINUTE = 60_000;
|
|
144
|
+
const HOUR = 60 * MINUTE;
|
|
145
|
+
const DAY = 24 * HOUR;
|
|
146
|
+
|
|
147
|
+
const TRANSPARENT_PNG =
|
|
148
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
|
|
149
|
+
const TINY_PDF = "data:application/pdf;base64,JVBERi0xLjQKJSVFT0YK";
|
|
150
|
+
|
|
151
|
+
function iso(ms: number): string {
|
|
152
|
+
return new Date(ms).toISOString();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function clampPageSize(value: number | undefined, fallback: number): number {
|
|
156
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
157
|
+
return Math.min(MAX_PAGE_SIZE, Math.max(1, Math.floor(value)));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function encodeCursor(prefix: string, offset: number): string {
|
|
161
|
+
return `${prefix}${offset.toString(36)}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Opaque to callers; malformed values are treated as a missing resource. */
|
|
165
|
+
function decodeCursor(prefix: string, cursor: string): number {
|
|
166
|
+
if (!cursor.startsWith(prefix)) {
|
|
167
|
+
throw new HistoryProviderError("not_found", "Unknown history cursor.");
|
|
168
|
+
}
|
|
169
|
+
const offset = Number.parseInt(cursor.slice(prefix.length), 36);
|
|
170
|
+
if (!Number.isFinite(offset) || offset < 0) {
|
|
171
|
+
throw new HistoryProviderError("not_found", "Unknown history cursor.");
|
|
172
|
+
}
|
|
173
|
+
return offset;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function truncatePreview(text: string): string {
|
|
177
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
178
|
+
if (collapsed.length <= PREVIEW_MAX) return collapsed;
|
|
179
|
+
return `${collapsed.slice(0, PREVIEW_MAX - 1)}…`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function derivePreview(messages: AgentWidgetMessage[]): string | null {
|
|
183
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
184
|
+
const message = messages[index];
|
|
185
|
+
if (isHistoryDisplayUnavailable(message)) continue;
|
|
186
|
+
if (message.content) return truncatePreview(message.content);
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function identityKey(status: HistoryIdentityStatus): string {
|
|
192
|
+
const reason = "reason" in status ? status.reason : "";
|
|
193
|
+
return `${status.state}:${reason}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function toMessage(
|
|
197
|
+
seed: DemoHistoryMessageSeed,
|
|
198
|
+
conversationId: string,
|
|
199
|
+
index: number,
|
|
200
|
+
createdAtMs: number
|
|
201
|
+
): AgentWidgetMessage {
|
|
202
|
+
const base: AgentWidgetMessage = {
|
|
203
|
+
id: seed.id ?? `${conversationId}-m${index + 1}`,
|
|
204
|
+
role: seed.role,
|
|
205
|
+
content: seed.displayUnavailable ? "" : seed.content,
|
|
206
|
+
createdAt: seed.createdAt ?? iso(createdAtMs),
|
|
207
|
+
};
|
|
208
|
+
if (seed.displayUnavailable) {
|
|
209
|
+
const agentMetadata: HistoryAgentMetadata = {
|
|
210
|
+
historyDisplayUnavailable: true,
|
|
211
|
+
};
|
|
212
|
+
return { ...base, agentMetadata };
|
|
213
|
+
}
|
|
214
|
+
if (seed.contentParts && seed.contentParts.length > 0) {
|
|
215
|
+
return { ...base, contentParts: [...seed.contentParts] };
|
|
216
|
+
}
|
|
217
|
+
return base;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Detached copy so callers cannot mutate the store through a returned page. */
|
|
221
|
+
function cloneMessage(message: AgentWidgetMessage): AgentWidgetMessage {
|
|
222
|
+
return {
|
|
223
|
+
...message,
|
|
224
|
+
...(message.contentParts && { contentParts: [...message.contentParts] }),
|
|
225
|
+
...(message.agentMetadata && { agentMetadata: { ...message.agentMetadata } }),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function summarize(conversation: DemoConversation): HistoryConversationSummary {
|
|
230
|
+
return {
|
|
231
|
+
id: conversation.id,
|
|
232
|
+
title: conversation.title,
|
|
233
|
+
targetId: conversation.targetId,
|
|
234
|
+
preview: conversation.preview,
|
|
235
|
+
messageCount: conversation.messages.length,
|
|
236
|
+
createdAt: conversation.createdAt,
|
|
237
|
+
updatedAt: conversation.updatedAt,
|
|
238
|
+
starred: conversation.starred,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** `updatedAt DESC, id DESC`, matching the Runtype list order. */
|
|
243
|
+
function byRecency(a: DemoConversation, b: DemoConversation): number {
|
|
244
|
+
if (a.updatedAt !== b.updatedAt) return a.updatedAt < b.updatedAt ? 1 : -1;
|
|
245
|
+
return a.id < b.id ? 1 : -1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const DEMO_TARGET_ID = "demo-flow";
|
|
249
|
+
const DEMO_SECOND_TARGET_ID = "demo-support-agent";
|
|
250
|
+
|
|
251
|
+
const USER_LINES = [
|
|
252
|
+
"Can you check that for me?",
|
|
253
|
+
"That still is not what I meant.",
|
|
254
|
+
"Okay, and what happens after that?",
|
|
255
|
+
"How long does this usually take?",
|
|
256
|
+
"Could you send that to my email too?",
|
|
257
|
+
"Thanks, one more thing.",
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
const ASSISTANT_LINES = [
|
|
261
|
+
"Sure. Give me a moment while I pull that up.",
|
|
262
|
+
"Here is what I found on your account.",
|
|
263
|
+
"That step usually completes within two business days.",
|
|
264
|
+
"I have noted it. Anything else you want changed?",
|
|
265
|
+
"Done. You should see the update shortly.",
|
|
266
|
+
"Happy to help with that.",
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
/** Alternating user/assistant filler with deterministic minute spacing. */
|
|
270
|
+
function buildTurns(count: number, startMs: number): DemoHistoryMessageSeed[] {
|
|
271
|
+
const seeds: DemoHistoryMessageSeed[] = [];
|
|
272
|
+
for (let index = 0; index < count; index += 1) {
|
|
273
|
+
const isUser = index % 2 === 0;
|
|
274
|
+
const lines = isUser ? USER_LINES : ASSISTANT_LINES;
|
|
275
|
+
seeds.push({
|
|
276
|
+
role: isUser ? "user" : "assistant",
|
|
277
|
+
content: lines[Math.floor(index / 2) % lines.length],
|
|
278
|
+
createdAt: iso(startMs + index * MINUTE),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return seeds;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Realistic default set: today, yesterday, last month, plus paging depth. */
|
|
285
|
+
function defaultSeeds(nowMs: number): DemoHistoryConversationSeed[] {
|
|
286
|
+
const orderStart = nowMs - 40 * MINUTE;
|
|
287
|
+
const refundStart = nowMs - 4 * HOUR;
|
|
288
|
+
const bulkStart = nowMs - 26 * HOUR;
|
|
289
|
+
const giftStart = nowMs - 5 * DAY;
|
|
290
|
+
const subscriptionStart = nowMs - 34 * DAY;
|
|
291
|
+
|
|
292
|
+
return [
|
|
293
|
+
{
|
|
294
|
+
id: "demo-conv-order-status",
|
|
295
|
+
title: "Where is my order?",
|
|
296
|
+
targetId: DEMO_TARGET_ID,
|
|
297
|
+
createdAt: iso(orderStart),
|
|
298
|
+
updatedAt: iso(orderStart + 5 * MINUTE),
|
|
299
|
+
messages: [
|
|
300
|
+
{
|
|
301
|
+
role: "user",
|
|
302
|
+
content: "Order 41822 still says processing. Has it shipped?",
|
|
303
|
+
createdAt: iso(orderStart),
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
role: "assistant",
|
|
307
|
+
content:
|
|
308
|
+
"It left the warehouse this morning and is due Thursday. Tracking number is 1Z999AA10123456784.",
|
|
309
|
+
createdAt: iso(orderStart + 2 * MINUTE),
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
role: "user",
|
|
313
|
+
content: "Can it go to my work address instead?",
|
|
314
|
+
createdAt: iso(orderStart + 4 * MINUTE),
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
role: "assistant",
|
|
318
|
+
content:
|
|
319
|
+
"Once a parcel is scanned I cannot reroute it, but the carrier can hold it at a pickup point for you.",
|
|
320
|
+
createdAt: iso(orderStart + 5 * MINUTE),
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: "demo-conv-damaged-mug",
|
|
326
|
+
title: "Refund for the damaged mug",
|
|
327
|
+
targetId: DEMO_TARGET_ID,
|
|
328
|
+
createdAt: iso(refundStart),
|
|
329
|
+
updatedAt: iso(refundStart + 33 * MINUTE),
|
|
330
|
+
messages: [
|
|
331
|
+
{
|
|
332
|
+
role: "user",
|
|
333
|
+
content: "The mug arrived cracked. Photo attached.",
|
|
334
|
+
contentParts: [
|
|
335
|
+
createTextPart("The mug arrived cracked. Photo attached."),
|
|
336
|
+
createImagePart(TRANSPARENT_PNG, {
|
|
337
|
+
mimeType: "image/png",
|
|
338
|
+
alt: "Cracked mug",
|
|
339
|
+
}),
|
|
340
|
+
],
|
|
341
|
+
createdAt: iso(refundStart),
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
role: "assistant",
|
|
345
|
+
content: "That should not have shipped. Here is your refund receipt.",
|
|
346
|
+
contentParts: [
|
|
347
|
+
createTextPart(
|
|
348
|
+
"That should not have shipped. Here is your refund receipt."
|
|
349
|
+
),
|
|
350
|
+
createFilePart(TINY_PDF, "application/pdf", "refund-receipt.pdf"),
|
|
351
|
+
],
|
|
352
|
+
createdAt: iso(refundStart + MINUTE),
|
|
353
|
+
},
|
|
354
|
+
...buildTurns(32, refundStart + 2 * MINUTE),
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: "demo-conv-bulk-order",
|
|
359
|
+
title: "Bulk order for the office",
|
|
360
|
+
targetId: DEMO_TARGET_ID,
|
|
361
|
+
createdAt: iso(bulkStart),
|
|
362
|
+
updatedAt: iso(bulkStart + 11 * MINUTE),
|
|
363
|
+
messages: [
|
|
364
|
+
{
|
|
365
|
+
role: "user",
|
|
366
|
+
content: "We need 40 of the navy notebooks by the 12th.",
|
|
367
|
+
createdAt: iso(bulkStart),
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
role: "assistant",
|
|
371
|
+
content: "",
|
|
372
|
+
displayUnavailable: true,
|
|
373
|
+
createdAt: iso(bulkStart + MINUTE),
|
|
374
|
+
},
|
|
375
|
+
...buildTurns(10, bulkStart + 2 * MINUTE),
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
id: "demo-conv-gift-wrap",
|
|
380
|
+
title: "Gift wrapping and delivery windows",
|
|
381
|
+
targetId: DEMO_SECOND_TARGET_ID,
|
|
382
|
+
createdAt: iso(giftStart),
|
|
383
|
+
updatedAt: iso(giftStart + 3 * MINUTE),
|
|
384
|
+
messages: [
|
|
385
|
+
{
|
|
386
|
+
role: "user",
|
|
387
|
+
content: "Do you wrap orders, and can I pick the delivery day?",
|
|
388
|
+
createdAt: iso(giftStart),
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
role: "assistant",
|
|
392
|
+
content:
|
|
393
|
+
"Wrapping is free on gift orders, and you can choose any weekday inside the next three weeks.",
|
|
394
|
+
createdAt: iso(giftStart + 2 * MINUTE),
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: "demo-conv-subscription",
|
|
400
|
+
title: "Pausing my subscription",
|
|
401
|
+
targetId: DEMO_TARGET_ID,
|
|
402
|
+
createdAt: iso(subscriptionStart),
|
|
403
|
+
updatedAt: iso(subscriptionStart + 51 * MINUTE),
|
|
404
|
+
messages: buildTurns(52, subscriptionStart),
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function createDemoHistoryProvider(
|
|
410
|
+
options: DemoHistoryProviderOptions = {}
|
|
411
|
+
): DemoHistoryProvider {
|
|
412
|
+
const now = options.now ?? (() => Date.now());
|
|
413
|
+
const pageSize = clampPageSize(options.pageSize, DEFAULT_PAGE_SIZE);
|
|
414
|
+
const seeds = options.conversations ?? defaultSeeds(now());
|
|
415
|
+
|
|
416
|
+
let revisionCounter = 0;
|
|
417
|
+
const nextRevision = (): string => {
|
|
418
|
+
revisionCounter += 1;
|
|
419
|
+
return `demo-rev-${revisionCounter.toString(36)}`;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const conversations = new Map<string, DemoConversation>();
|
|
423
|
+
seeds.forEach((seed, seedIndex) => {
|
|
424
|
+
const id = seed.id ?? `demo-conv-${seedIndex + 1}`;
|
|
425
|
+
const baseMs = now() - (seeds.length - seedIndex) * MINUTE;
|
|
426
|
+
const messages = seed.messages.map((message, index) =>
|
|
427
|
+
toMessage(message, id, index, baseMs + index * MINUTE)
|
|
428
|
+
);
|
|
429
|
+
conversations.set(id, {
|
|
430
|
+
id,
|
|
431
|
+
title: seed.title,
|
|
432
|
+
targetId: seed.targetId ?? null,
|
|
433
|
+
preview: seed.preview !== undefined ? seed.preview : derivePreview(messages),
|
|
434
|
+
createdAt: seed.createdAt ?? messages[0]?.createdAt ?? iso(baseMs),
|
|
435
|
+
updatedAt:
|
|
436
|
+
seed.updatedAt ?? messages[messages.length - 1]?.createdAt ?? iso(baseMs),
|
|
437
|
+
starred: seed.starred ?? false,
|
|
438
|
+
revision: nextRevision(),
|
|
439
|
+
messages,
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
let activeConversationId = options.activeConversationId ?? null;
|
|
444
|
+
let latencyMs = options.latencyMs ?? 0;
|
|
445
|
+
let identityStatus = options.identityStatus ?? DEFAULT_IDENTITY_STATUS;
|
|
446
|
+
const identitySubscribers = new Set<(status: HistoryIdentityStatus) => void>();
|
|
447
|
+
|
|
448
|
+
const persistentFailures = new Map<DemoHistoryOperation, DemoHistoryErrorInit>();
|
|
449
|
+
for (const [operation, error] of Object.entries(options.failures ?? {})) {
|
|
450
|
+
if (error) persistentFailures.set(operation as DemoHistoryOperation, error);
|
|
451
|
+
}
|
|
452
|
+
const queuedFailures = new Map<DemoHistoryOperation, DemoHistoryErrorInit[]>();
|
|
453
|
+
|
|
454
|
+
const capabilities = { scopes: ["browser"] as readonly HistoryScope[] };
|
|
455
|
+
|
|
456
|
+
const toError = (init: DemoHistoryErrorInit): HistoryProviderError =>
|
|
457
|
+
new HistoryProviderError(
|
|
458
|
+
init.code,
|
|
459
|
+
init.message ?? `Demo history provider failure: ${init.code}`,
|
|
460
|
+
init.retryAfterSeconds !== undefined
|
|
461
|
+
? { retryAfterSeconds: init.retryAfterSeconds }
|
|
462
|
+
: undefined
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const delay = async (): Promise<void> => {
|
|
466
|
+
if (latencyMs > 0) {
|
|
467
|
+
await new Promise((resolve) => setTimeout(resolve, latencyMs));
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
/** Scope check first (local failure), then latency, then injected failures. */
|
|
472
|
+
const begin = async (
|
|
473
|
+
operation: DemoHistoryOperation,
|
|
474
|
+
context: HistoryOperationContext
|
|
475
|
+
): Promise<void> => {
|
|
476
|
+
if (!capabilities.scopes.includes(context.scope)) {
|
|
477
|
+
throw new HistoryProviderError(
|
|
478
|
+
"unsupported_scope",
|
|
479
|
+
`Demo history provider supports browser scope only, got ${context.scope}.`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
await delay();
|
|
483
|
+
const queued = queuedFailures.get(operation);
|
|
484
|
+
const injected = queued?.shift() ?? persistentFailures.get(operation);
|
|
485
|
+
if (injected) throw toError(injected);
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const mustGet = (id: string): DemoConversation => {
|
|
489
|
+
const conversation = conversations.get(id);
|
|
490
|
+
if (!conversation) {
|
|
491
|
+
throw new HistoryProviderError("not_found", "Conversation not found.");
|
|
492
|
+
}
|
|
493
|
+
return conversation;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const touch = (conversation: DemoConversation): void => {
|
|
497
|
+
conversation.revision = nextRevision();
|
|
498
|
+
conversation.updatedAt = iso(now());
|
|
499
|
+
conversation.preview = derivePreview(conversation.messages);
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
/** Settles once: commit and discard are mutually exclusive and idempotent. */
|
|
503
|
+
const prepare = (
|
|
504
|
+
conversationId: string,
|
|
505
|
+
conversationRevision: string,
|
|
506
|
+
apply: () => void
|
|
507
|
+
): PreparedHistoryActivation => {
|
|
508
|
+
let settled = false;
|
|
509
|
+
return {
|
|
510
|
+
conversationId,
|
|
511
|
+
conversationRevision,
|
|
512
|
+
commit() {
|
|
513
|
+
if (settled) return;
|
|
514
|
+
settled = true;
|
|
515
|
+
apply();
|
|
516
|
+
},
|
|
517
|
+
discard() {
|
|
518
|
+
settled = true;
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
capabilities,
|
|
525
|
+
|
|
526
|
+
getIdentityStatus() {
|
|
527
|
+
return identityStatus;
|
|
528
|
+
},
|
|
529
|
+
|
|
530
|
+
subscribeIdentityStatus(callback) {
|
|
531
|
+
identitySubscribers.add(callback);
|
|
532
|
+
return () => {
|
|
533
|
+
identitySubscribers.delete(callback);
|
|
534
|
+
};
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
async list(opts: HistoryListOptions): Promise<HistoryListResult> {
|
|
538
|
+
await begin("list", opts.context);
|
|
539
|
+
const limit = clampPageSize(opts.limit, pageSize);
|
|
540
|
+
const rows = [...conversations.values()]
|
|
541
|
+
// The API lists only conversations that have stored messages.
|
|
542
|
+
.filter((conversation) => conversation.messages.length > 0)
|
|
543
|
+
.filter(
|
|
544
|
+
(conversation) =>
|
|
545
|
+
opts.targetId === undefined ||
|
|
546
|
+
conversation.targetId === opts.targetId
|
|
547
|
+
)
|
|
548
|
+
.sort(byRecency);
|
|
549
|
+
const offset = opts.cursor ? decodeCursor(LIST_CURSOR_PREFIX, opts.cursor) : 0;
|
|
550
|
+
const items = rows.slice(offset, offset + limit).map(summarize);
|
|
551
|
+
const nextOffset = offset + items.length;
|
|
552
|
+
return {
|
|
553
|
+
items,
|
|
554
|
+
nextCursor:
|
|
555
|
+
nextOffset < rows.length
|
|
556
|
+
? encodeCursor(LIST_CURSOR_PREFIX, nextOffset)
|
|
557
|
+
: null,
|
|
558
|
+
};
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Newest page first, oldest-first within the page; follow `nextCursor` for
|
|
563
|
+
* older pages. `beforeCreatedAt` is ignored: every seeded message has a
|
|
564
|
+
* timestamp, so nothing needs synthesizing.
|
|
565
|
+
*/
|
|
566
|
+
async getPage(id: string, opts: HistoryPageOptions): Promise<HistoryPageResult> {
|
|
567
|
+
await begin("getPage", opts.context);
|
|
568
|
+
const conversation = mustGet(id);
|
|
569
|
+
const end = opts.cursor
|
|
570
|
+
? Math.min(decodeCursor(PAGE_CURSOR_PREFIX, opts.cursor), conversation.messages.length)
|
|
571
|
+
: conversation.messages.length;
|
|
572
|
+
const start = Math.max(0, end - pageSize);
|
|
573
|
+
return {
|
|
574
|
+
summary: summarize(conversation),
|
|
575
|
+
messages: conversation.messages.slice(start, end).map(cloneMessage),
|
|
576
|
+
conversationRevision: conversation.revision,
|
|
577
|
+
nextCursor: start > 0 ? encodeCursor(PAGE_CURSOR_PREFIX, start) : null,
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
async prepareOpen(id, opts) {
|
|
582
|
+
await begin("prepareOpen", opts.context);
|
|
583
|
+
const conversation = mustGet(id);
|
|
584
|
+
return prepare(conversation.id, conversation.revision, () => {
|
|
585
|
+
activeConversationId = conversation.id;
|
|
586
|
+
});
|
|
587
|
+
},
|
|
588
|
+
|
|
589
|
+
async prepareStartNew(opts) {
|
|
590
|
+
await begin("prepareStartNew", opts.context);
|
|
591
|
+
const id = `demo-conv-new-${conversations.size + 1}`;
|
|
592
|
+
const revision = nextRevision();
|
|
593
|
+
const createdAt = iso(now());
|
|
594
|
+
return prepare(id, revision, () => {
|
|
595
|
+
conversations.set(id, {
|
|
596
|
+
id,
|
|
597
|
+
title: "New conversation",
|
|
598
|
+
targetId: DEMO_TARGET_ID,
|
|
599
|
+
preview: null,
|
|
600
|
+
createdAt,
|
|
601
|
+
updatedAt: createdAt,
|
|
602
|
+
starred: false,
|
|
603
|
+
revision,
|
|
604
|
+
messages: [],
|
|
605
|
+
});
|
|
606
|
+
activeConversationId = id;
|
|
607
|
+
});
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
async delete(id, opts) {
|
|
611
|
+
await begin("delete", opts.context);
|
|
612
|
+
mustGet(id);
|
|
613
|
+
conversations.delete(id);
|
|
614
|
+
if (activeConversationId === id) activeConversationId = null;
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
async update(id, patch, opts) {
|
|
618
|
+
await begin("update", opts.context);
|
|
619
|
+
const conversation = mustGet(id);
|
|
620
|
+
const title = patch.title?.trim();
|
|
621
|
+
// Rename never reorders the list: only the revision moves, not updatedAt.
|
|
622
|
+
if (title) conversation.title = title;
|
|
623
|
+
if (patch.starred !== undefined) conversation.starred = patch.starred;
|
|
624
|
+
conversation.revision = nextRevision();
|
|
625
|
+
return summarize(conversation);
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
async deleteAll(opts: HistoryDeleteAllOptions) {
|
|
629
|
+
await begin("deleteAll", opts.context);
|
|
630
|
+
const doomed = [...conversations.values()].filter(
|
|
631
|
+
(conversation) =>
|
|
632
|
+
opts.targetId === undefined || conversation.targetId === opts.targetId
|
|
633
|
+
);
|
|
634
|
+
for (const conversation of doomed) {
|
|
635
|
+
conversations.delete(conversation.id);
|
|
636
|
+
if (activeConversationId === conversation.id) activeConversationId = null;
|
|
637
|
+
}
|
|
638
|
+
return { deleted: doomed.length };
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
// --- demo/test controls -------------------------------------------------
|
|
642
|
+
|
|
643
|
+
setIdentityStatus(status) {
|
|
644
|
+
if (identityKey(status) === identityKey(identityStatus)) return;
|
|
645
|
+
identityStatus = status;
|
|
646
|
+
for (const subscriber of [...identitySubscribers]) subscriber(status);
|
|
647
|
+
},
|
|
648
|
+
|
|
649
|
+
setLatency(ms) {
|
|
650
|
+
latencyMs = Math.max(0, ms);
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
failNext(operation, error) {
|
|
654
|
+
const queue = queuedFailures.get(operation) ?? [];
|
|
655
|
+
queue.push(error);
|
|
656
|
+
queuedFailures.set(operation, queue);
|
|
657
|
+
},
|
|
658
|
+
|
|
659
|
+
setFailure(operation, error) {
|
|
660
|
+
if (error) persistentFailures.set(operation, error);
|
|
661
|
+
else persistentFailures.delete(operation);
|
|
662
|
+
},
|
|
663
|
+
|
|
664
|
+
clearFailures() {
|
|
665
|
+
persistentFailures.clear();
|
|
666
|
+
queuedFailures.clear();
|
|
667
|
+
},
|
|
668
|
+
|
|
669
|
+
getActiveConversationId() {
|
|
670
|
+
return activeConversationId;
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
getConversationRevision(id) {
|
|
674
|
+
return conversations.get(id)?.revision ?? null;
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
appendMessage(conversationId, message) {
|
|
678
|
+
const conversation = mustGet(conversationId);
|
|
679
|
+
conversation.messages.push(
|
|
680
|
+
toMessage(message, conversation.id, conversation.messages.length, now())
|
|
681
|
+
);
|
|
682
|
+
touch(conversation);
|
|
683
|
+
},
|
|
684
|
+
|
|
685
|
+
getConversationIds() {
|
|
686
|
+
return [...conversations.values()].sort(byRecency).map((row) => row.id);
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-scoped override for the internal history provider (D9). `ui.ts`
|
|
3
|
+
* consults this before constructing the Runtype provider, which lets a demo
|
|
4
|
+
* page mount the in-memory provider without a public config surface.
|
|
5
|
+
*
|
|
6
|
+
* Ships in the bundle (a few lines); the demo provider itself does not.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { HistoryProvider } from "./history-provider";
|
|
10
|
+
|
|
11
|
+
export type HistoryProviderFactory = () => HistoryProvider;
|
|
12
|
+
|
|
13
|
+
let factory: HistoryProviderFactory | null = null;
|
|
14
|
+
|
|
15
|
+
/** Pass `null` to fall back to the Runtype provider. */
|
|
16
|
+
export function setHistoryProviderFactory(
|
|
17
|
+
next: HistoryProviderFactory | null
|
|
18
|
+
): void {
|
|
19
|
+
factory = next;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Non-destructive: each widget instance builds its own provider. */
|
|
23
|
+
export function getHistoryProviderFactory(): HistoryProviderFactory | null {
|
|
24
|
+
return factory;
|
|
25
|
+
}
|