@agent-native/core 0.84.20 → 0.84.22
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 +16 -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/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 +27 -4
- package/corpus/core/src/client/PoweredByBadge.tsx +70 -17
- package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
- package/corpus/core/src/client/guided-questions.tsx +11 -6
- package/corpus/core/src/client/i18n.tsx +10 -5
- package/corpus/core/src/client/sse-event-processor.ts +147 -5
- package/corpus/core/src/client/tool-display.ts +8 -0
- package/corpus/core/src/file-upload/builder.ts +40 -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/notifications/channels.ts +235 -38
- package/corpus/core/src/scripts/call-agent.ts +7 -2
- package/corpus/core/src/server/analytics.ts +32 -6
- package/corpus/core/src/server/ssr-handler.ts +31 -12
- package/corpus/templates/analytics/AGENTS.md +21 -0
- package/corpus/templates/analytics/README.md +35 -0
- package/corpus/templates/analytics/actions/delete-analytics-alert-rule.ts +23 -0
- package/corpus/templates/analytics/actions/list-analytics-alert-rules.ts +20 -0
- package/corpus/templates/analytics/actions/run-analytics-alerts.ts +23 -0
- package/corpus/templates/analytics/actions/save-analytics-alert-rule.ts +69 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
- package/corpus/templates/analytics/changelog/2026-07-01-analytics-can-alert-when-first-party-events-spike.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-play-back-in-production-instead-of-showing.md +6 -0
- package/corpus/templates/analytics/netlify.toml +3 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
- package/corpus/templates/analytics/server/db/schema.ts +55 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +5 -2
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +71 -0
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +734 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +10 -3
- package/corpus/templates/analytics/server/plugins/analytics-alert-jobs.ts +54 -0
- package/corpus/templates/analytics/server/plugins/db.ts +100 -0
- package/corpus/templates/analytics/server/routes/api/analytics-alerts/run.post.ts +60 -0
- package/corpus/templates/assets/README.md +30 -8
- package/corpus/templates/brain/README.md +20 -552
- package/corpus/templates/calendar/README.md +22 -28
- 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/chat/README.md +32 -0
- package/corpus/templates/clips/README.md +37 -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/library/library-grid.tsx +18 -7
- package/corpus/templates/clips/app/components/library/recording-card.tsx +42 -5
- 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 +106 -2
- 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-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-move-to-folder-now-opens-folder-choices-directly-instead-of.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 +64 -6
- 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 +51 -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/content/AGENTS.md +7 -3
- package/corpus/templates/content/README.md +22 -34
- package/corpus/templates/content/actions/list-trashed-content-databases.ts +4 -0
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +58 -6
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +257 -24
- package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +41 -15
- package/corpus/templates/content/app/i18n/zh-TW.ts +25 -7
- package/corpus/templates/content/app/i18n-data.ts +278 -40
- package/corpus/templates/content/changelog/2026-06-30-database-once-again-creates-an-inline-database-in-the-curren.md +6 -0
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/design/AGENTS.md +7 -6
- package/corpus/templates/design/README.md +33 -0
- package/corpus/templates/design/actions/delete-file.ts +86 -10
- package/corpus/templates/design/actions/get-design-snapshot.ts +13 -0
- package/corpus/templates/design/actions/present-design-variants.ts +11 -13
- package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2 -2
- package/corpus/templates/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
- package/corpus/templates/dispatch/README.md +34 -0
- package/corpus/templates/forms/README.md +34 -0
- package/corpus/templates/macros/README.md +28 -0
- package/corpus/templates/mail/README.md +35 -0
- package/corpus/templates/plan/README.md +30 -118
- package/corpus/templates/plan/app/global.css +1 -1
- package/corpus/templates/slides/README.md +34 -0
- package/corpus/templates/videos/README.md +22 -201
- 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/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.map +1 -1
- package/dist/client/AssistantChat.js +28 -4
- 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/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/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +10 -6
- package/dist/client/guided-questions.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/client/tool-display.d.ts.map +1 -1
- package/dist/client/tool-display.js +8 -0
- package/dist/client/tool-display.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/file-upload/builder.d.ts.map +1 -1
- package/dist/file-upload/builder.js +26 -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/channels.d.ts +10 -0
- package/dist/notifications/channels.d.ts.map +1 -1
- package/dist/notifications/channels.js +172 -20
- package/dist/notifications/channels.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- 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/analytics.d.ts +16 -0
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +30 -7
- package/dist/server/analytics.js.map +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +28 -12
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
* but no React state lives here — callers subscribe via `onState`, `onChunk`,
|
|
7
7
|
* and `onError`.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
appBasePath,
|
|
11
|
+
captureClientException,
|
|
12
|
+
trackEvent,
|
|
13
|
+
} from "@agent-native/core/client";
|
|
14
|
+
import { waitForReadyRecordingAfterFinalizeError } from "@shared/finalize-recovery";
|
|
10
15
|
import {
|
|
11
16
|
chunkUploadUrl,
|
|
12
17
|
pickMimeType,
|
|
@@ -40,6 +45,25 @@ export type DisplaySurface = "monitor" | "window" | "browser";
|
|
|
40
45
|
export const NO_MIC_DEVICE_ID = "__clips_no_microphone__";
|
|
41
46
|
export const NO_CAMERA_DEVICE_ID = "__clips_no_camera__";
|
|
42
47
|
|
|
48
|
+
export function supportsBrowserTabCapture(): boolean {
|
|
49
|
+
if (typeof navigator === "undefined") return false;
|
|
50
|
+
const userAgent = navigator.userAgent || "";
|
|
51
|
+
if (/AgentNativeDesktop|Electron|Tauri|WKWebView|WebView/i.test(userAgent)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const isChromium =
|
|
55
|
+
/Chrome|Chromium|CriOS|Edg|OPR/i.test(userAgent) &&
|
|
56
|
+
!/Firefox|FxiOS/i.test(userAgent);
|
|
57
|
+
return isChromium;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeDisplaySurfaceForRuntime(
|
|
61
|
+
surface: DisplaySurface,
|
|
62
|
+
): DisplaySurface {
|
|
63
|
+
if (surface === "browser" && !supportsBrowserTabCapture()) return "window";
|
|
64
|
+
return surface;
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
type ExtendedDisplayMediaOptions = DisplayMediaStreamOptions & {
|
|
44
68
|
video: MediaTrackConstraints & { displaySurface?: DisplaySurface };
|
|
45
69
|
preferCurrentTab?: boolean;
|
|
@@ -334,6 +358,19 @@ function isRetryableChunkUploadStatus(status: number): boolean {
|
|
|
334
358
|
return RETRYABLE_CHUNK_UPLOAD_STATUSES.has(status);
|
|
335
359
|
}
|
|
336
360
|
|
|
361
|
+
function trackClipUploadBlockingFailure(props: Record<string, unknown>): void {
|
|
362
|
+
try {
|
|
363
|
+
trackEvent("clips_upload_blocking_failure", {
|
|
364
|
+
app: "clips",
|
|
365
|
+
template: "clips",
|
|
366
|
+
surface: "web_recorder",
|
|
367
|
+
...props,
|
|
368
|
+
});
|
|
369
|
+
} catch {
|
|
370
|
+
// Analytics should never change recording behavior.
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
337
374
|
function retryDelayMs(attempt: number): number {
|
|
338
375
|
return attempt === 1 ? 500 : 1500;
|
|
339
376
|
}
|
|
@@ -626,7 +663,9 @@ export class RecorderEngine {
|
|
|
626
663
|
// directly anchored to the user's click. Camera/mic prompts do not need
|
|
627
664
|
// that transient activation, and launching them in parallel with the
|
|
628
665
|
// screen picker can make Chrome/macOS report a false permission failure.
|
|
629
|
-
const displaySurface =
|
|
666
|
+
const displaySurface = normalizeDisplaySurfaceForRuntime(
|
|
667
|
+
this.opts.displaySurface ?? "window",
|
|
668
|
+
);
|
|
630
669
|
const displayOptions: ExtendedDisplayMediaOptions = {
|
|
631
670
|
video: {
|
|
632
671
|
frameRate: {
|
|
@@ -1699,6 +1738,7 @@ export class RecorderEngine {
|
|
|
1699
1738
|
|
|
1700
1739
|
const body = await blob.arrayBuffer();
|
|
1701
1740
|
let res: Response | null = null;
|
|
1741
|
+
let triedFinalUploadRecovery = false;
|
|
1702
1742
|
for (let attempt = 1; attempt <= CHUNK_UPLOAD_MAX_ATTEMPTS; attempt++) {
|
|
1703
1743
|
try {
|
|
1704
1744
|
res = await fetch(url, {
|
|
@@ -1718,6 +1758,13 @@ export class RecorderEngine {
|
|
|
1718
1758
|
attempt >= CHUNK_UPLOAD_MAX_ATTEMPTS ||
|
|
1719
1759
|
(err as { name?: string } | null)?.name === "AbortError"
|
|
1720
1760
|
) {
|
|
1761
|
+
if (
|
|
1762
|
+
extra.isFinal &&
|
|
1763
|
+
(err as { name?: string } | null)?.name !== "AbortError"
|
|
1764
|
+
) {
|
|
1765
|
+
const recovered = await this.recoverReadyAfterFinalUploadError();
|
|
1766
|
+
if (recovered) return recovered;
|
|
1767
|
+
}
|
|
1721
1768
|
throw err;
|
|
1722
1769
|
}
|
|
1723
1770
|
await waitForRetry(retryDelayMs(attempt), extra.signal);
|
|
@@ -1729,6 +1776,13 @@ export class RecorderEngine {
|
|
|
1729
1776
|
attempt < CHUNK_UPLOAD_MAX_ATTEMPTS &&
|
|
1730
1777
|
isRetryableChunkUploadStatus(res.status)
|
|
1731
1778
|
) {
|
|
1779
|
+
if (extra.isFinal && res.status === 504) {
|
|
1780
|
+
triedFinalUploadRecovery = true;
|
|
1781
|
+
await res.text().catch(() => "");
|
|
1782
|
+
const recovered = await this.recoverReadyAfterFinalUploadError();
|
|
1783
|
+
if (recovered) return recovered;
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1732
1786
|
await res.text().catch(() => "");
|
|
1733
1787
|
await waitForRetry(retryDelayMs(attempt), extra.signal);
|
|
1734
1788
|
continue;
|
|
@@ -1738,6 +1792,15 @@ export class RecorderEngine {
|
|
|
1738
1792
|
}
|
|
1739
1793
|
|
|
1740
1794
|
if (!res) {
|
|
1795
|
+
trackClipUploadBlockingFailure({
|
|
1796
|
+
stage: "chunk_upload",
|
|
1797
|
+
failureKind: "no_response",
|
|
1798
|
+
recordingId: this.opts.recordingId,
|
|
1799
|
+
chunkIndex: index,
|
|
1800
|
+
isFinal: extra.isFinal === true,
|
|
1801
|
+
chunkBytes: blob.size,
|
|
1802
|
+
uploadMode: this.opts.uploadMode,
|
|
1803
|
+
});
|
|
1741
1804
|
throw new Error(`Chunk ${index} upload failed: no response`);
|
|
1742
1805
|
}
|
|
1743
1806
|
|
|
@@ -1746,6 +1809,28 @@ export class RecorderEngine {
|
|
|
1746
1809
|
const err = new Error(
|
|
1747
1810
|
`Chunk ${index} upload failed (${res.status}): ${text || res.statusText}`,
|
|
1748
1811
|
);
|
|
1812
|
+
if (
|
|
1813
|
+
extra.isFinal &&
|
|
1814
|
+
!triedFinalUploadRecovery &&
|
|
1815
|
+
this.isFinalUploadRecoveryCandidate(res.status, err)
|
|
1816
|
+
) {
|
|
1817
|
+
const recovered = await this.recoverReadyAfterFinalUploadError();
|
|
1818
|
+
if (recovered) return recovered;
|
|
1819
|
+
}
|
|
1820
|
+
trackClipUploadBlockingFailure({
|
|
1821
|
+
stage: "chunk_upload",
|
|
1822
|
+
failureKind: "http_error",
|
|
1823
|
+
recordingId: this.opts.recordingId,
|
|
1824
|
+
chunkIndex: index,
|
|
1825
|
+
isFinal: extra.isFinal === true,
|
|
1826
|
+
httpStatus: res.status,
|
|
1827
|
+
statusText: res.statusText,
|
|
1828
|
+
chunkBytes: blob.size,
|
|
1829
|
+
uploadMode: this.opts.uploadMode,
|
|
1830
|
+
finalUploadRecoveryAttempted:
|
|
1831
|
+
extra.isFinal === true &&
|
|
1832
|
+
this.isFinalUploadRecoveryCandidate(res.status, err),
|
|
1833
|
+
});
|
|
1749
1834
|
// Capture rich context to Sentry BEFORE throwing — when this hits
|
|
1750
1835
|
// production we want enough breadcrumbs in the event to debug a
|
|
1751
1836
|
// "Builder.io upload failed (500)" without re-running the upload.
|
|
@@ -1799,6 +1884,25 @@ export class RecorderEngine {
|
|
|
1799
1884
|
}
|
|
1800
1885
|
}
|
|
1801
1886
|
|
|
1887
|
+
private isFinalUploadRecoveryCandidate(
|
|
1888
|
+
status: number,
|
|
1889
|
+
error: Error,
|
|
1890
|
+
): boolean {
|
|
1891
|
+
if (status === 413) return false;
|
|
1892
|
+
return !/too large|exceeds.*limit|chunk too large/i.test(error.message);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
private async recoverReadyAfterFinalUploadError(): Promise<Record<
|
|
1896
|
+
string,
|
|
1897
|
+
unknown
|
|
1898
|
+
> | null> {
|
|
1899
|
+
return waitForReadyRecordingAfterFinalizeError({
|
|
1900
|
+
uploadUrl: this.opts.uploadUrl,
|
|
1901
|
+
recordingId: this.opts.recordingId,
|
|
1902
|
+
preferAuthenticated: true,
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1802
1906
|
private readDimensions(): { width: number; height: number } {
|
|
1803
1907
|
const videoTrack =
|
|
1804
1908
|
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.11",
|
|
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";
|