@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +21 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/caller-auth.ts +37 -8
- package/corpus/core/src/a2a/client.ts +178 -67
- package/corpus/core/src/agent/production-agent.ts +19 -3
- package/corpus/core/src/agent/run-store.ts +180 -13
- package/corpus/core/src/agent/types.ts +1 -1
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
- package/corpus/core/src/cli/recap.ts +43 -34
- package/corpus/core/src/client/AssistantChat.tsx +49 -0
- package/corpus/core/src/client/PoweredByBadge.tsx +70 -17
- package/corpus/core/src/client/active-run-state.ts +30 -0
- package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +41 -3
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +133 -0
- package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +2 -2
- package/corpus/core/src/client/i18n.tsx +10 -5
- package/corpus/core/src/client/sse-event-processor.ts +147 -5
- package/corpus/core/src/extensions/html-shell.ts +17 -3
- package/corpus/core/src/extensions/theme.ts +39 -0
- package/corpus/core/src/file-upload/builder.ts +35 -14
- package/corpus/core/src/file-upload/types.ts +3 -0
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +49 -24
- package/corpus/core/src/mcp/builtin-tools.ts +3 -0
- package/corpus/core/src/mcp/org-directory.ts +66 -20
- package/corpus/core/src/scripts/call-agent.ts +7 -2
- package/corpus/templates/analytics/actions/provider-api-request.ts +8 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +91 -33
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
- package/corpus/templates/calendar/app/pages/BookingPage.tsx +14 -8
- package/corpus/templates/calendar/changelog/2026-07-01-meeting-invite-pages-have-cleaner-branding-a-black-dark-mo.md +6 -0
- package/corpus/templates/clips/actions/create-recording.ts +7 -2
- package/corpus/templates/clips/actions/finalize-recording.ts +11 -0
- package/corpus/templates/clips/actions/get-recording-player-data.ts +4 -2
- package/corpus/templates/clips/actions/import-loom-recording.ts +1 -0
- package/corpus/templates/clips/actions/lib/loom-video.ts +16 -0
- package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -2
- package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +4 -3
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +39 -12
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +65 -1
- package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +1 -22
- package/corpus/templates/clips/app/routes/record.tsx +48 -8
- package/corpus/templates/clips/changelog/2026-07-01-chrome-recording-uploads-now-serialize-chunks-to-avoid-resum.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-loom-imports-finish-sooner-by-skipping-the-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-playback-recover-more-reliably-from-storage.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-desktop-recording-avoids-unsupported-browser-tab-capture.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-embedded-player-progress-is-easier-to-see-on-dark-videos.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-loom-imports-show-clear-guidance-when-loom-does-not-provide-a-downloadable-video.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-preserve-in-flight-streaming-uploads-when-the-streaming-kill.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-recording-controls-are-cleaner-and-the-countdown-uses-shadow.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-recordings-now-recover-when-final-upload-status-gets-lost.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +7 -224
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +94 -29
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +87 -5
- package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +7 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +4 -1
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +96 -1
- package/corpus/templates/clips/shared/finalize-recovery.ts +242 -0
- package/corpus/templates/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
- package/corpus/templates/plan/app/global.css +1 -1
- package/dist/a2a/caller-auth.d.ts +1 -0
- package/dist/a2a/caller-auth.d.ts.map +1 -1
- package/dist/a2a/caller-auth.js +34 -8
- package/dist/a2a/caller-auth.js.map +1 -1
- package/dist/a2a/client.d.ts +4 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +148 -58
- package/dist/a2a/client.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +19 -4
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts +19 -6
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +163 -13
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/types.d.ts +1 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +3 -3
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +41 -34
- package/dist/cli/recap.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +2 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +36 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/PoweredByBadge.d.ts +9 -3
- package/dist/client/PoweredByBadge.d.ts.map +1 -1
- package/dist/client/PoweredByBadge.js +47 -15
- package/dist/client/PoweredByBadge.js.map +1 -1
- package/dist/client/active-run-state.d.ts +3 -0
- package/dist/client/active-run-state.d.ts.map +1 -1
- package/dist/client/active-run-state.js +24 -0
- package/dist/client/active-run-state.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +5 -1
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +32 -3
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +59 -2
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js +2 -2
- package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
- package/dist/client/i18n.d.ts +1 -1
- package/dist/client/i18n.d.ts.map +1 -1
- package/dist/client/i18n.js +6 -2
- package/dist/client/i18n.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +2 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +121 -5
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/extensions/html-shell.d.ts.map +1 -1
- package/dist/extensions/html-shell.js +17 -3
- package/dist/extensions/html-shell.js.map +1 -1
- package/dist/extensions/theme.d.ts +9 -0
- package/dist/extensions/theme.d.ts.map +1 -1
- package/dist/extensions/theme.js +38 -0
- package/dist/extensions/theme.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/file-upload/builder.d.ts.map +1 -1
- package/dist/file-upload/builder.js +22 -8
- package/dist/file-upload/builder.js.map +1 -1
- package/dist/file-upload/types.d.ts +5 -1
- package/dist/file-upload/types.d.ts.map +1 -1
- package/dist/file-upload/types.js.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +43 -18
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +3 -0
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/mcp/org-directory.d.ts.map +1 -1
- package/dist/mcp/org-directory.js +58 -20
- package/dist/mcp/org-directory.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +6 -6
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +2 -2
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- 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 =
|
|
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 =
|
|
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
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
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
|
)}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Agent-Native Clips",
|
|
4
4
|
"short_name": "Clips",
|
|
5
|
-
"version": "0.1.
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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";
|