@agent-native/core 0.180.0-nightly-20260915193344 → 0.180.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 (49) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +10 -0
  3. package/corpus/templates/calendar/CHANGELOG.md +39 -0
  4. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +2 -0
  5. package/corpus/templates/clips/CHANGELOG.md +19 -0
  6. package/corpus/templates/clips/actions/save-browser-diagnostics.ts +43 -4
  7. package/corpus/templates/clips/app/components/player/browser-diagnostics-panel.tsx +114 -1
  8. package/corpus/templates/clips/app/i18n/en-US.ts +7 -0
  9. package/corpus/templates/clips/app/lib/browser-diagnostics-capture.ts +108 -0
  10. package/corpus/templates/clips/app/routes/record.tsx +1 -0
  11. package/corpus/templates/clips/changelog/2026-09-15-recordings-include-a-structured-timeline-of-browser-interact.md +6 -0
  12. package/corpus/templates/clips/chrome-extension/public/manifest.json +7 -0
  13. package/corpus/templates/clips/chrome-extension/src/background.ts +235 -9
  14. package/corpus/templates/clips/chrome-extension/src/content-history-bridge.ts +80 -0
  15. package/corpus/templates/clips/chrome-extension/src/content-script.ts +166 -5
  16. package/corpus/templates/clips/chrome-extension/vite.config.ts +4 -0
  17. package/corpus/templates/clips/server/db/schema.ts +3 -0
  18. package/corpus/templates/clips/server/lib/public-agent-context.ts +40 -2
  19. package/corpus/templates/clips/server/plugins/db.ts +5 -0
  20. package/corpus/templates/clips/shared/browser-diagnostics.ts +171 -0
  21. package/corpus/templates/content/.agents/skills/content/SKILL.md +4 -2
  22. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +8 -1
  23. package/corpus/templates/content/AGENTS.md +5 -6
  24. package/corpus/templates/content/CHANGELOG.md +24 -0
  25. package/corpus/templates/content/actions/_document-edit-mutation.ts +74 -23
  26. package/corpus/templates/content/actions/edit-document.ts +48 -8
  27. package/corpus/templates/content/server/plugins/agent-chat.ts +1 -1
  28. package/corpus/templates/design/CHANGELOG.md +22 -0
  29. package/corpus/templates/factory/CHANGELOG.md +11 -0
  30. package/corpus/templates/forms/CHANGELOG.md +6 -0
  31. package/corpus/templates/mail/CHANGELOG.md +68 -0
  32. package/corpus/templates/slides/CHANGELOG.md +32 -0
  33. package/dist/automation/index.d.ts +1 -1
  34. package/dist/client/CommandMenu.js +14 -2
  35. package/dist/client/settings/AccountSettingsCard.js +3 -2
  36. package/dist/client/settings/SettingsTabsPage.js +9 -1
  37. package/dist/client/settings/agent-settings-search.js +5 -1
  38. package/dist/client/sign-out.d.ts +1 -0
  39. package/dist/client/sign-out.js +8 -0
  40. package/dist/collab/awareness.d.ts +2 -2
  41. package/dist/collab/routes.d.ts +1 -1
  42. package/dist/collab/struct-routes.d.ts +1 -1
  43. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  44. package/dist/notifications/routes.d.ts +3 -3
  45. package/dist/observability/routes.d.ts +3 -3
  46. package/dist/progress/routes.d.ts +1 -1
  47. package/dist/secrets/routes.d.ts +6 -6
  48. package/dist/server/core-routes-plugin.d.ts +1 -1
  49. package/package.json +4 -4
@@ -1,3 +1,4 @@
1
+ import { sanitizeBrowserDiagnosticNavigationUrl } from "@shared/browser-diagnostics";
1
2
  import { buildRecordingShareUrl } from "@shared/recording-link";
2
3
 
