@agent-native/core 0.173.0 → 0.173.1-nightly-20260825133915
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/templates/analytics/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/assets/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/brain/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/calendar/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/chat/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/clips/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +36 -2
- package/corpus/templates/clips/app/components/player/signed-out-share-actions.tsx +13 -3
- package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
- package/corpus/templates/clips/app/lib/time-param.ts +40 -0
- package/corpus/templates/clips/app/routes/embed.$shareId.tsx +2 -35
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +58 -63
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +16 -9
- package/corpus/templates/clips/changelog/2026-08-24-chrome-recording-recovers-a-dropped-mic-grant.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/background.ts +138 -29
- package/corpus/templates/clips/chrome-extension/src/media-permission.ts +163 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +43 -11
- package/corpus/templates/clips/chrome-extension/src/options.html +8 -6
- package/corpus/templates/clips/chrome-extension/src/permission.ts +4 -17
- package/corpus/templates/clips/chrome-extension/src/popup.ts +27 -39
- package/corpus/templates/clips/chrome-extension/src/styles.css +23 -0
- package/corpus/templates/clips/shared/share-attribution.ts +4 -2
- package/corpus/templates/clips/shared/share-dashboard-redirect.ts +1 -1
- package/corpus/templates/content/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +8 -0
- package/corpus/templates/content/changelog/2026-08-24-database-columns-keep-their-chosen-order-across-reloads-visi.md +6 -0
- package/corpus/templates/crm/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/design/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +166 -109
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +82 -52
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +17 -12
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +242 -137
- package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +6 -1
- package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +11 -6
- package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +7 -2
- package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +4 -1
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +27 -4
- package/corpus/templates/design/app/hooks/use-shortcut-label.ts +21 -0
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +38 -16
- package/corpus/templates/design/app/pages/DesignEditor.tsx +17 -10
- package/corpus/templates/design/app/pages/design-editor/auto-layout-suggestion.ts +5 -3
- package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/paste-to-replace.ts +28 -1
- package/corpus/templates/design/app/pages/design-editor/layout-operations.ts +33 -4
- package/corpus/templates/design/changelog/2026-08-24-adding-auto-layout-to-a-stack-of-full-width-rows-now-picks.md +6 -0
- package/corpus/templates/design/changelog/2026-08-24-dragging-an-element-no-longer-fades-it-or-changes-its-stacki.md +6 -0
- package/corpus/templates/design/changelog/2026-08-24-dropping-an-element-into-a-frame-that-already-has-content.md +6 -0
- package/corpus/templates/design/shared/code-layer.ts +35 -4
- package/corpus/templates/dispatch/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/factory/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/forms/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/mail/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/plan/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/slides/.agents/skills/actions/SKILL.md +1 -0
- package/corpus/templates/tasks/.agents/skills/actions/SKILL.md +1 -0
- package/dist/agent/engine/anthropic-engine.js +13 -3
- package/dist/agent/engine/types.d.ts +15 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/posthog-ai.d.ts +25 -0
- package/dist/observability/posthog-ai.js +122 -0
- package/dist/observability/traces.js +30 -5
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/workspace-provider-oauth.d.ts +1 -0
- package/dist/server/workspace-provider-oauth.js +4 -1
- package/dist/templates/chat/.agents/skills/actions/SKILL.md +1 -0
- package/dist/templates/default/.agents/skills/actions/SKILL.md +1 -0
- package/dist/templates/headless/.agents/skills/actions/SKILL.md +1 -0
- package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +1 -0
- package/dist/usage/store.d.ts +14 -2
- package/dist/usage/store.js +38 -10
- package/package.json +2 -2
- package/src/templates/chat/.agents/skills/actions/SKILL.md +1 -0
- package/src/templates/default/.agents/skills/actions/SKILL.md +1 -0
- package/src/templates/headless/.agents/skills/actions/SKILL.md +1 -0
- package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +1 -0
package/corpus/README.md
CHANGED
|
@@ -121,6 +121,7 @@ Everything else — CRUD, settings, search, list/detail reads, auth state, anyth
|
|
|
121
121
|
|
|
122
122
|
- **Do** keep one action, one job; document a reusable action (when to use it, key args, return fields to preserve) in `AGENTS.md` once it's called from outside one narrow screen; promote workflow-heavy actions (provider-backed, cross-app, MCP/A2A, multi-step) into a skill.
|
|
123
123
|
- **Do** use `fail()` for user-friendly errors and import primitives from `@agent-native/core`(`/action`) instead of redefining them; use the core `upload-image` action or `uploadFile()` for durable images/files — never base64 into SQL, markdown, or action results.
|
|
124
|
+
- **Do** signal failure by throwing (`fail()`), never by returning `{ error: ... }`. A returned envelope is a successful return everywhere the framework looks: the retry breakers (`MAX_IDENTICAL_TOOL_ERRORS`, `MAX_SAME_ERROR_ACROSS_ARGUMENTS`) never count it, the call is recorded `completedSideEffect: true` even though nothing was written, and `failed_tools` / `$ai_is_error` stay clean while the model keeps guessing. One production run spent 32% of its cost on three rejected writes that every dashboard reported as successes.
|
|
124
125
|
- **Don't** re-export actions as REST — `/_agent-native/actions/:name` is already the REST surface; duplicating it under `/api/*` hides the operation from agents.
|
|
125
126
|
- **Don't** reach for provider integrations, `outputSchema`, `authorize`, `needsApproval`, or `_agentImages` without checking `references/` first — each has sharp edges covered there, not here.
|
|
126
127
|
|
|
@@ -121,6 +121,7 @@ Everything else — CRUD, settings, search, list/detail reads, auth state, anyth
|
|
|
121
121
|
|
|
122
122
|
- **Do** keep one action, one job; document a reusable action (when to use it, key args, return fields to preserve) in `AGENTS.md` once it's called from outside one narrow screen; promote workflow-heavy actions (provider-backed, cross-app, MCP/A2A, multi-step) into a skill.
|
|
123
123
|
- **Do** use `fail()` for user-friendly errors and import primitives from `@agent-native/core`(`/action`) instead of redefining them; use the core `upload-image` action or `uploadFile()` for durable images/files — never base64 into SQL, markdown, or action results.
|
|
124
|
+
- **Do** signal failure by throwing (`fail()`), never by returning `{ error: ... }`. A returned envelope is a successful return everywhere the framework looks: the retry breakers (`MAX_IDENTICAL_TOOL_ERRORS`, `MAX_SAME_ERROR_ACROSS_ARGUMENTS`) never count it, the call is recorded `completedSideEffect: true` even though nothing was written, and `failed_tools` / `$ai_is_error` stay clean while the model keeps guessing. One production run spent 32% of its cost on three rejected writes that every dashboard reported as successes.
|
|
124
125
|
- **Don't** re-export actions as REST — `/_agent-native/actions/:name` is already the REST surface; duplicating it under `/api/*` hides the operation from agents.
|
|
125
126
|
- **Don't** reach for provider integrations, `outputSchema`, `authorize`, `needsApproval`, or `_agentImages` without checking `references/` first — each has sharp edges covered there, not here.
|
|
126
127
|
|
|
@@ -121,6 +121,7 @@ Everything else — CRUD, settings, search, list/detail reads, auth state, anyth
|
|
|
121
121
|
|
|
122
122
|
- **Do** keep one action, one job; document a reusable action (when to use it, key args, return fields to preserve) in `AGENTS.md` once it's called from outside one narrow screen; promote workflow-heavy actions (provider-backed, cross-app, MCP/A2A, multi-step) into a skill.
|
|
123
123
|
- **Do** use `fail()` for user-friendly errors and import primitives from `@agent-native/core`(`/action`) instead of redefining them; use the core `upload-image` action or `uploadFile()` for durable images/files — never base64 into SQL, markdown, or action results.
|
|
124
|
+
- **Do** signal failure by throwing (`fail()`), never by returning `{ error: ... }`. A returned envelope is a successful return everywhere the framework looks: the retry breakers (`MAX_IDENTICAL_TOOL_ERRORS`, `MAX_SAME_ERROR_ACROSS_ARGUMENTS`) never count it, the call is recorded `completedSideEffect: true` even though nothing was written, and `failed_tools` / `$ai_is_error` stay clean while the model keeps guessing. One production run spent 32% of its cost on three rejected writes that every dashboard reported as successes.
|
|
124
125
|
- **Don't** re-export actions as REST — `/_agent-native/actions/:name` is already the REST surface; duplicating it under `/api/*` hides the operation from agents.
|
|
125
126
|
- **Don't** reach for provider integrations, `outputSchema`, `authorize`, `needsApproval`, or `_agentImages` without checking `references/` first — each has sharp edges covered there, not here.
|
|
126
127
|
|
|
@@ -121,6 +121,7 @@ Everything else — CRUD, settings, search, list/detail reads, auth state, anyth
|
|
|
121
121
|
|
|
122
122
|
- **Do** keep one action, one job; document a reusable action (when to use it, key args, return fields to preserve) in `AGENTS.md` once it's called from outside one narrow screen; promote workflow-heavy actions (provider-backed, cross-app, MCP/A2A, multi-step) into a skill.
|
|
123
123
|
- **Do** use `fail()` for user-friendly errors and import primitives from `@agent-native/core`(`/action`) instead of redefining them; use the core `upload-image` action or `uploadFile()` for durable images/files — never base64 into SQL, markdown, or action results.
|
|
124
|
+
- **Do** signal failure by throwing (`fail()`), never by returning `{ error: ... }`. A returned envelope is a successful return everywhere the framework looks: the retry breakers (`MAX_IDENTICAL_TOOL_ERRORS`, `MAX_SAME_ERROR_ACROSS_ARGUMENTS`) never count it, the call is recorded `completedSideEffect: true` even though nothing was written, and `failed_tools` / `$ai_is_error` stay clean while the model keeps guessing. One production run spent 32% of its cost on three rejected writes that every dashboard reported as successes.
|
|
124
125
|
- **Don't** re-export actions as REST — `/_agent-native/actions/:name` is already the REST surface; duplicating it under `/api/*` hides the operation from agents.
|
|
125
126
|
- **Don't** reach for provider integrations, `outputSchema`, `authorize`, `needsApproval`, or `_agentImages` without checking `references/` first — each has sharp edges covered there, not here.
|
|
126
127
|
|
|
@@ -121,6 +121,7 @@ Everything else — CRUD, settings, search, list/detail reads, auth state, anyth
|
|
|
121
121
|
|
|
122
122
|
- **Do** keep one action, one job; document a reusable action (when to use it, key args, return fields to preserve) in `AGENTS.md` once it's called from outside one narrow screen; promote workflow-heavy actions (provider-backed, cross-app, MCP/A2A, multi-step) into a skill.
|
|
123
123
|
- **Do** use `fail()` for user-friendly errors and import primitives from `@agent-native/core`(`/action`) instead of redefining them; use the core `upload-image` action or `uploadFile()` for durable images/files — never base64 into SQL, markdown, or action results.
|
|
124
|
+
- **Do** signal failure by throwing (`fail()`), never by returning `{ error: ... }`. A returned envelope is a successful return everywhere the framework looks: the retry breakers (`MAX_IDENTICAL_TOOL_ERRORS`, `MAX_SAME_ERROR_ACROSS_ARGUMENTS`) never count it, the call is recorded `completedSideEffect: true` even though nothing was written, and `failed_tools` / `$ai_is_error` stay clean while the model keeps guessing. One production run spent 32% of its cost on three rejected writes that every dashboard reported as successes.
|
|
124
125
|
- **Don't** re-export actions as REST — `/_agent-native/actions/:name` is already the REST surface; duplicating it under `/api/*` hides the operation from agents.
|
|
125
126
|
- **Don't** reach for provider integrations, `outputSchema`, `authorize`, `needsApproval`, or `_agentImages` without checking `references/` first — each has sharp edges covered there, not here.
|
|
126
127
|
|
|
@@ -121,6 +121,7 @@ Everything else — CRUD, settings, search, list/detail reads, auth state, anyth
|
|
|
121
121
|
|
|
122
122
|
- **Do** keep one action, one job; document a reusable action (when to use it, key args, return fields to preserve) in `AGENTS.md` once it's called from outside one narrow screen; promote workflow-heavy actions (provider-backed, cross-app, MCP/A2A, multi-step) into a skill.
|
|
123
123
|
- **Do** use `fail()` for user-friendly errors and import primitives from `@agent-native/core`(`/action`) instead of redefining them; use the core `upload-image` action or `uploadFile()` for durable images/files — never base64 into SQL, markdown, or action results.
|
|
124
|
+
- **Do** signal failure by throwing (`fail()`), never by returning `{ error: ... }`. A returned envelope is a successful return everywhere the framework looks: the retry breakers (`MAX_IDENTICAL_TOOL_ERRORS`, `MAX_SAME_ERROR_ACROSS_ARGUMENTS`) never count it, the call is recorded `completedSideEffect: true` even though nothing was written, and `failed_tools` / `$ai_is_error` stay clean while the model keeps guessing. One production run spent 32% of its cost on three rejected writes that every dashboard reported as successes.
|
|
124
125
|
- **Don't** re-export actions as REST — `/_agent-native/actions/:name` is already the REST surface; duplicating it under `/api/*` hides the operation from agents.
|
|
125
126
|
- **Don't** reach for provider integrations, `outputSchema`, `authorize`, `needsApproval`, or `_agentImages` without checking `references/` first — each has sharp edges covered there, not here.
|
|
126
127
|
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
} from "@/components/ui/popover";
|
|
41
41
|
import { Switch } from "@/components/ui/switch";
|
|
42
42
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
|
43
|
+
import { formatMs } from "@/lib/timestamp-mapping";
|
|
43
44
|
import { cn } from "@/lib/utils";
|
|
44
45
|
|
|
45
46
|
import { buildAgentApiUrls } from "../../../shared/agent-context";
|
|
@@ -70,6 +71,7 @@ export interface ShareRecordingPopoverProps {
|
|
|
70
71
|
thumbnailUrl?: string | null;
|
|
71
72
|
animatedThumbnailUrl?: string | null;
|
|
72
73
|
isLoomRecording?: boolean;
|
|
74
|
+
currentMs?: number;
|
|
73
75
|
hasPassword?: boolean;
|
|
74
76
|
/**
|
|
75
77
|
* Restricts the dialog to a bare copy-link control for viewers who can
|
|
@@ -108,6 +110,7 @@ export function ShareRecordingPopover({
|
|
|
108
110
|
thumbnailUrl,
|
|
109
111
|
animatedThumbnailUrl,
|
|
110
112
|
isLoomRecording = false,
|
|
113
|
+
currentMs,
|
|
111
114
|
hasPassword,
|
|
112
115
|
viewerReshareOnly = false,
|
|
113
116
|
children,
|
|
@@ -131,6 +134,7 @@ export function ShareRecordingPopover({
|
|
|
131
134
|
thumbnailUrl={thumbnailUrl}
|
|
132
135
|
animatedThumbnailUrl={animatedThumbnailUrl}
|
|
133
136
|
isLoomRecording={isLoomRecording}
|
|
137
|
+
currentMs={currentMs}
|
|
134
138
|
hasPassword={hasPassword}
|
|
135
139
|
viewerReshareOnly={viewerReshareOnly}
|
|
136
140
|
/>
|
|
@@ -153,6 +157,7 @@ export function ShareRecordingDialog({
|
|
|
153
157
|
thumbnailUrl,
|
|
154
158
|
animatedThumbnailUrl,
|
|
155
159
|
isLoomRecording = false,
|
|
160
|
+
currentMs,
|
|
156
161
|
open,
|
|
157
162
|
onOpenChange,
|
|
158
163
|
hasPassword,
|
|
@@ -176,6 +181,7 @@ export function ShareRecordingDialog({
|
|
|
176
181
|
thumbnailUrl={thumbnailUrl}
|
|
177
182
|
animatedThumbnailUrl={animatedThumbnailUrl}
|
|
178
183
|
isLoomRecording={isLoomRecording}
|
|
184
|
+
currentMs={currentMs}
|
|
179
185
|
hasPassword={hasPassword}
|
|
180
186
|
viewerReshareOnly={viewerReshareOnly}
|
|
181
187
|
reserveCloseButton
|
|
@@ -194,6 +200,7 @@ function ShareRecordingContent({
|
|
|
194
200
|
thumbnailUrl,
|
|
195
201
|
animatedThumbnailUrl,
|
|
196
202
|
isLoomRecording = false,
|
|
203
|
+
currentMs,
|
|
197
204
|
reserveCloseButton = false,
|
|
198
205
|
hasPassword,
|
|
199
206
|
viewerReshareOnly = false,
|
|
@@ -206,6 +213,7 @@ function ShareRecordingContent({
|
|
|
206
213
|
thumbnailUrl?: string | null;
|
|
207
214
|
animatedThumbnailUrl?: string | null;
|
|
208
215
|
isLoomRecording?: boolean;
|
|
216
|
+
currentMs?: number;
|
|
209
217
|
reserveCloseButton?: boolean;
|
|
210
218
|
hasPassword?: boolean;
|
|
211
219
|
viewerReshareOnly?: boolean;
|
|
@@ -292,6 +300,7 @@ function ShareRecordingContent({
|
|
|
292
300
|
thumbnailUrl={thumbnailUrl}
|
|
293
301
|
animatedThumbnailUrl={animatedThumbnailUrl}
|
|
294
302
|
isLoomRecording={isLoomRecording}
|
|
303
|
+
currentMs={currentMs}
|
|
295
304
|
hasPassword={hasPassword}
|
|
296
305
|
canViewShares={canViewShares}
|
|
297
306
|
/>
|
|
@@ -328,6 +337,7 @@ function LinkTab({
|
|
|
328
337
|
thumbnailUrl,
|
|
329
338
|
animatedThumbnailUrl,
|
|
330
339
|
isLoomRecording: isLoomRecordingProp,
|
|
340
|
+
currentMs = 0,
|
|
331
341
|
hasPassword,
|
|
332
342
|
canViewShares,
|
|
333
343
|
}: {
|
|
@@ -341,6 +351,7 @@ function LinkTab({
|
|
|
341
351
|
thumbnailUrl?: string | null;
|
|
342
352
|
animatedThumbnailUrl?: string | null;
|
|
343
353
|
isLoomRecording?: boolean;
|
|
354
|
+
currentMs?: number;
|
|
344
355
|
hasPassword?: boolean;
|
|
345
356
|
canViewShares: boolean;
|
|
346
357
|
}) {
|
|
@@ -352,6 +363,16 @@ function LinkTab({
|
|
|
352
363
|
);
|
|
353
364
|
const isPublic = visibility === "public";
|
|
354
365
|
const sharesLoaded = visibility !== null;
|
|
366
|
+
const timestampMs = Number.isFinite(currentMs)
|
|
367
|
+
? Math.max(0, Math.floor(currentMs! / 1000) * 1000)
|
|
368
|
+
: 0;
|
|
369
|
+
const [shareFromTimestamp, setShareFromTimestamp] = useState(false);
|
|
370
|
+
const linkUrl = useMemo(() => {
|
|
371
|
+
if (!shareFromTimestamp || timestampMs === 0) return shareUrl;
|
|
372
|
+
const url = new URL(shareUrl);
|
|
373
|
+
url.searchParams.set("at", String(timestampMs / 1000));
|
|
374
|
+
return url.toString();
|
|
375
|
+
}, [shareFromTimestamp, shareUrl, timestampMs]);
|
|
355
376
|
const visibilityPending = isPending || sharesQuery.isLoading;
|
|
356
377
|
const isLoomRecording = isLoomRecordingProp || isLoomEmbedUrl(videoUrl);
|
|
357
378
|
const needsScopedAgentContext = !isPublic || hasPassword !== false;
|
|
@@ -504,10 +525,23 @@ function LinkTab({
|
|
|
504
525
|
? t("shareDialog.shareLink")
|
|
505
526
|
: t("shareDialog.shareWithHumans")
|
|
506
527
|
}
|
|
507
|
-
value={
|
|
528
|
+
value={linkUrl}
|
|
508
529
|
disabled={visibilityPending || !sharesLoaded}
|
|
509
530
|
/>
|
|
510
531
|
|
|
532
|
+
<div className="flex items-center justify-between gap-3">
|
|
533
|
+
<Label className="text-sm" htmlFor="share-from-timestamp">
|
|
534
|
+
{t("shareDialog.startAtTimestamp", {
|
|
535
|
+
time: formatMs(timestampMs),
|
|
536
|
+
})}
|
|
537
|
+
</Label>
|
|
538
|
+
<Switch
|
|
539
|
+
id="share-from-timestamp"
|
|
540
|
+
checked={shareFromTimestamp}
|
|
541
|
+
onCheckedChange={setShareFromTimestamp}
|
|
542
|
+
/>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
511
545
|
{canViewShares ? (
|
|
512
546
|
visibility ? (
|
|
513
547
|
<GeneralAccessSelect
|
|
@@ -594,7 +628,7 @@ function LinkTab({
|
|
|
594
628
|
}
|
|
595
629
|
onMakePublic={() =>
|
|
596
630
|
setResourceVisibility("public", {
|
|
597
|
-
onSuccess: () => copyToClipboard(
|
|
631
|
+
onSuccess: () => copyToClipboard(linkUrl),
|
|
598
632
|
})
|
|
599
633
|
}
|
|
600
634
|
/>
|
|
@@ -7,15 +7,25 @@ import { Button } from "@/components/ui/button";
|
|
|
7
7
|
|
|
8
8
|
export type SignedOutShareCta = "signin" | "try_clips";
|
|
9
9
|
|
|
10
|
-
export function buildShareSignInHref(
|
|
11
|
-
|
|
10
|
+
export function buildShareSignInHref(
|
|
11
|
+
recordingId: string,
|
|
12
|
+
startAt?: string | null,
|
|
13
|
+
): string {
|
|
14
|
+
const params = new URLSearchParams();
|
|
15
|
+
if (startAt) params.set("at", startAt);
|
|
16
|
+
const query = params.toString();
|
|
17
|
+
return buildSignInReturnHref({
|
|
18
|
+
returnTo: `/share/${recordingId}${query ? `?${query}` : ""}`,
|
|
19
|
+
});
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
export function SignedOutShareActions({
|
|
15
23
|
recordingId,
|
|
24
|
+
startAt,
|
|
16
25
|
onCtaClick,
|
|
17
26
|
}: {
|
|
18
27
|
recordingId: string;
|
|
28
|
+
startAt?: string | null;
|
|
19
29
|
onCtaClick?: (cta: SignedOutShareCta) => void;
|
|
20
30
|
}) {
|
|
21
31
|
const t = useT();
|
|
@@ -24,7 +34,7 @@ export function SignedOutShareActions({
|
|
|
24
34
|
<>
|
|
25
35
|
<Button variant="outline" size="sm" asChild>
|
|
26
36
|
<a
|
|
27
|
-
href={buildShareSignInHref(recordingId)}
|
|
37
|
+
href={buildShareSignInHref(recordingId, startAt)}
|
|
28
38
|
className="gap-1.5"
|
|
29
39
|
onClick={() => onCtaClick?.("signin")}
|
|
30
40
|
>
|
|
@@ -144,6 +144,7 @@ const messages = {
|
|
|
144
144
|
recordingNotFound: "Recording not found",
|
|
145
145
|
noAccess: "You may not have access to this clip.",
|
|
146
146
|
backToLibrary: "Back to library",
|
|
147
|
+
sharedWithYou: "Shared with you",
|
|
147
148
|
storageStillDisconnected: "Storage still isn't connected",
|
|
148
149
|
finishBuilderOrS3:
|
|
149
150
|
"Finish the Builder.io popup or configure S3-compatible storage, then try again.",
|
|
@@ -515,6 +516,7 @@ const messages = {
|
|
|
515
516
|
height: "Height",
|
|
516
517
|
autoplay: "Autoplay",
|
|
517
518
|
startAt: "Start at (seconds)",
|
|
519
|
+
startAtTimestamp: "Start at {{time}}",
|
|
518
520
|
embedCode: "Embed code",
|
|
519
521
|
},
|
|
520
522
|
shareUi: {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function resolveStartMs(
|
|
2
|
+
startMs: number,
|
|
3
|
+
durationMs?: number | null,
|
|
4
|
+
): number {
|
|
5
|
+
if (!Number.isFinite(startMs) || startMs < 0) return 0;
|
|
6
|
+
if (
|
|
7
|
+
typeof durationMs === "number" &&
|
|
8
|
+
Number.isFinite(durationMs) &&
|
|
9
|
+
durationMs > 0 &&
|
|
10
|
+
startMs > durationMs
|
|
11
|
+
) {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
return startMs;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseTimeParam(raw: string | null): number {
|
|
18
|
+
if (!raw) return 0;
|
|
19
|
+
const value = raw.trim();
|
|
20
|
+
if (!value) return 0;
|
|
21
|
+
|
|
22
|
+
if (/^\d+(\.\d+)?$/.test(value)) {
|
|
23
|
+
return Math.floor(parseFloat(value) * 1000);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (/^\d+:\d+(:\d+)?$/.test(value)) {
|
|
27
|
+
const parts = value.split(":").map((part) => parseInt(part, 10));
|
|
28
|
+
if (parts.length === 2) return (parts[0] * 60 + parts[1]) * 1000;
|
|
29
|
+
if (parts.length === 3) {
|
|
30
|
+
return (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1000;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const match = value.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/i);
|
|
35
|
+
if (!match) return 0;
|
|
36
|
+
const hours = parseInt(match[1] ?? "0", 10);
|
|
37
|
+
const minutes = parseInt(match[2] ?? "0", 10);
|
|
38
|
+
const seconds = parseInt(match[3] ?? "0", 10);
|
|
39
|
+
return (hours * 3600 + minutes * 60 + seconds) * 1000;
|
|
40
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { Spinner } from "@/components/ui/spinner";
|
|
13
13
|
import { useViewTracking } from "@/hooks/use-view-tracking";
|
|
14
14
|
import { parsePlaybackSpeed } from "@/lib/playback-speed";
|
|
15
|
+
import { parseTimeParam, resolveStartMs } from "@/lib/time-param";
|
|
15
16
|
|
|
16
17
|
import { isLoomEmbedBackedRecording } from "../../shared/loom";
|
|
17
18
|
|
|
@@ -23,40 +24,6 @@ const STORAGE_KEY_PREFIX = "clips-share-pw-";
|
|
|
23
24
|
const READY_MEDIA_SETTLE_POLL_MS = 20 * 1000;
|
|
24
25
|
const READY_MEDIA_SETTLE_POLL_INTERVAL_MS = 1000;
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
* Parse `t` URL param into ms (supports plain seconds or `1m20s` / `1h2m3s`).
|
|
28
|
-
* "80" → 80_000
|
|
29
|
-
* "1m20s" → 80_000
|
|
30
|
-
* "1h2m3s" → 3_723_000
|
|
31
|
-
* "1:20" → 80_000 (MM:SS)
|
|
32
|
-
*/
|
|
33
|
-
function parseTimeParam(raw: string | null): number {
|
|
34
|
-
if (!raw) return 0;
|
|
35
|
-
const v = raw.trim();
|
|
36
|
-
if (!v) return 0;
|
|
37
|
-
|
|
38
|
-
// Plain seconds
|
|
39
|
-
if (/^\d+(\.\d+)?$/.test(v)) return Math.floor(parseFloat(v) * 1000);
|
|
40
|
-
|
|
41
|
-
// MM:SS or HH:MM:SS
|
|
42
|
-
if (/^\d+:\d+(:\d+)?$/.test(v)) {
|
|
43
|
-
const parts = v.split(":").map((n) => parseInt(n, 10));
|
|
44
|
-
if (parts.length === 2) return (parts[0] * 60 + parts[1]) * 1000;
|
|
45
|
-
if (parts.length === 3)
|
|
46
|
-
return (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1000;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// 1h2m3s style
|
|
50
|
-
const m = v.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/i);
|
|
51
|
-
if (m) {
|
|
52
|
-
const h = parseInt(m[1] ?? "0", 10);
|
|
53
|
-
const mm = parseInt(m[2] ?? "0", 10);
|
|
54
|
-
const s = parseInt(m[3] ?? "0", 10);
|
|
55
|
-
return (h * 3600 + mm * 60 + s) * 1000;
|
|
56
|
-
}
|
|
57
|
-
return 0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
27
|
export default function EmbedRoute() {
|
|
61
28
|
const t = useT();
|
|
62
29
|
const { shareId } = useParams<{ shareId: string }>();
|
|
@@ -251,7 +218,7 @@ export default function EmbedRoute() {
|
|
|
251
218
|
thumbnailUrl={recording.thumbnailUrl}
|
|
252
219
|
defaultSpeed={parsePlaybackSpeed(recording.defaultSpeed) ?? 1.2}
|
|
253
220
|
autoPlay={autoplay}
|
|
254
|
-
startMs={startMs}
|
|
221
|
+
startMs={resolveStartMs(startMs, recording.durationMs)}
|
|
255
222
|
comments={comments}
|
|
256
223
|
chapters={chapters}
|
|
257
224
|
transcriptSegments={transcriptSegments}
|
|
@@ -102,6 +102,7 @@ import enMessages from "@/i18n/en-US";
|
|
|
102
102
|
import { parsePlaybackSpeed } from "@/lib/playback-speed";
|
|
103
103
|
import { recordingShareUrl } from "@/lib/recording-link";
|
|
104
104
|
import { isStorageSetupFailureReason } from "@/lib/storage-failures";
|
|
105
|
+
import { parseTimeParam, resolveStartMs } from "@/lib/time-param";
|
|
105
106
|
import { cn } from "@/lib/utils";
|
|
106
107
|
|
|
107
108
|
import { STALE_PENDING_TRANSCRIPT_REASON } from "../../shared/transcript-status";
|
|
@@ -270,31 +271,6 @@ export function BackButton({ onBack }: { onBack: () => void }) {
|
|
|
270
271
|
);
|
|
271
272
|
}
|
|
272
273
|
|
|
273
|
-
function parseTimeParam(raw: string | null): number {
|
|
274
|
-
if (!raw) return 0;
|
|
275
|
-
const value = raw.trim();
|
|
276
|
-
if (!value) return 0;
|
|
277
|
-
|
|
278
|
-
if (/^\d+(\.\d+)?$/.test(value)) {
|
|
279
|
-
return Math.floor(parseFloat(value) * 1000);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (/^\d+:\d+(:\d+)?$/.test(value)) {
|
|
283
|
-
const parts = value.split(":").map((part) => parseInt(part, 10));
|
|
284
|
-
if (parts.length === 2) return (parts[0] * 60 + parts[1]) * 1000;
|
|
285
|
-
if (parts.length === 3) {
|
|
286
|
-
return (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1000;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const match = value.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/i);
|
|
291
|
-
if (!match) return 0;
|
|
292
|
-
const hours = parseInt(match[1] ?? "0", 10);
|
|
293
|
-
const minutes = parseInt(match[2] ?? "0", 10);
|
|
294
|
-
const seconds = parseInt(match[3] ?? "0", 10);
|
|
295
|
-
return (hours * 3600 + minutes * 60 + seconds) * 1000;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
274
|
export default function RecordingPage() {
|
|
299
275
|
const t = useT();
|
|
300
276
|
useAutoTitleBridge();
|
|
@@ -302,7 +278,9 @@ export default function RecordingPage() {
|
|
|
302
278
|
const { recordingId } = useParams<{ recordingId: string }>();
|
|
303
279
|
const navigate = useNavigate();
|
|
304
280
|
const [searchParams] = useSearchParams();
|
|
305
|
-
const startMs = parseTimeParam(
|
|
281
|
+
const startMs = parseTimeParam(
|
|
282
|
+
searchParams.get("at") ?? searchParams.get("t"),
|
|
283
|
+
);
|
|
306
284
|
const panelParam = searchParams.get("panel");
|
|
307
285
|
const { session, isLoading: sessionLoading } = useSession();
|
|
308
286
|
const playerRef = useRef<VideoPlayerHandle | null>(null);
|
|
@@ -310,19 +288,12 @@ export default function RecordingPage() {
|
|
|
310
288
|
const [panel, setPanel] = useState<SidePanel>("comments");
|
|
311
289
|
const [theaterMode, setTheaterMode] = useState(false);
|
|
312
290
|
const [editing, setEditing] = useState(false);
|
|
313
|
-
const [currentMs, setCurrentMs] = useState(
|
|
291
|
+
const [currentMs, setCurrentMs] = useState(startMs);
|
|
314
292
|
const [commentOpen, setCommentOpen] = useState(false);
|
|
315
293
|
const [commentAtMs, setCommentAtMs] = useState(0);
|
|
316
294
|
const [commentDraft, setCommentDraft] = useState("");
|
|
317
295
|
const [isPlayerFullscreen, setIsPlayerFullscreen] = useState(false);
|
|
318
296
|
const isCompactLayout = useIsCompactRecordingLayout();
|
|
319
|
-
// Resolve the playback position for reactions/comments. Native <video> exposes
|
|
320
|
-
// a live `currentTime`; Loom embeds render in a cross-origin iframe with no
|
|
321
|
-
// live time bridge, so we fall back to the last position the player reported
|
|
322
|
-
// via onTimeUpdate (seek/initial start).
|
|
323
|
-
const resolvePlaybackMs = useCallback(() => {
|
|
324
|
-
return playerRef.current?.getCurrentOriginalMs() ?? currentMs;
|
|
325
|
-
}, [currentMs]);
|
|
326
297
|
// The compact layout stacks the panel below the video, so switching tabs
|
|
327
298
|
// alone leaves the user looking at the player. Desktop always renders the
|
|
328
299
|
// side aside, so nothing to scroll there.
|
|
@@ -458,6 +429,14 @@ export default function RecordingPage() {
|
|
|
458
429
|
}, [recordingId, playerDataForbidden, navigate]);
|
|
459
430
|
|
|
460
431
|
const recording = playerDataQ.data?.recording;
|
|
432
|
+
const playbackMs = resolveStartMs(currentMs, recording?.durationMs);
|
|
433
|
+
// Resolve the playback position for reactions/comments. Native <video> exposes
|
|
434
|
+
// a live `currentTime`; Loom embeds render in a cross-origin iframe with no
|
|
435
|
+
// live time bridge, so we fall back to the last position the player reported
|
|
436
|
+
// via onTimeUpdate (seek/initial start).
|
|
437
|
+
const resolvePlaybackMs = useCallback(() => {
|
|
438
|
+
return playerRef.current?.getCurrentOriginalMs() ?? playbackMs;
|
|
439
|
+
}, [playbackMs]);
|
|
461
440
|
const verificationPending = recording?.verificationPending === true;
|
|
462
441
|
const role = playerDataQ.data?.role as
|
|
463
442
|
| "owner"
|
|
@@ -541,14 +520,14 @@ export default function RecordingPage() {
|
|
|
541
520
|
{
|
|
542
521
|
view: "recording",
|
|
543
522
|
recordingId: recording.id,
|
|
544
|
-
currentMs: Math.
|
|
523
|
+
currentMs: Math.round(playbackMs),
|
|
545
524
|
durationMs: recording.durationMs,
|
|
546
525
|
panel,
|
|
547
526
|
updatedAt: new Date(now).toISOString(),
|
|
548
527
|
},
|
|
549
528
|
{ requestSource: browserTabId },
|
|
550
529
|
).catch(() => {});
|
|
551
|
-
}, [browserTabId,
|
|
530
|
+
}, [browserTabId, panel, playbackMs, recording?.durationMs, recording?.id]);
|
|
552
531
|
const appStateVersion = useChangeVersions(["app-state", "action"]);
|
|
553
532
|
const generatedWorkflowQ = useQuery<GeneratedWorkflowState | null>({
|
|
554
533
|
queryKey: [
|
|
@@ -615,6 +594,9 @@ export default function RecordingPage() {
|
|
|
615
594
|
const viewerReshareOnly =
|
|
616
595
|
(role === "viewer" || role === "commenter") &&
|
|
617
596
|
(recording?.visibility === "public" || recording?.visibility === "org");
|
|
597
|
+
const isPrivateRecipient =
|
|
598
|
+
(role === "viewer" || role === "commenter") &&
|
|
599
|
+
recording?.visibility === "private";
|
|
618
600
|
const shareVideoUrl =
|
|
619
601
|
canDownloadRecording || isLoomEmbedBacked
|
|
620
602
|
? (recording?.videoUrl ?? null)
|
|
@@ -1064,16 +1046,22 @@ export default function RecordingPage() {
|
|
|
1064
1046
|
) : null}
|
|
1065
1047
|
</p>
|
|
1066
1048
|
</div>
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1049
|
+
{isPrivateRecipient ? (
|
|
1050
|
+
<span className="shrink-0 whitespace-nowrap text-sm font-medium text-muted-foreground">
|
|
1051
|
+
{t("recordingPage.sharedWithYou")}
|
|
1052
|
+
</span>
|
|
1053
|
+
) : (
|
|
1054
|
+
<ShareRecordingPopover
|
|
1055
|
+
recordingId={recording.id}
|
|
1056
|
+
recordingTitle={recording.title}
|
|
1057
|
+
initialVisibility={recording.visibility}
|
|
1058
|
+
initialRole={role}
|
|
1059
|
+
hasPassword={Boolean(recording.hasPassword)}
|
|
1060
|
+
viewerReshareOnly={viewerReshareOnly}
|
|
1061
|
+
>
|
|
1062
|
+
<ClipsShareTrigger label={t("recordingPage.share")} />
|
|
1063
|
+
</ShareRecordingPopover>
|
|
1064
|
+
)}
|
|
1077
1065
|
</header>
|
|
1078
1066
|
|
|
1079
1067
|
<main className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-5 p-4 sm:p-6">
|
|
@@ -1283,7 +1271,7 @@ export default function RecordingPage() {
|
|
|
1283
1271
|
segments={transcriptSegments}
|
|
1284
1272
|
fullText={transcriptFullText}
|
|
1285
1273
|
durationMs={recording.durationMs}
|
|
1286
|
-
currentMs={
|
|
1274
|
+
currentMs={playbackMs}
|
|
1287
1275
|
onSeek={(ms) => playerRef.current?.seek(ms)}
|
|
1288
1276
|
status={
|
|
1289
1277
|
requestTranscript.isPending && transcriptStatus === "failed"
|
|
@@ -1322,7 +1310,7 @@ export default function RecordingPage() {
|
|
|
1322
1310
|
<CommentsPanel
|
|
1323
1311
|
recordingId={recording.id}
|
|
1324
1312
|
comments={comments}
|
|
1325
|
-
currentMs={
|
|
1313
|
+
currentMs={playbackMs}
|
|
1326
1314
|
currentUserEmail={session?.email}
|
|
1327
1315
|
enableComments={recording.enableComments}
|
|
1328
1316
|
canComment={canComment}
|
|
@@ -1602,20 +1590,27 @@ export default function RecordingPage() {
|
|
|
1602
1590
|
) : null}
|
|
1603
1591
|
|
|
1604
1592
|
{!editing ? (
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1593
|
+
isPrivateRecipient ? (
|
|
1594
|
+
<span className="shrink-0 whitespace-nowrap text-sm font-medium text-muted-foreground">
|
|
1595
|
+
{t("recordingPage.sharedWithYou")}
|
|
1596
|
+
</span>
|
|
1597
|
+
) : (
|
|
1598
|
+
<ShareRecordingPopover
|
|
1599
|
+
recordingId={recording.id}
|
|
1600
|
+
recordingTitle={recording.title}
|
|
1601
|
+
initialVisibility={recording.visibility}
|
|
1602
|
+
initialRole={role}
|
|
1603
|
+
videoUrl={shareVideoUrl}
|
|
1604
|
+
thumbnailUrl={recording.thumbnailUrl}
|
|
1605
|
+
animatedThumbnailUrl={recording.animatedThumbnailUrl}
|
|
1606
|
+
isLoomRecording={isLoomEmbedBacked}
|
|
1607
|
+
currentMs={playbackMs}
|
|
1608
|
+
hasPassword={Boolean(recording.hasPassword)}
|
|
1609
|
+
viewerReshareOnly={viewerReshareOnly}
|
|
1610
|
+
>
|
|
1611
|
+
<ClipsShareTrigger label={t("recordingPage.share")} />
|
|
1612
|
+
</ShareRecordingPopover>
|
|
1613
|
+
)
|
|
1619
1614
|
) : null}
|
|
1620
1615
|
|
|
1621
1616
|
{canDelete || canDownloadRecording ? (
|
|
@@ -1662,7 +1657,7 @@ export default function RecordingPage() {
|
|
|
1662
1657
|
defaultSpeed={
|
|
1663
1658
|
parsePlaybackSpeed(recording.defaultSpeed) ?? 1.2
|
|
1664
1659
|
}
|
|
1665
|
-
startMs={startMs}
|
|
1660
|
+
startMs={resolveStartMs(startMs, recording.durationMs)}
|
|
1666
1661
|
comments={comments}
|
|
1667
1662
|
chapters={chapters}
|
|
1668
1663
|
reactions={reactions}
|