@agent-native/core 0.84.18 → 0.84.21

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 (157) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +21 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/caller-auth.ts +37 -8
  5. package/corpus/core/src/a2a/client.ts +178 -67
  6. package/corpus/core/src/agent/production-agent.ts +19 -3
  7. package/corpus/core/src/agent/run-store.ts +180 -13
  8. package/corpus/core/src/agent/types.ts +1 -1
  9. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  10. package/corpus/core/src/cli/recap.ts +43 -34
  11. package/corpus/core/src/client/AssistantChat.tsx +49 -0
  12. package/corpus/core/src/client/PoweredByBadge.tsx +70 -17
  13. package/corpus/core/src/client/active-run-state.ts +30 -0
  14. package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
  15. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +41 -3
  16. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +133 -0
  17. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +2 -2
  18. package/corpus/core/src/client/i18n.tsx +10 -5
  19. package/corpus/core/src/client/sse-event-processor.ts +147 -5
  20. package/corpus/core/src/extensions/html-shell.ts +17 -3
  21. package/corpus/core/src/extensions/theme.ts +39 -0
  22. package/corpus/core/src/file-upload/builder.ts +35 -14
  23. package/corpus/core/src/file-upload/types.ts +3 -0
  24. package/corpus/core/src/integrations/a2a-continuation-processor.ts +49 -24
  25. package/corpus/core/src/mcp/builtin-tools.ts +3 -0
  26. package/corpus/core/src/mcp/org-directory.ts +66 -20
  27. package/corpus/core/src/scripts/call-agent.ts +7 -2
  28. package/corpus/templates/analytics/actions/provider-api-request.ts +8 -1
  29. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -1
  30. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +91 -33
  31. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
  32. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
  33. package/corpus/templates/calendar/app/pages/BookingPage.tsx +14 -8
  34. package/corpus/templates/calendar/changelog/2026-07-01-meeting-invite-pages-have-cleaner-branding-a-black-dark-mo.md +6 -0
  35. package/corpus/templates/clips/actions/create-recording.ts +7 -2
  36. package/corpus/templates/clips/actions/finalize-recording.ts +11 -0
  37. package/corpus/templates/clips/actions/get-recording-player-data.ts +4 -2
  38. package/corpus/templates/clips/actions/import-loom-recording.ts +1 -0
  39. package/corpus/templates/clips/actions/lib/loom-video.ts +16 -0
  40. package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -2
  41. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +4 -3
  42. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +39 -12
  43. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +65 -1
  44. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +1 -22
  45. package/corpus/templates/clips/app/routes/record.tsx +48 -8
  46. package/corpus/templates/clips/changelog/2026-07-01-chrome-recording-uploads-now-serialize-chunks-to-avoid-resum.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-loom-imports-finish-sooner-by-skipping-the-.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-playback-recover-more-reliably-from-storage.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-01-desktop-recording-avoids-unsupported-browser-tab-capture.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-01-embedded-player-progress-is-easier-to-see-on-dark-videos.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-01-loom-imports-show-clear-guidance-when-loom-does-not-provide-a-downloadable-video.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-01-preserve-in-flight-streaming-uploads-when-the-streaming-kill.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-01-recording-controls-are-cleaner-and-the-countdown-uses-shadow.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-01-recordings-now-recover-when-final-upload-status-gets-lost.md +6 -0
  55. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  56. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +7 -224
  57. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +94 -29
  58. package/corpus/templates/clips/desktop/src/lib/recorder.ts +87 -5
  59. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +7 -0
  60. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -1
  61. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +4 -1
  62. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +96 -1
  63. package/corpus/templates/clips/shared/finalize-recovery.ts +242 -0
  64. package/corpus/templates/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
  65. package/corpus/templates/plan/app/global.css +1 -1
  66. package/dist/a2a/caller-auth.d.ts +1 -0
  67. package/dist/a2a/caller-auth.d.ts.map +1 -1
  68. package/dist/a2a/caller-auth.js +34 -8
  69. package/dist/a2a/caller-auth.js.map +1 -1
  70. package/dist/a2a/client.d.ts +4 -0
  71. package/dist/a2a/client.d.ts.map +1 -1
  72. package/dist/a2a/client.js +148 -58
  73. package/dist/a2a/client.js.map +1 -1
  74. package/dist/agent/production-agent.d.ts.map +1 -1
  75. package/dist/agent/production-agent.js +19 -4
  76. package/dist/agent/production-agent.js.map +1 -1
  77. package/dist/agent/run-store.d.ts +19 -6
  78. package/dist/agent/run-store.d.ts.map +1 -1
  79. package/dist/agent/run-store.js +163 -13
  80. package/dist/agent/run-store.js.map +1 -1
  81. package/dist/agent/types.d.ts +1 -0
  82. package/dist/agent/types.d.ts.map +1 -1
  83. package/dist/agent/types.js.map +1 -1
  84. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  85. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  86. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  87. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  88. package/dist/cli/recap.d.ts +3 -3
  89. package/dist/cli/recap.d.ts.map +1 -1
  90. package/dist/cli/recap.js +41 -34
  91. package/dist/cli/recap.js.map +1 -1
  92. package/dist/client/AssistantChat.d.ts +2 -0
  93. package/dist/client/AssistantChat.d.ts.map +1 -1
  94. package/dist/client/AssistantChat.js +36 -2
  95. package/dist/client/AssistantChat.js.map +1 -1
  96. package/dist/client/PoweredByBadge.d.ts +9 -3
  97. package/dist/client/PoweredByBadge.d.ts.map +1 -1
  98. package/dist/client/PoweredByBadge.js +47 -15
  99. package/dist/client/PoweredByBadge.js.map +1 -1
  100. package/dist/client/active-run-state.d.ts +3 -0
  101. package/dist/client/active-run-state.d.ts.map +1 -1
  102. package/dist/client/active-run-state.js +24 -0
  103. package/dist/client/active-run-state.js.map +1 -1
  104. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  105. package/dist/client/agent-chat-adapter.js +5 -1
  106. package/dist/client/agent-chat-adapter.js.map +1 -1
  107. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  108. package/dist/client/extensions/EmbeddedExtension.js +32 -3
  109. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  110. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  111. package/dist/client/extensions/ExtensionViewer.js +59 -2
  112. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  113. package/dist/client/extensions/ExtensionsSidebarSection.js +2 -2
  114. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  115. package/dist/client/i18n.d.ts +1 -1
  116. package/dist/client/i18n.d.ts.map +1 -1
  117. package/dist/client/i18n.js +6 -2
  118. package/dist/client/i18n.js.map +1 -1
  119. package/dist/client/sse-event-processor.d.ts +2 -0
  120. package/dist/client/sse-event-processor.d.ts.map +1 -1
  121. package/dist/client/sse-event-processor.js +121 -5
  122. package/dist/client/sse-event-processor.js.map +1 -1
  123. package/dist/collab/awareness.d.ts +2 -2
  124. package/dist/collab/awareness.d.ts.map +1 -1
  125. package/dist/collab/routes.d.ts +1 -1
  126. package/dist/extensions/html-shell.d.ts.map +1 -1
  127. package/dist/extensions/html-shell.js +17 -3
  128. package/dist/extensions/html-shell.js.map +1 -1
  129. package/dist/extensions/theme.d.ts +9 -0
  130. package/dist/extensions/theme.d.ts.map +1 -1
  131. package/dist/extensions/theme.js +38 -0
  132. package/dist/extensions/theme.js.map +1 -1
  133. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  134. package/dist/file-upload/builder.d.ts.map +1 -1
  135. package/dist/file-upload/builder.js +22 -8
  136. package/dist/file-upload/builder.js.map +1 -1
  137. package/dist/file-upload/types.d.ts +5 -1
  138. package/dist/file-upload/types.d.ts.map +1 -1
  139. package/dist/file-upload/types.js.map +1 -1
  140. package/dist/integrations/a2a-continuation-processor.js +43 -18
  141. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  142. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  143. package/dist/mcp/builtin-tools.js +3 -0
  144. package/dist/mcp/builtin-tools.js.map +1 -1
  145. package/dist/mcp/org-directory.d.ts.map +1 -1
  146. package/dist/mcp/org-directory.js +58 -20
  147. package/dist/mcp/org-directory.js.map +1 -1
  148. package/dist/notifications/routes.d.ts +3 -3
  149. package/dist/observability/routes.d.ts +6 -6
  150. package/dist/progress/routes.d.ts +1 -1
  151. package/dist/resources/handlers.d.ts +3 -3
  152. package/dist/scripts/call-agent.d.ts.map +1 -1
  153. package/dist/scripts/call-agent.js +2 -2
  154. package/dist/scripts/call-agent.js.map +1 -1
  155. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  156. package/dist/server/transcribe-voice.d.ts +1 -1
  157. package/package.json +1 -1