3
4
  import {
@@ -30,6 +31,8 @@ const DEFAULT_CLIPS_BASE_URL = "https://clips.agent-native.com";
30
31
  const DEBUGGER_PROTOCOL_VERSION = "1.3";
31
32
  const MAX_CONSOLE_LOGS = 400;
32
33
  const MAX_NETWORK_REQUESTS = 400;
34
+ const CLICK_INPUT_INGRESS_WINDOW_MS = 1_000;
35
+ const MAX_CLICK_INPUT_INGRESS_PER_WINDOW = 100;
33
36
  const MAX_MESSAGE_LENGTH = 2_000;
34
37
  const MAX_URL_LENGTH = 1_000;
35
38
  const STORAGE_SETUP_REQUIRED_MESSAGE =
@@ -56,6 +59,7 @@ const UNQUOTED_SECRET_VALUE_RE = new RegExp(
56
59
  type CaptureSurface = "browser" | "window" | "monitor" | "camera";
57
60
  type ConsoleLevel = "debug" | "log" | "info" | "warn" | "error";
58
61
  type NetworkType = "fetch" | "xhr";
62
+ type InteractionKind = "navigation" | "click" | "input" | "scroll";
59
63
  type UploadMode = "streaming" | "buffered";
60
64
 
61
65
  type ExtensionSettings = {
@@ -124,6 +128,14 @@ type NetworkRequest = {
124
128
  error?: string;
125
129
  };
126
130
 
131
+ type InteractionEvent = {
132
+ timestampMs: number;
133
+ elapsedMs: number;
134
+ kind: InteractionKind;
135
+ target?: string;
136
+ url?: string;
137
+ };
138
+
127
139
  type BrowserDiagnosticsData = {
128
140
  pageUrl: string | null;
129
141
  userAgent: string | null;
@@ -131,6 +143,7 @@ type BrowserDiagnosticsData = {
131
143
  endedAt: string;
132
144
  consoleLogs: ConsoleLog[];
133
145
  networkRequests: NetworkRequest[];
146
+ interactionEvents?: InteractionEvent[];
134
147
  summary: {
135
148
  consoleCount: number;
136
149
  consoleErrorCount: number;
@@ -169,6 +182,8 @@ type NativeRecording = {
169
182
  status: NativeRecordingStatus;
170
183
  recordingUrl: string;
171
184
  error: string | null;
185
+ diagnosticsPausedAtMs?: number;
186
+ diagnosticsPausedDurationMs?: number;
172
187
  // When an upload fails, the recording is saved to the user's Downloads as a
173
188
  // fallback so it is never lost; these describe that saved file.
174
189
  savedToDisk?: boolean;
@@ -236,6 +251,11 @@ type CaptureSession = {
236
251
  attachError: string | null;
237
252
  consoleLogs: ConsoleLog[];
238
253
  networkRequests: NetworkRequest[];
254
+ interactionEvents: InteractionEvent[];
255
+ clickInputIngressWindowStartedAtMs: number;
256
+ clickInputIngressCount: number;
257
+ diagnosticsPausedAtMs: number | null;
258
+ diagnosticsPausedDurationMs: number;
239
259
  pendingNetworkRequests: Map<string, PendingNetworkRequest>;
240
260
  };
241
261
 
@@ -398,6 +418,15 @@ async function restoreRuntimeState(): Promise<void> {
398
418
  ) {
399
419
  activeNativeRecording = restoredRecording;
400
420
  }
421
+ if (
422
+ activeNativeRecording &&
423
+ !sessions.has(activeNativeRecording.sessionId) &&
424
+ activeNativeRecording.status !== "complete" &&
425
+ activeNativeRecording.status !== "error"
426
+ ) {
427
+ const session = restoreCaptureSession(activeNativeRecording);
428
+ await attachSession(session);
429
+ }
401
430
  const freshArmingSessionId = await readFreshPersistedArmingSessionId(
402
431
  stored.armingNativeRecordingSessionId,
403
432
  );
@@ -549,11 +578,23 @@ async function injectContentScript(tabId: number): Promise<boolean> {
549
578
  executeScript: (args: {
550
579
  target: { tabId: number };
551
580
  files: string[];
581
+ world?: "ISOLATED" | "MAIN";
552
582
  }) => Promise<unknown>;
553
583
  };
554
584
  }
555
585
  ).scripting;
556
586
  if (!scripting) return false;
587
+ try {
588
+ await scripting.executeScript({
589
+ target: { tabId },
590
+ files: ["assets/content-history-bridge.js"],
591
+ world: "MAIN",
592
+ });
593
+ // coercion-ok: MAIN-world injection is optional; the isolated script still records.
594
+ } catch {
595
+ // Some restricted pages reject MAIN-world injection but still accept the
596
+ // isolated overlay/content script.
597
+ }
557
598
  try {
558
599
  await scripting.executeScript({
559
600
  target: { tabId },
@@ -595,11 +636,16 @@ async function broadcastMount(): Promise<void> {
595
636
  return;
596
637
  }
597
638
  const parts = desiredParts();
639
+ const resetDiagnosticQuotas = overlayPhase === "recording";
598
640
  const tabs = await allTabs();
599
641
  await Promise.all(
600
642
  tabs.map((tab) =>
601
643
  typeof tab.id === "number"
602
- ? sendTabMessage(tab.id, { type: "CLIPS_OVERLAY_MOUNT", parts })
644
+ ? sendTabMessage(tab.id, {
645
+ type: "CLIPS_OVERLAY_MOUNT",
646
+ parts,
647
+ ...(resetDiagnosticQuotas ? { resetDiagnosticQuotas: true } : {}),
648
+ })
603
649
  : Promise.resolve(),
604
650
  ),
605
651
  );
@@ -1329,6 +1375,11 @@ function createSession(
1329
1375
  attachError: null,
1330
1376
  consoleLogs: [],
1331
1377
  networkRequests: [],
1378
+ interactionEvents: [],
1379
+ clickInputIngressWindowStartedAtMs: 0,
1380
+ clickInputIngressCount: 0,
1381
+ diagnosticsPausedAtMs: null,
1382
+ diagnosticsPausedDurationMs: 0,
1332
1383
  pendingNetworkRequests: new Map(),
1333
1384
  };
1334
1385
  sessions.set(sessionId, session);
@@ -1336,6 +1387,37 @@ function createSession(
1336
1387
  return session;
1337
1388
  }
1338
1389
 
1390
+ function restoreCaptureSession(recording: NativeRecording): CaptureSession {
1391
+ const session = createSession(
1392
+ recording.sessionId,
1393
+ {
1394
+ id: recording.targetTabId,
1395
+ title: recording.targetTitle ?? undefined,
1396
+ url: recording.targetUrl ?? undefined,
1397
+ },
1398
+ settingsFromRecording(recording),
1399
+ );
1400
+ session.recordingId = recording.recordingId;
1401
+ session.startedAt = recording.startedAt;
1402
+ session.startedAtMs = recording.startedAtMs;
1403
+ session.diagnosticsPausedAtMs =
1404
+ typeof recording.diagnosticsPausedAtMs === "number"
1405
+ ? recording.diagnosticsPausedAtMs
1406
+ : null;
1407
+ session.diagnosticsPausedDurationMs =
1408
+ typeof recording.diagnosticsPausedDurationMs === "number"
1409
+ ? Math.max(0, recording.diagnosticsPausedDurationMs)
1410
+ : 0;
1411
+ if (session.targetUrl) {
1412
+ pushInteraction(session, {
1413
+ kind: "navigation",
1414
+ url: session.targetUrl,
1415
+ timestampMs: session.startedAtMs,
1416
+ });
1417
+ }
1418
+ return session;
1419
+ }
1420
+
1339
1421
  async function handlePopupStart(message: PopupStartMessage) {
1340
1422
  const tab = await queryActiveTab();
1341
1423
  if (!tab || typeof tab.id !== "number") {
@@ -1717,6 +1799,8 @@ async function markRecordingStarted() {
1717
1799
  : null;
1718
1800
  if (session) beginSessionCapture(session, overlayBaseEpochMs);
1719
1801
  if (activeNativeRecording) {
1802
+ delete activeNativeRecording.diagnosticsPausedAtMs;
1803
+ delete activeNativeRecording.diagnosticsPausedDurationMs;
1720
1804
  activeNativeRecording.startedAtMs = overlayBaseEpochMs;
1721
1805
  activeNativeRecording.startedAt = new Date(
1722
1806
  overlayBaseEpochMs,
@@ -1743,9 +1827,16 @@ async function handleOverlaySkip() {
1743
1827
 
1744
1828
  function handleOverlayPause() {
1745
1829
  if (overlayPhase !== "recording") return { ok: true };
1746
- overlayBaseElapsedMs += Math.max(0, nowMs() - overlayBaseEpochMs);
1830
+ const pausedAtMs = nowMs();
1831
+ overlayBaseElapsedMs += Math.max(0, pausedAtMs - overlayBaseEpochMs);
1747
1832
  overlayPhase = "paused";
1833
+ const session = activeNativeRecording
1834
+ ? sessions.get(activeNativeRecording.sessionId)
1835
+ : null;
1836
+ if (session) session.diagnosticsPausedAtMs = pausedAtMs;
1748
1837
  if (activeNativeRecording) {
1838
+ activeNativeRecording.diagnosticsPausedAtMs = pausedAtMs;
1839
+ activeNativeRecording.diagnosticsPausedDurationMs ??= 0;
1749
1840
  activeNativeRecording.status = "paused";
1750
1841
  void saveActiveNativeRecording();
1751
1842
  void sendOffscreenMessage({
@@ -1759,7 +1850,27 @@ function handleOverlayPause() {
1759
1850
 
1760
1851
  function handleOverlayResume() {
1761
1852
  if (overlayPhase !== "paused") return { ok: true };
1762
- overlayBaseEpochMs = nowMs();
1853
+ const resumedAtMs = nowMs();
1854
+ const session = activeNativeRecording
1855
+ ? sessions.get(activeNativeRecording.sessionId)
1856
+ : null;
1857
+ const pausedAtMs =
1858
+ session?.diagnosticsPausedAtMs ??
1859
+ activeNativeRecording?.diagnosticsPausedAtMs;
1860
+ if (typeof pausedAtMs === "number") {
1861
+ const pausedDurationMs = Math.max(0, resumedAtMs - pausedAtMs);
1862
+ if (session) {
1863
+ session.diagnosticsPausedDurationMs += pausedDurationMs;
1864
+ session.diagnosticsPausedAtMs = null;
1865
+ }
1866
+ if (activeNativeRecording) {
1867
+ activeNativeRecording.diagnosticsPausedDurationMs =
1868
+ (activeNativeRecording.diagnosticsPausedDurationMs ?? 0) +
1869
+ pausedDurationMs;
1870
+ delete activeNativeRecording.diagnosticsPausedAtMs;
1871
+ }
1872
+ }
1873
+ overlayBaseEpochMs = resumedAtMs;
1763
1874
  overlayPhase = "recording";
1764
1875
  if (activeNativeRecording) {
1765
1876
  activeNativeRecording.status = "recording";
@@ -2022,6 +2133,7 @@ async function saveNativeDiagnostics(
2022
2133
  endedAt: snapshot.endedAt,
2023
2134
  consoleLogs: snapshot.consoleLogs,
2024
2135
  networkRequests: snapshot.networkRequests,
2136
+ interactionEvents: snapshot.interactionEvents,
2025
2137
  },
2026
2138
  ).catch((err) => {
2027
2139
  console.warn("[clips-extension] diagnostics save failed:", err);
@@ -2164,6 +2276,22 @@ function pendingNetworkSnapshot(session: CaptureSession): NetworkRequest[] {
2164
2276
  }));
2165
2277
  }
2166
2278
 
2279
+ function diagnosticElapsedMs(
2280
+ timestampMs: number,
2281
+ session: CaptureSession,
2282
+ ): number | null {
2283
+ const elapsedMs = elapsedMsFromCaptureStart(timestampMs, session.startedAtMs);
2284
+ if (elapsedMs === null) return null;
2285
+ const activePauseMs =
2286
+ session.diagnosticsPausedAtMs === null
2287
+ ? 0
2288
+ : Math.max(0, timestampMs - session.diagnosticsPausedAtMs);
2289
+ return Math.max(
2290
+ 0,
2291
+ elapsedMs - session.diagnosticsPausedDurationMs - activePauseMs,
2292
+ );
2293
+ }
2294
+
2167
2295
  function snapshotSession(session: CaptureSession): BrowserDiagnosticsData {
2168
2296
  const endedAt = nowIso();
2169
2297
  const consoleLogs = session.consoleLogs.slice(-MAX_CONSOLE_LOGS);
@@ -2181,6 +2309,7 @@ function snapshotSession(session: CaptureSession): BrowserDiagnosticsData {
2181
2309
  endedAt,
2182
2310
  consoleLogs,
2183
2311
  networkRequests,
2312
+ interactionEvents: session.interactionEvents.slice(-800),
2184
2313
  summary: summarize({ endedAt, consoleLogs, networkRequests }),
2185
2314
  };
2186
2315
  }
@@ -2193,6 +2322,18 @@ function beginSessionCapture(
2193
2322
  session.startedAt = new Date(startedAtMs).toISOString();
2194
2323
  session.consoleLogs = [];
2195
2324
  session.networkRequests = [];
2325
+ session.interactionEvents = [];
2326
+ session.clickInputIngressWindowStartedAtMs = 0;
2327
+ session.clickInputIngressCount = 0;
2328
+ session.diagnosticsPausedAtMs = null;
2329
+ session.diagnosticsPausedDurationMs = 0;
2330
+ if (session.targetUrl) {
2331
+ pushInteraction(session, {
2332
+ kind: "navigation",
2333
+ url: session.targetUrl,
2334
+ timestampMs: startedAtMs,
2335
+ });
2336
+ }
2196
2337
  session.pendingNetworkRequests.clear();
2197
2338
  }
2198
2339
 
@@ -2248,7 +2389,7 @@ function pushConsole(
2248
2389
  const timestampMs = Number.isFinite(entry.timestampMs)
2249
2390
  ? (entry.timestampMs as number)
2250
2391
  : nowMs();
2251
- const elapsedMs = elapsedMsFromCaptureStart(timestampMs, session.startedAtMs);
2392
+ const elapsedMs = diagnosticElapsedMs(timestampMs, session);
2252
2393
  // Runtime and Log can replay old entries when the debugger attaches. Those
2253
2394
  // entries belong to the page history, not the recording that just started.
2254
2395
  if (elapsedMs === null) return;
@@ -2287,6 +2428,62 @@ function pushNetwork(session: CaptureSession, entry: NetworkRequest): void {
2287
2428
  }
2288
2429
  }
2289
2430
 
2431
+ function pushInteraction(
2432
+ session: CaptureSession,
2433
+ entry: Omit<InteractionEvent, "timestampMs" | "elapsedMs"> & {
2434
+ timestampMs?: number;
2435
+ },
2436
+ ): void {
2437
+ const timestampMs = entry.timestampMs ?? nowMs();
2438
+ const elapsedMs = diagnosticElapsedMs(timestampMs, session);
2439
+ if (elapsedMs === null) return;
2440
+ const url = entry.url
2441
+ ? sanitizeBrowserDiagnosticNavigationUrl(entry.url)
2442
+ : undefined;
2443
+ if (entry.kind === "navigation" && url) {
2444
+ const first = session.interactionEvents[0];
2445
+ if (
2446
+ session.interactionEvents.length === 1 &&
2447
+ first?.kind === "navigation" &&
2448
+ first.url === url
2449
+ ) {
2450
+ return;
2451
+ }
2452
+ }
2453
+ session.interactionEvents.push({
2454
+ timestampMs,
2455
+ elapsedMs,
2456
+ kind: entry.kind,
2457
+ ...(entry.target
2458
+ ? { target: truncate(redactString(entry.target), 200) }
2459
+ : {}),
2460
+ ...(url ? { url } : {}),
2461
+ });
2462
+ if (session.interactionEvents.length > 800) {
2463
+ session.interactionEvents.splice(0, session.interactionEvents.length - 800);
2464
+ }
2465
+ }
2466
+
2467
+ function allowClickInputIngress(
2468
+ session: CaptureSession,
2469
+ kind: InteractionKind,
2470
+ ): boolean {
2471
+ if (kind !== "click" && kind !== "input") return true;
2472
+ const now = nowMs();
2473
+ if (
2474
+ now - session.clickInputIngressWindowStartedAtMs >=
2475
+ CLICK_INPUT_INGRESS_WINDOW_MS
2476
+ ) {
2477
+ session.clickInputIngressWindowStartedAtMs = now;
2478
+ session.clickInputIngressCount = 0;
2479
+ }
2480
+ if (session.clickInputIngressCount >= MAX_CLICK_INPUT_INGRESS_PER_WINDOW) {
2481
+ return false;
2482
+ }
2483
+ session.clickInputIngressCount += 1;
2484
+ return true;
2485
+ }
2486
+
2290
2487
  function consoleLevel(value: unknown): ConsoleLevel {
2291
2488
  if (
2292
2489
  value === "debug" ||
@@ -2427,7 +2624,7 @@ function handleRequestWillBeSent(
2427
2624
  : null;
2428
2625
  if (!type || !requestId || !request) return;
2429
2626
  const timestampMs = requestTimestampMs(event);
2430
- const elapsedMs = elapsedMsFromCaptureStart(timestampMs, session.startedAtMs);
2627
+ const elapsedMs = diagnosticElapsedMs(timestampMs, session);
2431
2628
  if (elapsedMs === null) return;
2432
2629
  const url = sanitizeUrl(typeof request.url === "string" ? request.url : "");
2433
2630
  if (!url) return;
@@ -2616,7 +2813,7 @@ function ensureRestored(): Promise<void> {
2616
2813
  }
2617
2814
  void ensureRestored();
2618
2815
 
2619
- chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
2816
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
2620
2817
  if (!message || typeof message !== "object") return false;
2621
2818
  void (async () => {
2622
2819
  // Critical: never read activeNativeRecording/overlayPhase before state is
@@ -2625,7 +2822,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
2625
2822
  await ensureRestored();
2626
2823
  let response: unknown;
2627
2824
  try {
2628
- response = await dispatchRuntimeMessage(message);
2825
+ response = await dispatchRuntimeMessage(message, sender);
2629
2826
  } catch (err) {
2630
2827
  console.error(
2631
2828
  "[clips-bg] message failed:",
@@ -2655,9 +2852,39 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
2655
2852
  return true;
2656
2853
  });
2657
2854
 
2658
- async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
2855
+ async function dispatchRuntimeMessage(
2856
+ message: unknown,
2857
+ sender?: chrome.runtime.MessageSender,
2858
+ ): Promise<unknown> {
2659
2859
  const type = (message as { type?: unknown }).type;
2660
2860
 
2861
+ if (type === "CLIPS_DIAGNOSTIC_INTERACTION") {
2862
+ const tabId = sender?.tab?.id;
2863
+ const sessionId =
2864
+ typeof tabId === "number" ? tabToSession.get(tabId) : null;
2865
+ const session = sessionId ? sessions.get(sessionId) : null;
2866
+ const kind = (message as { kind?: unknown }).kind;
2867
+ if (
2868
+ !session ||
2869
+ (kind !== "navigation" &&
2870
+ kind !== "click" &&
2871
+ kind !== "input" &&
2872
+ kind !== "scroll")
2873
+ ) {
2874
+ return { ok: false };
2875
+ }
2876
+ if (overlayPhase === "paused") return { ok: false };
2877
+ if (!allowClickInputIngress(session, kind)) return { ok: false };
2878
+ const target = (message as { target?: unknown }).target;
2879
+ const url = (message as { url?: unknown }).url;
2880
+ pushInteraction(session, {
2881
+ kind,
2882
+ ...(typeof target === "string" ? { target } : {}),
2883
+ ...(typeof url === "string" ? { url } : {}),
2884
+ });
2885
+ return { ok: true };
2886
+ }
2887
+
2661
2888
  if (type === "CLIPS_EXTENSION_ERROR") {
2662
2889
  const report = message as ExtensionErrorMessage;
2663
2890
  const error = new Error(report.message || "Chrome extension error");
@@ -2997,7 +3224,6 @@ chrome.debugger.onDetach.addListener((source) => {
2997
3224
  if (!sessionId) return;
2998
3225
  const session = sessions.get(sessionId);
2999
3226
  if (session) session.attached = false;
3000
- tabToSession.delete(tabId);
3001
3227
  });
3002
3228
 
3003
3229
  // ---- Dev auto-reload (unpacked installs only) ------------------------------
@@ -0,0 +1,80 @@
1
+ (function clipsDiagnosticHistoryBridge() {
2
+ const installedFlag = "__clipsDiagnosticHistoryBridgeInstalled";
3
+ const page = window as unknown as Record<string, unknown>;
4
+ if (page[installedFlag]) return;
5
+ page[installedFlag] = true;
6
+ const createToken = (): string => {
7
+ const webCrypto = globalThis.crypto;
8
+ if (typeof webCrypto?.randomUUID === "function") {
9
+ return webCrypto.randomUUID();
10
+ }
11
+ const bytes = new Uint8Array(16);
12
+ let filled = false;
13
+ try {
14
+ if (typeof webCrypto?.getRandomValues === "function") {
15
+ webCrypto.getRandomValues(bytes);
16
+ filled = true;
17
+ }
18
+ // coercion-ok: insecure pages may reject Web Crypto; the fallback still creates a nonce.
19
+ } catch {}
20
+ if (!filled) {
21
+ for (let index = 0; index < bytes.length; index += 1) {
22
+ bytes[index] = Math.floor(Math.random() * 256);
23
+ }
24
+ }
25
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
26
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
27
+ const hex = Array.from(bytes, (byte) =>
28
+ byte.toString(16).padStart(2, "0"),
29
+ ).join("");
30
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
31
+ };
32
+ const token = createToken();
33
+
34
+ const notify = (): void => {
35
+ window.postMessage(
36
+ {
37
+ source: "clips-diagnostic-history",
38
+ kind: "navigation",
39
+ token,
40
+ url: window.location.href,
41
+ },
42
+ "*",
43
+ );
44
+ };
45
+ window.addEventListener("message", (event) => {
46
+ const data = event.data as { source?: unknown; kind?: unknown } | undefined;
47
+ if (
48
+ event.source === window &&
49
+ data?.source === "clips-diagnostic-history" &&
50
+ data.kind === "request-token"
51
+ ) {
52
+ window.postMessage(
53
+ { source: "clips-diagnostic-history", kind: "token", token },
54
+ "*",
55
+ );
56
+ }
57
+ });
58
+ const originalPushState = history.pushState;
59
+ const originalReplaceState = history.replaceState;
60
+ history.pushState = function patchedPushState(
61
+ this: History,
62
+ state: unknown,
63
+ unused: string,
64
+ url?: string | URL | null,
65
+ ) {
66
+ const result = originalPushState.call(this, state, unused, url);
67
+ notify();
68
+ return result;
69
+ };
70
+ history.replaceState = function patchedReplaceState(
71
+ this: History,
72
+ state: unknown,
73
+ unused: string,
74
+ url?: string | URL | null,
75
+ ) {
76
+ const result = originalReplaceState.call(this, state, unused, url);
77
+ notify();
78
+ return result;
79
+ };
80
+ })();