@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
appApiPath,
|
|
2
3
|
PromptComposer,
|
|
3
|
-
useActionQuery,
|
|
4
4
|
useSendToAgentChat,
|
|
5
5
|
useT,
|
|
6
6
|
} from "@agent-native/core/client";
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
IconRoute,
|
|
19
19
|
IconTimelineEvent,
|
|
20
20
|
} from "@tabler/icons-react";
|
|
21
|
+
import { useQuery } from "@tanstack/react-query";
|
|
21
22
|
import {
|
|
22
23
|
type ReactNode,
|
|
23
24
|
useCallback,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
TooltipProvider,
|
|
43
44
|
TooltipTrigger,
|
|
44
45
|
} from "@/components/ui/tooltip";
|
|
46
|
+
import { getIdToken } from "@/lib/auth";
|
|
45
47
|
import { cn } from "@/lib/utils";
|
|
46
48
|
|
|
47
49
|
type SessionRecordingSummary = {
|
|
@@ -83,7 +85,20 @@ type ReplayChunkEvents = {
|
|
|
83
85
|
unavailable?: boolean;
|
|
84
86
|
};
|
|
85
87
|
|
|
86
|
-
type
|
|
88
|
+
type SessionReplayManifestResponse = {
|
|
89
|
+
recording: SessionRecordingSummary;
|
|
90
|
+
chunks: Array<{
|
|
91
|
+
seq: number;
|
|
92
|
+
checksum: string;
|
|
93
|
+
byteLength: number;
|
|
94
|
+
eventCount: number;
|
|
95
|
+
startedAt: string | null;
|
|
96
|
+
endedAt: string | null;
|
|
97
|
+
bytesPath: string;
|
|
98
|
+
}>;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type SessionReplayPlaybackResponse = {
|
|
87
102
|
recording: SessionRecordingSummary;
|
|
88
103
|
chunks: ReplayChunkEvents[];
|
|
89
104
|
eventCount: number;
|
|
@@ -122,6 +137,8 @@ const SPEED_OPTIONS = [0.5, 1, 2, 4, 8];
|
|
|
122
137
|
const SKIP_STEP_MS = 5000;
|
|
123
138
|
const MIN_IDLE_SKIP_MS = 8000;
|
|
124
139
|
const IDLE_EDGE_PAD_MS = 1200;
|
|
140
|
+
const REPLAY_CHUNK_FETCH_CONCURRENCY = 6;
|
|
141
|
+
const REPLAY_CHUNK_UNAVAILABLE_MESSAGE = "Session replay chunk is unavailable";
|
|
125
142
|
|
|
126
143
|
const RRWEB_EVENT_TYPE = {
|
|
127
144
|
FullSnapshot: 2,
|
|
@@ -155,12 +172,7 @@ export default function SessionDetailPage() {
|
|
|
155
172
|
const t = useT();
|
|
156
173
|
const { recordingId = "" } = useParams();
|
|
157
174
|
const { codeRequiredDialog } = useSendToAgentChat();
|
|
158
|
-
const { data, isLoading, error } =
|
|
159
|
-
useActionQuery<SessionReplayEventsResponse>(
|
|
160
|
-
"get-session-replay-events",
|
|
161
|
-
{ recordingId, limit: 10000 },
|
|
162
|
-
{ enabled: Boolean(recordingId), staleTime: 30_000 },
|
|
163
|
-
);
|
|
175
|
+
const { data, isLoading, error } = useSessionReplayPlayback(recordingId);
|
|
164
176
|
const recording = data?.recording;
|
|
165
177
|
|
|
166
178
|
return (
|
|
@@ -196,7 +208,9 @@ export default function SessionDetailPage() {
|
|
|
196
208
|
<Card>
|
|
197
209
|
<CardContent className="flex items-center gap-3 p-6 text-sm text-destructive">
|
|
198
210
|
<IconExclamationCircle className="h-5 w-5" />
|
|
199
|
-
{t("sessions.loadFailed", {
|
|
211
|
+
{t("sessions.loadFailed", {
|
|
212
|
+
message: error instanceof Error ? error.message : String(error),
|
|
213
|
+
})}
|
|
200
214
|
</CardContent>
|
|
201
215
|
</Card>
|
|
202
216
|
) : isLoading ? (
|
|
@@ -268,7 +282,7 @@ function AskSessionPopover({
|
|
|
268
282
|
function ReplayWorkbench({
|
|
269
283
|
response,
|
|
270
284
|
}: {
|
|
271
|
-
response:
|
|
285
|
+
response: SessionReplayPlaybackResponse;
|
|
272
286
|
}) {
|
|
273
287
|
const events = useReplayEvents(response);
|
|
274
288
|
const markers = useMemo(() => buildReplayMarkers(events), [events]);
|
|
@@ -318,7 +332,7 @@ function ReplayPlayer({
|
|
|
318
332
|
}: {
|
|
319
333
|
events: AnyReplayEvent[];
|
|
320
334
|
markers: ReplayMarker[];
|
|
321
|
-
response:
|
|
335
|
+
response: SessionReplayPlaybackResponse;
|
|
322
336
|
onTimeUpdate: (ms: number) => void;
|
|
323
337
|
registerSeek: (seek: (ms: number, autoplay?: boolean) => void) => void;
|
|
324
338
|
}) {
|
|
@@ -956,8 +970,143 @@ function DetailSkeleton() {
|
|
|
956
970
|
);
|
|
957
971
|
}
|
|
958
972
|
|
|
973
|
+
function useSessionReplayPlayback(recordingId: string) {
|
|
974
|
+
return useQuery({
|
|
975
|
+
queryKey: ["session-replay-playback", recordingId],
|
|
976
|
+
enabled: Boolean(recordingId),
|
|
977
|
+
staleTime: 30_000,
|
|
978
|
+
gcTime: 60_000,
|
|
979
|
+
queryFn: () => fetchSessionReplayPlayback(recordingId),
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
export async function fetchSessionReplayPlayback(
|
|
984
|
+
recordingId: string,
|
|
985
|
+
): Promise<SessionReplayPlaybackResponse> {
|
|
986
|
+
const manifest = await fetchReplayManifest(recordingId);
|
|
987
|
+
const chunks = await fetchReplayChunks(manifest.chunks);
|
|
988
|
+
const unavailableChunks = chunks.filter((chunk) => chunk.unavailable).length;
|
|
989
|
+
const eventCount = chunks.reduce(
|
|
990
|
+
(sum, chunk) => sum + chunk.events.length,
|
|
991
|
+
0,
|
|
992
|
+
);
|
|
993
|
+
return {
|
|
994
|
+
recording: manifest.recording,
|
|
995
|
+
chunks,
|
|
996
|
+
eventCount,
|
|
997
|
+
truncated: false,
|
|
998
|
+
unavailableChunks,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async function fetchReplayManifest(
|
|
1003
|
+
recordingId: string,
|
|
1004
|
+
): Promise<SessionReplayManifestResponse> {
|
|
1005
|
+
const response = await fetchReplayApi(
|
|
1006
|
+
`/api/session-replay/recordings/${encodeURIComponent(
|
|
1007
|
+
recordingId,
|
|
1008
|
+
)}/manifest`,
|
|
1009
|
+
);
|
|
1010
|
+
if (!response.ok) throw await replayFetchError(response);
|
|
1011
|
+
return (await response.json()) as SessionReplayManifestResponse;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
async function fetchReplayChunks(
|
|
1015
|
+
chunks: SessionReplayManifestResponse["chunks"],
|
|
1016
|
+
): Promise<ReplayChunkEvents[]> {
|
|
1017
|
+
const results = new Array<ReplayChunkEvents>(chunks.length);
|
|
1018
|
+
let nextIndex = 0;
|
|
1019
|
+
|
|
1020
|
+
async function worker() {
|
|
1021
|
+
while (nextIndex < chunks.length) {
|
|
1022
|
+
const index = nextIndex;
|
|
1023
|
+
nextIndex += 1;
|
|
1024
|
+
results[index] = await fetchReplayChunk(chunks[index]);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
await Promise.all(
|
|
1029
|
+
Array.from(
|
|
1030
|
+
{ length: Math.min(REPLAY_CHUNK_FETCH_CONCURRENCY, chunks.length) },
|
|
1031
|
+
worker,
|
|
1032
|
+
),
|
|
1033
|
+
);
|
|
1034
|
+
return results;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async function fetchReplayChunk(
|
|
1038
|
+
chunk: SessionReplayManifestResponse["chunks"][number],
|
|
1039
|
+
): Promise<ReplayChunkEvents> {
|
|
1040
|
+
const response = await fetchReplayApi(chunk.bytesPath);
|
|
1041
|
+
if (!response.ok) {
|
|
1042
|
+
const error = await replayFetchError(response);
|
|
1043
|
+
if (isUnavailableReplayChunk(response, error)) {
|
|
1044
|
+
return replayUnavailableChunk(chunk);
|
|
1045
|
+
}
|
|
1046
|
+
throw error;
|
|
1047
|
+
}
|
|
1048
|
+
const payload = await response.json();
|
|
1049
|
+
return {
|
|
1050
|
+
seq: chunk.seq,
|
|
1051
|
+
checksum: chunk.checksum,
|
|
1052
|
+
byteLength: chunk.byteLength,
|
|
1053
|
+
eventCount: chunk.eventCount,
|
|
1054
|
+
events: replayPayloadEvents(payload),
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function isUnavailableReplayChunk(response: Response, error: Error): boolean {
|
|
1059
|
+
return (
|
|
1060
|
+
response.status === 404 &&
|
|
1061
|
+
error.message === REPLAY_CHUNK_UNAVAILABLE_MESSAGE
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function replayUnavailableChunk(
|
|
1066
|
+
chunk: SessionReplayManifestResponse["chunks"][number],
|
|
1067
|
+
): ReplayChunkEvents {
|
|
1068
|
+
return {
|
|
1069
|
+
seq: chunk.seq,
|
|
1070
|
+
checksum: chunk.checksum,
|
|
1071
|
+
byteLength: chunk.byteLength,
|
|
1072
|
+
eventCount: chunk.eventCount,
|
|
1073
|
+
events: [],
|
|
1074
|
+
unavailable: true,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
async function fetchReplayApi(path: string): Promise<Response> {
|
|
1079
|
+
const token = await getIdToken();
|
|
1080
|
+
return fetch(appApiPath(path), {
|
|
1081
|
+
headers: {
|
|
1082
|
+
Accept: "application/json",
|
|
1083
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async function replayFetchError(response: Response): Promise<Error> {
|
|
1089
|
+
try {
|
|
1090
|
+
const payload = await response.json();
|
|
1091
|
+
if (isRecord(payload) && typeof payload.error === "string") {
|
|
1092
|
+
return new Error(payload.error);
|
|
1093
|
+
}
|
|
1094
|
+
} catch {
|
|
1095
|
+
// Fall through to the status text.
|
|
1096
|
+
}
|
|
1097
|
+
return new Error(response.statusText || `HTTP ${response.status}`);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export function replayPayloadEvents(payload: unknown): unknown[] {
|
|
1101
|
+
if (Array.isArray(payload)) return payload;
|
|
1102
|
+
if (isRecord(payload) && Array.isArray(payload.events)) {
|
|
1103
|
+
return payload.events;
|
|
1104
|
+
}
|
|
1105
|
+
return payload ? [payload] : [];
|
|
1106
|
+
}
|
|
1107
|
+
|
|
959
1108
|
function useReplayEvents(
|
|
960
|
-
response:
|
|
1109
|
+
response: SessionReplayPlaybackResponse,
|
|
961
1110
|
): AnyReplayEvent[] {
|
|
962
1111
|
return useMemo(
|
|
963
1112
|
() =>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
OpenSourceBadge,
|
|
3
3
|
PoweredByBadge,
|
|
4
|
+
LanguagePicker,
|
|
4
5
|
StarfieldBackground,
|
|
5
6
|
useT,
|
|
6
7
|
} from "@agent-native/core/client";
|
|
@@ -53,13 +54,23 @@ function BookingPageShell({
|
|
|
53
54
|
return (
|
|
54
55
|
<div
|
|
55
56
|
className={cn(
|
|
56
|
-
"relative min-h-screen
|
|
57
|
+
"relative min-h-screen bg-background dark:bg-black",
|
|
57
58
|
className,
|
|
58
59
|
)}
|
|
59
60
|
>
|
|
60
61
|
<StarfieldBackground className="fixed inset-0 opacity-25 dark:opacity-60" />
|
|
61
|
-
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(ellipse_at_center,hsl(var(--background)/0.35)_0%,hsl(var(--background)/0.88)_72%)]" />
|
|
62
|
-
<div className="
|
|
62
|
+
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(ellipse_at_center,hsl(var(--background)/0.35)_0%,hsl(var(--background)/0.88)_72%)] dark:bg-[radial-gradient(ellipse_at_center,hsl(var(--background)/0.35)_0%,#000000_100%)]" />
|
|
63
|
+
<div className="fixed top-4 right-4 z-50 flex items-center gap-1">
|
|
64
|
+
<LanguagePicker variant="ghost-icon" />
|
|
65
|
+
<ThemeToggle />
|
|
66
|
+
</div>
|
|
67
|
+
<div className="fixed bottom-[21px] left-4 z-50 flex flex-col items-start gap-2 max-sm:static max-sm:mx-auto max-sm:mt-8">
|
|
68
|
+
<PoweredByBadge variant="plain" embedded />
|
|
69
|
+
<OpenSourceBadge embedded />
|
|
70
|
+
</div>
|
|
71
|
+
<div className="relative z-10 min-h-screen overflow-x-hidden p-4">
|
|
72
|
+
{children}
|
|
73
|
+
</div>
|
|
63
74
|
</div>
|
|
64
75
|
);
|
|
65
76
|
}
|
|
@@ -281,9 +292,6 @@ export default function BookingPage() {
|
|
|
281
292
|
|
|
282
293
|
return (
|
|
283
294
|
<BookingPageShell className="pb-20">
|
|
284
|
-
<div className="absolute top-4 right-4 z-20">
|
|
285
|
-
<ThemeToggle />
|
|
286
|
-
</div>
|
|
287
295
|
<div className="mx-auto mt-[7.5vh] w-full max-w-lg">
|
|
288
296
|
{/* Header */}
|
|
289
297
|
<div className="mb-8 text-center">
|
|
@@ -509,8 +517,6 @@ export default function BookingPage() {
|
|
|
509
517
|
)}
|
|
510
518
|
</div>
|
|
511
519
|
</div>
|
|
512
|
-
<OpenSourceBadge />
|
|
513
|
-
<PoweredByBadge />
|
|
514
520
|
</BookingPageShell>
|
|
515
521
|
);
|
|
516
522
|
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
stringifySpaceIds,
|
|
24
24
|
} from "../server/lib/recordings.js";
|
|
25
25
|
import { setResumableSession } from "../server/lib/resumable-session.js";
|
|
26
|
+
import { isStreamingUploadDisabled } from "../server/lib/streaming-upload-mode.js";
|
|
26
27
|
import { createRecordingSchema } from "./lib/create-recording-schema.js";
|
|
27
28
|
import { DEFAULT_RECORDING_TITLE } from "./lib/title-source.js";
|
|
28
29
|
|
|
@@ -86,7 +87,11 @@ export default defineAction({
|
|
|
86
87
|
// init fails.
|
|
87
88
|
let uploadMode: UploadMode = "buffered";
|
|
88
89
|
const uploadProvider = await getActiveFileUploadProviderForRequest();
|
|
89
|
-
if (
|
|
90
|
+
if (
|
|
91
|
+
args.requestStreaming &&
|
|
92
|
+
!isStreamingUploadDisabled() &&
|
|
93
|
+
uploadProvider?.resumable
|
|
94
|
+
) {
|
|
90
95
|
try {
|
|
91
96
|
const recordingMimeType =
|
|
92
97
|
args.mimeType?.split(";")[0]?.trim() || "video/webm";
|
|
@@ -103,7 +108,7 @@ export default defineAction({
|
|
|
103
108
|
await setResumableSession(id, {
|
|
104
109
|
providerId: uploadProvider.id,
|
|
105
110
|
sessionId: session.sessionId,
|
|
106
|
-
meta: session.meta,
|
|
111
|
+
meta: { ...session.meta, skipCompressionWait: true },
|
|
107
112
|
bytesUploaded: 0,
|
|
108
113
|
lastCommittedIndex: -1,
|
|
109
114
|
});
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
deleteResumableSession,
|
|
40
40
|
getResumableSession,
|
|
41
41
|
} from "../server/lib/resumable-session.js";
|
|
42
|
+
import { isStreamingUploadDisabled } from "../server/lib/streaming-upload-mode.js";
|
|
42
43
|
import {
|
|
43
44
|
requiresConfiguredVideoStorage,
|
|
44
45
|
STORAGE_SETUP_REQUIRED_REASON,
|
|
@@ -341,6 +342,11 @@ export default defineAction({
|
|
|
341
342
|
// Resumable path: create-recording initialized a session and chunk.post.ts
|
|
342
343
|
// forwarded all chunks to the provider. Complete the session to get the CDN URL.
|
|
343
344
|
const resumableSession = await getResumableSession(id);
|
|
345
|
+
if (resumableSession && isStreamingUploadDisabled()) {
|
|
346
|
+
console.warn(
|
|
347
|
+
`[finalize] streaming uploads are disabled, but completing existing resumable session for in-flight recording: ${id}`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
344
350
|
if (resumableSession) {
|
|
345
351
|
debugLog("[finalize] resumable session found, completing upload", {
|
|
346
352
|
id,
|
|
@@ -351,6 +357,9 @@ export default defineAction({
|
|
|
351
357
|
if (!uploadProvider?.resumable) {
|
|
352
358
|
throw new Error("No resumable upload provider configured");
|
|
353
359
|
}
|
|
360
|
+
if (resumableSession.bytesUploaded <= 0) {
|
|
361
|
+
throw new Error("Recording upload contained no video bytes");
|
|
362
|
+
}
|
|
354
363
|
const videoUrl = await uploadProvider.resumable.completeSession(
|
|
355
364
|
{
|
|
356
365
|
sessionId: resumableSession.sessionId,
|
|
@@ -359,6 +368,7 @@ export default defineAction({
|
|
|
359
368
|
typeof resumableSession.meta.filename === "string"
|
|
360
369
|
? resumableSession.meta.filename
|
|
361
370
|
: "",
|
|
371
|
+
{ skipCompressionWait: true },
|
|
362
372
|
);
|
|
363
373
|
debugLog("[finalize] resumable upload completed", { id, videoUrl });
|
|
364
374
|
const result = await markRecordingReady({
|
|
@@ -560,6 +570,7 @@ export default defineAction({
|
|
|
560
570
|
filename: `${id}.${videoFormat}`,
|
|
561
571
|
mimeType,
|
|
562
572
|
ownerEmail,
|
|
573
|
+
skipCompressionWait: true,
|
|
563
574
|
});
|
|
564
575
|
} catch (err) {
|
|
565
576
|
// Capture structured context so a "Builder.io upload failed (500)" can
|
|
@@ -233,10 +233,12 @@ export default defineAction({
|
|
|
233
233
|
// `?password=<pw>` (legacy fallback) so old share pages keep
|
|
234
234
|
// working during rollout. (audit 11 F-07)
|
|
235
235
|
// Owners are skipped — the blob route bypasses the password gate
|
|
236
|
-
// for them, so they don't need the token.
|
|
237
|
-
//
|
|
236
|
+
// for them, so they don't need the token. Remote provider URLs are
|
|
237
|
+
// still proxied through same-origin media serving so CORS, range
|
|
238
|
+
// requests, and signed URL quirks match public share playback.
|
|
238
239
|
const resolvedVideoUrl = resolvePlayerVideoUrl(rec, {
|
|
239
240
|
addPasswordToken: access.role !== "owner",
|
|
241
|
+
proxyRemoteMedia: true,
|
|
240
242
|
});
|
|
241
243
|
|
|
242
244
|
return {
|
|
@@ -17,6 +17,18 @@ export type LoomVideoDownload = {
|
|
|
17
17
|
sourceUrl: string;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
const LOOM_VIDEO_UNAVAILABLE_MESSAGE =
|
|
21
|
+
"Loom did not provide a downloadable MP4 for this video. Download the original from Loom and use Upload video in Clips.";
|
|
22
|
+
|
|
23
|
+
export class LoomVideoUnavailableError extends Error {
|
|
24
|
+
statusCode = 422;
|
|
25
|
+
|
|
26
|
+
constructor(message = LOOM_VIDEO_UNAVAILABLE_MESSAGE) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "LoomVideoUnavailableError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
function formatBytes(bytes: number): string {
|
|
21
33
|
if (bytes >= 1024 * 1024) {
|
|
22
34
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -129,6 +141,10 @@ async function fetchTranscodedVideoUrl({
|
|
|
129
141
|
{ maxRedirects: 2 },
|
|
130
142
|
);
|
|
131
143
|
|
|
144
|
+
if (response.status === 204) {
|
|
145
|
+
throw new LoomVideoUnavailableError();
|
|
146
|
+
}
|
|
147
|
+
|
|
132
148
|
if (!response.ok) {
|
|
133
149
|
throw new Error(
|
|
134
150
|
`Loom did not provide a downloadable MP4 (${response.status} ${response.statusText}). Make sure the link is public and allows playback.`,
|
|
@@ -153,11 +153,11 @@ export function Scrubber(props: ScrubberProps) {
|
|
|
153
153
|
<div
|
|
154
154
|
ref={barRef}
|
|
155
155
|
data-player-scrubber-bar
|
|
156
|
-
className="relative w-full h-1.5 bg-white/
|
|
156
|
+
className="relative w-full h-1.5 bg-white/35 rounded-full cursor-pointer group/bar shadow-[0_0_0_1px_rgba(0,0,0,0.16)]"
|
|
157
157
|
>
|
|
158
158
|
{/* Filled portion */}
|
|
159
159
|
<div
|
|
160
|
-
className="absolute inset-y-0 left-0 bg-
|
|
160
|
+
className="absolute inset-y-0 left-0 bg-white rounded-full shadow-[0_0_10px_rgba(255,255,255,0.45)]"
|
|
161
161
|
style={{ width: pct + "%" }}
|
|
162
162
|
/>
|
|
163
163
|
|
|
@@ -95,10 +95,11 @@ export function CountdownOverlay({
|
|
|
95
95
|
|
|
96
96
|
<div
|
|
97
97
|
key={remaining}
|
|
98
|
-
className="flex
|
|
98
|
+
className="flex min-w-32 items-center justify-center text-[clamp(96px,22vmin,240px)] font-extrabold leading-none text-white duration-200 animate-in zoom-in-75 fade-in"
|
|
99
99
|
style={{
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
fontVariantNumeric: "tabular-nums",
|
|
101
|
+
textShadow:
|
|
102
|
+
"1px 0 1px rgba(17,24,39,0.22), -1px 0 1px rgba(17,24,39,0.22), 0 1px 1px rgba(17,24,39,0.22), 0 -1px 1px rgba(17,24,39,0.22), 0 2px 4px rgba(0,0,0,0.48), 0 12px 34px rgba(0,0,0,0.36), 0 0 2px rgba(0,0,0,0.72)",
|
|
102
103
|
}}
|
|
103
104
|
>
|
|
104
105
|
{remaining > 0 ? remaining : "Go"}
|
|
@@ -58,6 +58,8 @@ import {
|
|
|
58
58
|
import {
|
|
59
59
|
NO_CAMERA_DEVICE_ID,
|
|
60
60
|
NO_MIC_DEVICE_ID,
|
|
61
|
+
normalizeDisplaySurfaceForRuntime,
|
|
62
|
+
supportsBrowserTabCapture,
|
|
61
63
|
type DisplaySurface,
|
|
62
64
|
type RecordingMode,
|
|
63
65
|
} from "./recorder-engine";
|
|
@@ -157,14 +159,20 @@ export function PreRecordPanel({
|
|
|
157
159
|
const t = useT();
|
|
158
160
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
159
161
|
const loomInputRef = useRef<HTMLInputElement>(null);
|
|
162
|
+
const browserTabCaptureSupported = useMemo(
|
|
163
|
+
() => supportsBrowserTabCapture(),
|
|
164
|
+
[],
|
|
165
|
+
);
|
|
160
166
|
// Saved selections from the last visit. A `?mode=`/`?surface=` deep link
|
|
161
167
|
// (initialMode/initialDisplaySurface) still takes precedence over them.
|
|
162
168
|
const savedPrefs = useMemo(() => loadRecorderPreferences(), []);
|
|
163
169
|
const [mode, setMode] = useState<RecordingMode>(
|
|
164
170
|
() => initialMode ?? savedPrefs.mode ?? "screen+camera",
|
|
165
171
|
);
|
|
166
|
-
const [displaySurface, setDisplaySurface] = useState<DisplaySurface>(
|
|
167
|
-
(
|
|
172
|
+
const [displaySurface, setDisplaySurface] = useState<DisplaySurface>(() =>
|
|
173
|
+
normalizeDisplaySurfaceForRuntime(
|
|
174
|
+
initialDisplaySurface ?? savedPrefs.displaySurface ?? "window",
|
|
175
|
+
),
|
|
168
176
|
);
|
|
169
177
|
const [sourceOpen, setSourceOpen] = useState(false);
|
|
170
178
|
const [deviceSettingsOpen, setDeviceSettingsOpen] = useState(false);
|
|
@@ -223,8 +231,8 @@ export function PreRecordPanel({
|
|
|
223
231
|
[isMobile, modeOptions],
|
|
224
232
|
);
|
|
225
233
|
|
|
226
|
-
const surfaceOptions = useMemo<SurfaceOption[]>(
|
|
227
|
-
|
|
234
|
+
const surfaceOptions = useMemo<SurfaceOption[]>(() => {
|
|
235
|
+
const options: SurfaceOption[] = [
|
|
228
236
|
{
|
|
229
237
|
value: "window",
|
|
230
238
|
label: t("preRecord.surfaceWindow"),
|
|
@@ -243,9 +251,11 @@ export function PreRecordPanel({
|
|
|
243
251
|
icon: IconDeviceScreen,
|
|
244
252
|
sub: t("preRecord.surfaceScreenDescription"),
|
|
245
253
|
},
|
|
246
|
-
]
|
|
247
|
-
|
|
248
|
-
|
|
254
|
+
];
|
|
255
|
+
return browserTabCaptureSupported
|
|
256
|
+
? options
|
|
257
|
+
: options.filter((option) => option.value !== "browser");
|
|
258
|
+
}, [browserTabCaptureSupported, t]);
|
|
249
259
|
|
|
250
260
|
useEffect(() => {
|
|
251
261
|
if (isMobile) {
|
|
@@ -258,9 +268,19 @@ export function PreRecordPanel({
|
|
|
258
268
|
}, [initialMode, isMobile]);
|
|
259
269
|
|
|
260
270
|
useEffect(() => {
|
|
261
|
-
if (initialDisplaySurface)
|
|
271
|
+
if (initialDisplaySurface) {
|
|
272
|
+
setDisplaySurface(
|
|
273
|
+
normalizeDisplaySurfaceForRuntime(initialDisplaySurface),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
262
276
|
}, [initialDisplaySurface]);
|
|
263
277
|
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (displaySurface !== "browser" || browserTabCaptureSupported) return;
|
|
280
|
+
setDisplaySurface("window");
|
|
281
|
+
saveRecorderPreferences({ displaySurface: "window" });
|
|
282
|
+
}, [browserTabCaptureSupported, displaySurface]);
|
|
283
|
+
|
|
264
284
|
const enumerateDevices = useCallback(async () => {
|
|
265
285
|
try {
|
|
266
286
|
if (!navigator.mediaDevices?.enumerateDevices) {
|
|
@@ -350,8 +370,9 @@ export function PreRecordPanel({
|
|
|
350
370
|
saveRecorderPreferences({ mode: value });
|
|
351
371
|
}, []);
|
|
352
372
|
const chooseDisplaySurface = useCallback((value: DisplaySurface) => {
|
|
353
|
-
|
|
354
|
-
|
|
373
|
+
const next = normalizeDisplaySurfaceForRuntime(value);
|
|
374
|
+
setDisplaySurface(next);
|
|
375
|
+
saveRecorderPreferences({ displaySurface: next });
|
|
355
376
|
}, []);
|
|
356
377
|
const chooseMic = useCallback((value: string) => {
|
|
357
378
|
setMicId(value);
|
|
@@ -676,7 +697,12 @@ export function PreRecordPanel({
|
|
|
676
697
|
</button>
|
|
677
698
|
</CollapsibleTrigger>
|
|
678
699
|
<CollapsibleContent>
|
|
679
|
-
<div
|
|
700
|
+
<div
|
|
701
|
+
className={cn(
|
|
702
|
+
"grid gap-2 px-6 pb-5",
|
|
703
|
+
surfaceOptions.length === 2 ? "grid-cols-2" : "grid-cols-3",
|
|
704
|
+
)}
|
|
705
|
+
>
|
|
680
706
|
{surfaceOptions.map((opt) => {
|
|
681
707
|
const Icon = opt.icon;
|
|
682
708
|
const active = opt.value === displaySurface;
|
|
@@ -913,7 +939,8 @@ export function PreRecordPanel({
|
|
|
913
939
|
// to acquire a webcam stream.
|
|
914
940
|
mode:
|
|
915
941
|
mode === "screen+camera" && !needsCamera ? "screen" : mode,
|
|
916
|
-
displaySurface
|
|
942
|
+
displaySurface:
|
|
943
|
+
normalizeDisplaySurfaceForRuntime(displaySurface),
|
|
917
944
|
micDeviceId: micId === "default" ? null : micId,
|
|
918
945
|
cameraDeviceId:
|
|
919
946
|
needsCamera && cameraId !== "default" ? cameraId : null,
|