@@ -7,6 +7,7 @@
7
7
  * and `onError`.
8
8
  */
9
9
  import { appBasePath, captureClientException } from "@agent-native/core/client";
10
+ import { waitForReadyRecordingAfterFinalizeError } from "@shared/finalize-recovery";
10
11
  import {
11
12
  chunkUploadUrl,
12
13
  pickMimeType,
@@ -40,6 +41,25 @@ export type DisplaySurface = "monitor" | "window" | "browser";
40
41
  export const NO_MIC_DEVICE_ID = "__clips_no_microphone__";
41
42
  export const NO_CAMERA_DEVICE_ID = "__clips_no_camera__";
42
43
 
44
+ export function supportsBrowserTabCapture(): boolean {
45
+ if (typeof navigator === "undefined") return false;
46
+ const userAgent = navigator.userAgent || "";
47
+ if (/AgentNativeDesktop|Electron|Tauri|WKWebView|WebView/i.test(userAgent)) {
48
+ return false;
49
+ }
50
+ const isChromium =
51
+ /Chrome|Chromium|CriOS|Edg|OPR/i.test(userAgent) &&
52
+ !/Firefox|FxiOS/i.test(userAgent);
53
+ return isChromium;
54
+ }
55
+
56
+ export function normalizeDisplaySurfaceForRuntime(
57
+ surface: DisplaySurface,
58
+ ): DisplaySurface {
59
+ if (surface === "browser" && !supportsBrowserTabCapture()) return "window";
60
+ return surface;
61
+ }
62
+
43
63
  type ExtendedDisplayMediaOptions = DisplayMediaStreamOptions & {
44
64
  video: MediaTrackConstraints & { displaySurface?: DisplaySurface };
45
65
  preferCurrentTab?: boolean;
@@ -626,7 +646,9 @@ export class RecorderEngine {
626
646
  // directly anchored to the user's click. Camera/mic prompts do not need
627
647
  // that transient activation, and launching them in parallel with the
628
648
  // screen picker can make Chrome/macOS report a false permission failure.
629
- const displaySurface = this.opts.displaySurface ?? "window";
649
+ const displaySurface = normalizeDisplaySurfaceForRuntime(
650
+ this.opts.displaySurface ?? "window",
651
+ );
630
652
  const displayOptions: ExtendedDisplayMediaOptions = {
631
653
  video: {
632
654
  frameRate: {
@@ -1699,6 +1721,7 @@ export class RecorderEngine {
1699
1721
 
1700
1722
  const body = await blob.arrayBuffer();
1701
1723
  let res: Response | null = null;
1724
+ let triedFinalUploadRecovery = false;
1702
1725
  for (let attempt = 1; attempt <= CHUNK_UPLOAD_MAX_ATTEMPTS; attempt++) {
1703
1726
  try {
1704
1727
  res = await fetch(url, {
@@ -1718,6 +1741,13 @@ export class RecorderEngine {
1718
1741
  attempt >= CHUNK_UPLOAD_MAX_ATTEMPTS ||
1719
1742
  (err as { name?: string } | null)?.name === "AbortError"
1720
1743
  ) {
1744
+ if (
1745
+ extra.isFinal &&
1746
+ (err as { name?: string } | null)?.name !== "AbortError"
1747
+ ) {
1748
+ const recovered = await this.recoverReadyAfterFinalUploadError();
1749
+ if (recovered) return recovered;
1750
+ }
1721
1751
  throw err;
1722
1752
  }
1723
1753
  await waitForRetry(retryDelayMs(attempt), extra.signal);
@@ -1729,6 +1759,13 @@ export class RecorderEngine {
1729
1759
  attempt < CHUNK_UPLOAD_MAX_ATTEMPTS &&
1730
1760
  isRetryableChunkUploadStatus(res.status)
1731
1761
  ) {
1762
+ if (extra.isFinal && res.status === 504) {
1763
+ triedFinalUploadRecovery = true;
1764
+ await res.text().catch(() => "");
1765
+ const recovered = await this.recoverReadyAfterFinalUploadError();
1766
+ if (recovered) return recovered;
1767
+ break;
1768
+ }
1732
1769
  await res.text().catch(() => "");
1733
1770
  await waitForRetry(retryDelayMs(attempt), extra.signal);
1734
1771
  continue;
@@ -1746,6 +1783,14 @@ export class RecorderEngine {
1746
1783
  const err = new Error(
1747
1784
  `Chunk ${index} upload failed (${res.status}): ${text || res.statusText}`,
1748
1785
  );
1786
+ if (
1787
+ extra.isFinal &&
1788
+ !triedFinalUploadRecovery &&
1789
+ this.isFinalUploadRecoveryCandidate(res.status, err)
1790
+ ) {
1791
+ const recovered = await this.recoverReadyAfterFinalUploadError();
1792
+ if (recovered) return recovered;
1793
+ }
1749
1794
  // Capture rich context to Sentry BEFORE throwing — when this hits
1750
1795
  // production we want enough breadcrumbs in the event to debug a
1751
1796
  // "Builder.io upload failed (500)" without re-running the upload.
@@ -1799,6 +1844,25 @@ export class RecorderEngine {
1799
1844
  }
1800
1845
  }
1801
1846
 
1847
+ private isFinalUploadRecoveryCandidate(
1848
+ status: number,
1849
+ error: Error,
1850
+ ): boolean {
1851
+ if (status === 413) return false;
1852
+ return !/too large|exceeds.*limit|chunk too large/i.test(error.message);
1853
+ }
1854
+
1855
+ private async recoverReadyAfterFinalUploadError(): Promise<Record<
1856
+ string,
1857
+ unknown
1858
+ > | null> {
1859
+ return waitForReadyRecordingAfterFinalizeError({
1860
+ uploadUrl: this.opts.uploadUrl,
1861
+ recordingId: this.opts.recordingId,
1862
+ preferAuthenticated: true,
1863
+ });
1864
+ }
1865
+
1802
1866
  private readDimensions(): { width: number; height: number } {
1803
1867
  const videoTrack =
1804
1868
  this.combinedStream?.getVideoTracks()[0] ||
@@ -1,6 +1,5 @@
1
1
  import { useT } from "@agent-native/core/client";
2
2
  import {
3
- IconConfetti,
4
3
  IconPlayerPause,
5
4
  IconPlayerPlay,
6
5
  IconPlayerStop,
@@ -13,7 +12,6 @@ import {
13
12
  TooltipContent,
14
13
  TooltipTrigger,
15
14
  } from "@/components/ui/tooltip";
16
- import { isMacPlatform } from "@/lib/utils";
17
15
 
18
16
  import { clampRectToViewport, type BubblePosition } from "./camera-positioner";
19
17
 
@@ -22,11 +20,10 @@ export interface RecordingToolbarProps {
22
20
  isPaused: boolean;
23
21
  onTogglePause: () => void;
24
22
  onStop: () => void;
25
- onConfetti: () => void;
26
23
  onCancel: () => void;
27
24
  }
28
25
 
29
- const TOOLBAR_WIDTH = 276;
26
+ const TOOLBAR_WIDTH = 232;
30
27
  const TOOLBAR_HEIGHT = 56;
31
28
  // Drop the toolbar just below the centered "Recording your screen…" status
32
29
  // text (which sits at the viewport's vertical center) so the controls don't
@@ -45,7 +42,6 @@ export function RecordingToolbar({
45
42
  isPaused,
46
43
  onTogglePause,
47
44
  onStop,
48
- onConfetti,
49
45
  onCancel,
50
46
  }: RecordingToolbarProps) {
51
47
  const t = useT();
@@ -201,23 +197,6 @@ export function RecordingToolbar({
201
197
  )}
202
198
  </div>
203
199
 
204
- <Tooltip>
205
- <TooltipTrigger asChild>
206
- <button
207
- data-toolbar-btn
208
- type="button"
209
- onClick={onConfetti}
210
- className="flex h-9 w-9 items-center justify-center rounded-full hover:bg-white/15"
211
- aria-label="Confetti"
212
- >
213
- <IconConfetti className="h-4 w-4" />
214
- </button>
215
- </TooltipTrigger>
216
- <TooltipContent>
217
- Confetti ({isMacPlatform() ? "Ctrl+\u2318+C" : "Ctrl+Alt+C"})
218
- </TooltipContent>
219
- </Tooltip>
220
-
221
200
  <Tooltip>
222
201
  <TooltipTrigger asChild>
223
202
  <button
@@ -7,6 +7,7 @@ import {
7
7
  } from "@agent-native/core/client";
8
8
  import { useLiveTranscription } from "@agent-native/core/client/transcription/use-live-transcription";
9
9
  import type { BrowserDiagnosticsData } from "@shared/browser-diagnostics";
10
+ import { waitForReadyRecordingAfterFinalizeError } from "@shared/finalize-recovery";
10
11
  import {
11
12
  chunkUploadUrl,
12
13
  pickMimeType,
@@ -498,6 +499,10 @@ function friendlyRecordingErrorMessage(error: string): string {
498
499
  return error;
499
500
  }
500
501
 
502
+ function userFacingActionErrorMessage(error: string): string {
503
+ return error.replace(/^Action [a-z0-9-]+ failed:\s*/i, "").trim() || error;
504
+ }
505
+
501
506
  function captureThumbnailFromPreview(
502
507
  video: HTMLVideoElement | null,
503
508
  recordingId: string,
@@ -1562,12 +1567,32 @@ export default function RecordRoute() {
1562
1567
  hasAudio: isFinal ? true : undefined,
1563
1568
  hasCamera: isFinal ? false : undefined,
1564
1569
  });
1565
- const chunkRes = await fetch(chunkUrl, {
1566
- method: "POST",
1567
- headers: { "Content-Type": uploadMimeType },
1568
- body: await slice.arrayBuffer(),
1569
- signal: abort.signal,
1570
- });
1570
+ let chunkRes: Response;
1571
+ try {
1572
+ chunkRes = await fetch(chunkUrl, {
1573
+ method: "POST",
1574
+ headers: { "Content-Type": uploadMimeType },
1575
+ body: await slice.arrayBuffer(),
1576
+ signal: abort.signal,
1577
+ });
1578
+ } catch (err) {
1579
+ if (
1580
+ isFinal &&
1581
+ createdId &&
1582
+ (err as { name?: string } | null)?.name !== "AbortError"
1583
+ ) {
1584
+ const recovered = await waitForReadyRecordingAfterFinalizeError({
1585
+ uploadUrl: uploadBase,
1586
+ recordingId: createdId,
1587
+ preferAuthenticated: true,
1588
+ });
1589
+ if (recovered) {
1590
+ finalChunkResult = recovered;
1591
+ break;
1592
+ }
1593
+ }
1594
+ throw err;
1595
+ }
1571
1596
  if (!chunkRes.ok) {
1572
1597
  const text = await chunkRes.text().catch(() => "");
1573
1598
  const error = new Error(
@@ -1578,6 +1603,22 @@ export default function RecordRoute() {
1578
1603
  }),
1579
1604
  );
1580
1605
  (error as Error & { status?: number }).status = chunkRes.status;
1606
+ if (
1607
+ isFinal &&
1608
+ createdId &&
1609
+ chunkRes.status !== 413 &&
1610
+ !isUploadSizeError(error.message)
1611
+ ) {
1612
+ const recovered = await waitForReadyRecordingAfterFinalizeError({
1613
+ uploadUrl: uploadBase,
1614
+ recordingId: createdId,
1615
+ preferAuthenticated: true,
1616
+ });
1617
+ if (recovered) {
1618
+ finalChunkResult = recovered;
1619
+ break;
1620
+ }
1621
+ }
1581
1622
  throw error;
1582
1623
  }
1583
1624
  if (isFinal) {
@@ -1708,7 +1749,7 @@ export default function RecordRoute() {
1708
1749
  } catch (err) {
1709
1750
  throw new Error(
1710
1751
  err instanceof Error
1711
- ? err.message
1752
+ ? userFacingActionErrorMessage(err.message)
1712
1753
  : t("recordRoute.couldNotImportLoom"),
1713
1754
  );
1714
1755
  } finally {
@@ -2377,7 +2418,6 @@ export default function RecordRoute() {
2377
2418
  isPaused={isPaused}
2378
2419
  onTogglePause={togglePause}
2379
2420
  onStop={() => void doStop()}
2380
- onConfetti={fireConfetti}
2381
2421
  onCancel={() => void doCancel()}
2382
2422
  />
2383
2423
  )}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Chrome recording uploads now serialize chunks to avoid resumable upload offset races.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Clip uploads and Loom imports finish sooner by skipping the Builder.io compression wait after upload registration.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Clip uploads and playback recover more reliably from storage timeouts.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Desktop recording setup no longer offers unsupported browser-tab capture in desktop app windows.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Embedded player progress is easier to see on dark videos.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Loom imports now show clear guidance when Loom does not provide a downloadable video.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Preserve in-flight streaming uploads when the streaming kill switch is disabled mid-recording.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Recording controls are cleaner and the countdown uses shadowed numbers instead of a filled circle.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Recordings now recover when the final upload finishes but the browser receives a timeout instead of the saved clip response.
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "Agent-Native Clips",
4
4
  "short_name": "Clips",
5
- "version": "0.1.8",
5
+ "version": "0.1.10",
6
6
  "description": "Start Clips recordings from Chrome, capture optional diagnostics, and preview Clips links on GitHub.",
7
7
  "minimum_chrome_version": "116",
8
8
  "action": {
@@ -1,224 +1,7 @@
1
- type FetchLike = (
2
- input: RequestInfo | URL,
3
- init?: RequestInit,
4
- ) => Promise<Response>;
5
-
6
- export type RecoveredReadyRecording = {
7
- ok: true;
8
- finalized: true;
9
- recoveredAfterFinalizeError: true;
10
- id: string;
11
- recordingId: string;
12
- status: "ready";
13
- videoUrl: string;
14
- durationMs?: number;
15
- width?: number;
16
- height?: number;
17
- hasAudio?: boolean;
18
- hasCamera?: boolean;
19
- };
20
-
21
- type ProbeResult =
22
- | { ready: true; result: RecoveredReadyRecording }
23
- | { ready: false; terminal: boolean; status?: string };
24
-
25
- const DEFAULT_READY_RECOVERY_TIMEOUT_MS = 90_000;
26
- const DEFAULT_READY_RECOVERY_INTERVAL_MS = 3_000;
27
- const DEFAULT_READY_RECOVERY_FETCH_TIMEOUT_MS = 2_000;
28
-
29
- function sleep(ms: number): Promise<void> {
30
- return new Promise((resolve) => setTimeout(resolve, ms));
31
- }
32
-
33
- export function publicRecordingStatusUrl(
34
- uploadUrl: string,
35
- recordingId: string,
36
- ): string {
37
- const url = new URL(uploadUrl);
38
- const match = url.pathname.match(/^(.*)\/api\/uploads\/[^/]+\/chunk$/);
39
- const basePath = match?.[1] ?? "";
40
- url.pathname = `${basePath}/api/public-recording`;
41
- url.search = "";
42
- url.searchParams.set("id", recordingId);
43
- return url.toString();
44
- }
45
-
46
- export function authenticatedRecordingStatusUrl(
47
- uploadUrl: string,
48
- recordingId: string,
49
- ): string {
50
- const url = new URL(uploadUrl);
51
- const match = url.pathname.match(/^(.*)\/api\/uploads\/[^/]+\/chunk$/);
52
- const basePath = match?.[1] ?? "";
53
- url.pathname = `${basePath}/api/uploads/${encodeURIComponent(recordingId)}/status`;
54
- url.search = "";
55
- return url.toString();
56
- }
57
-
58
- function optionalNumber(value: unknown): number | undefined {
59
- return typeof value === "number" && Number.isFinite(value)
60
- ? value
61
- : undefined;
62
- }
63
-
64
- function optionalBoolean(value: unknown): boolean | undefined {
65
- return typeof value === "boolean" ? value : undefined;
66
- }
67
-
68
- export function readyRecordingFromPublicPayload(
69
- payload: unknown,
70
- fallbackRecordingId: string,
71
- ): ProbeResult {
72
- const root =
73
- payload && typeof payload === "object"
74
- ? (payload as Record<string, unknown>)
75
- : null;
76
- const recording =
77
- root?.recording && typeof root.recording === "object"
78
- ? (root.recording as Record<string, unknown>)
79
- : null;
80
- const status =
81
- typeof recording?.status === "string" ? recording.status : undefined;
82
-
83
- if (!recording) return { ready: false, terminal: false };
84
- if (status === "failed") return { ready: false, terminal: true, status };
85
-
86
- const videoUrl =
87
- typeof recording.videoUrl === "string" ? recording.videoUrl : "";
88
- if (status !== "ready" || !videoUrl) {
89
- return { ready: false, terminal: false, status };
90
- }
91
-
92
- const id =
93
- typeof recording.id === "string" && recording.id
94
- ? recording.id
95
- : fallbackRecordingId;
96
- return {
97
- ready: true,
98
- result: {
99
- ok: true,
100
- finalized: true,
101
- recoveredAfterFinalizeError: true,
102
- id,
103
- recordingId: id,
104
- status: "ready",
105
- videoUrl,
106
- durationMs: optionalNumber(recording.durationMs),
107
- width: optionalNumber(recording.width),
108
- height: optionalNumber(recording.height),
109
- hasAudio: optionalBoolean(recording.hasAudio),
110
- hasCamera: optionalBoolean(recording.hasCamera),
111
- },
112
- };
113
- }
114
-
115
- async function fetchWithTimeout(
116
- fetchImpl: FetchLike,
117
- url: string,
118
- init: RequestInit,
119
- timeoutMs: number,
120
- ): Promise<Response> {
121
- if (timeoutMs <= 0 || typeof AbortController === "undefined") {
122
- return fetchImpl(url, init);
123
- }
124
-
125
- const controller = new AbortController();
126
- const timer = setTimeout(() => controller.abort(), timeoutMs);
127
- try {
128
- return await fetchImpl(url, { ...init, signal: controller.signal });
129
- } finally {
130
- clearTimeout(timer);
131
- }
132
- }
133
-
134
- export async function waitForReadyRecordingAfterFinalizeError(args: {
135
- uploadUrl: string;
136
- recordingId: string;
137
- authToken?: string | null;
138
- fetchImpl?: FetchLike;
139
- sleepImpl?: (ms: number) => Promise<void>;
140
- timeoutMs?: number;
141
- intervalMs?: number;
142
- fetchTimeoutMs?: number;
143
- }): Promise<RecoveredReadyRecording | null> {
144
- const fetchImpl = args.fetchImpl ?? fetch;
145
- const sleepImpl = args.sleepImpl ?? sleep;
146
- const timeoutMs = Math.max(
147
- 1,
148
- args.timeoutMs ?? DEFAULT_READY_RECOVERY_TIMEOUT_MS,
149
- );
150
- const intervalMs = Math.max(
151
- 1,
152
- args.intervalMs ?? DEFAULT_READY_RECOVERY_INTERVAL_MS,
153
- );
154
- const attempts = Math.max(1, Math.ceil(timeoutMs / intervalMs));
155
- let url: string;
156
- let authenticatedUrl: string | null = null;
157
- try {
158
- url = publicRecordingStatusUrl(args.uploadUrl, args.recordingId);
159
- authenticatedUrl = args.authToken
160
- ? authenticatedRecordingStatusUrl(args.uploadUrl, args.recordingId)
161
- : null;
162
- } catch {
163
- return null;
164
- }
165
-
166
- for (let attempt = 0; attempt < attempts; attempt += 1) {
167
- const urls = authenticatedUrl
168
- ? [
169
- { url: authenticatedUrl, authenticated: true },
170
- { url, authenticated: false },
171
- ]
172
- : [{ url, authenticated: false }];
173
-
174
- for (const endpoint of urls) {
175
- const headers: Record<string, string> = {
176
- Accept: "application/json",
177
- "X-Agent-Native-Frontend": "1",
178
- };
179
- if (endpoint.authenticated && args.authToken) {
180
- headers.Authorization = `Bearer ${args.authToken}`;
181
- }
182
-
183
- try {
184
- const response = await fetchWithTimeout(
185
- fetchImpl,
186
- endpoint.url,
187
- {
188
- method: "GET",
189
- headers,
190
- credentials: "include",
191
- cache: "no-store",
192
- },
193
- args.fetchTimeoutMs ?? DEFAULT_READY_RECOVERY_FETCH_TIMEOUT_MS,
194
- );
195
-
196
- if (response.ok) {
197
- const payload = await response.json().catch(() => null);
198
- const probe = readyRecordingFromPublicPayload(
199
- payload,
200
- args.recordingId,
201
- );
202
- if (probe.ready) return probe.result;
203
- if (probe.terminal) return null;
204
- break;
205
- }
206
-
207
- if (
208
- response.status >= 400 &&
209
- response.status < 500 &&
210
- !endpoint.authenticated &&
211
- !authenticatedUrl
212
- ) {
213
- return null;
214
- }
215
- } catch {
216
- // Try the public endpoint fallback below, then keep polling.
217
- }
218
- }
219
-
220
- if (attempt < attempts - 1) await sleepImpl(intervalMs);
221
- }
222
-
223
- return null;
224
- }
1
+ export {
2
+ authenticatedRecordingStatusUrl,
3
+ publicRecordingStatusUrl,
4
+ readyRecordingFromPublicPayload,
5
+ waitForReadyRecordingAfterFinalizeError,
6
+ type RecoveredReadyRecording,
7
+ } from "@shared/finalize-recovery";