@agent-native/core 0.102.0 → 0.102.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AgentPanel.tsx +7 -0
  5. package/corpus/core/src/client/AssistantChat.tsx +7 -0
  6. package/corpus/core/src/client/agent-chat-adapter.ts +7 -1
  7. package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +1 -1
  8. package/corpus/core/src/client/chat/message-components.tsx +17 -3
  9. package/corpus/core/src/client/composer/ComposerPlusMenu.tsx +96 -6
  10. package/corpus/core/src/client/composer/asset-picker-url.ts +80 -0
  11. package/corpus/core/src/client/error-format.ts +24 -0
  12. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +8 -1
  13. package/corpus/templates/analytics/changelog/2026-07-15-fixed-the-agent-discarding-a-successful-dashboard-edit-and-r.md +6 -0
  14. package/corpus/templates/analytics/server/lib/real-data-actions.ts +30 -1
  15. package/corpus/templates/analytics/server/plugins/agent-chat.ts +15 -0
  16. package/corpus/templates/assets/app/routes/library.tsx +69 -0
  17. package/corpus/templates/assets/changelog/2026-07-15-fixed-top-level-image-picker-selections-returning-to-the-com.md +6 -0
  18. package/corpus/templates/clips/actions/disconnect-calendar.ts +57 -9
  19. package/corpus/templates/clips/actions/generate-workflow.ts +102 -59
  20. package/corpus/templates/clips/actions/get-recording-player-data.ts +64 -1
  21. package/corpus/templates/clips/actions/regenerate-summary.ts +7 -0
  22. package/corpus/templates/clips/actions/regenerate-title.ts +29 -11
  23. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +22 -11
  24. package/corpus/templates/clips/app/components/editor/thumbnail-picker.tsx +87 -9
  25. package/corpus/templates/clips/app/components/editor/trim-handles.tsx +15 -0
  26. package/corpus/templates/clips/app/components/editor/waveform.tsx +21 -8
  27. package/corpus/templates/clips/app/components/library/folder-tree.tsx +52 -0
  28. package/corpus/templates/clips/app/components/player/settings-panel.tsx +86 -46
  29. package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -37
  30. package/corpus/templates/clips/app/components/player/video-player.tsx +45 -1
  31. package/corpus/templates/clips/app/i18n/ar-SA.ts +2 -0
  32. package/corpus/templates/clips/app/i18n/de-DE.ts +2 -0
  33. package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
  34. package/corpus/templates/clips/app/i18n/es-ES.ts +2 -0
  35. package/corpus/templates/clips/app/i18n/fr-FR.ts +2 -0
  36. package/corpus/templates/clips/app/i18n/hi-IN.ts +2 -0
  37. package/corpus/templates/clips/app/i18n/ja-JP.ts +2 -0
  38. package/corpus/templates/clips/app/i18n/ko-KR.ts +2 -0
  39. package/corpus/templates/clips/app/i18n/pt-BR.ts +2 -0
  40. package/corpus/templates/clips/app/i18n/zh-CN.ts +2 -0
  41. package/corpus/templates/clips/app/i18n/zh-TW.ts +2 -0
  42. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +14 -0
  43. package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -2
  44. package/corpus/templates/clips/app/routes/_index.tsx +1 -2
  45. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +71 -31
  46. package/corpus/templates/clips/app/routes/share.$shareId.tsx +4 -1
  47. package/corpus/templates/clips/changelog/2026-07-15-ai-workflow-generation-now-stays-visible-avoids-duplicate-re.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-15-clip-playback-and-cta-controls-remain-interactive-through-ed.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-15-clip-titles-no-longer-describe-the-product-as-a-loom-alterna.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-15-description-regeneration-now-opens-the-agent-chat-so-progres.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-15-direct-recording-urls-now-respect-share-link-and-password-ac.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-15-disconnecting-a-calendar-now-clears-unrecorded-synced-meetin.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-15-folders-now-expose-rename-and-delete-actions-directly-in-the.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-15-recording-pages-now-explain-sign-in-access-and-avoid-showing.md +6 -0
  55. package/corpus/templates/clips/changelog/2026-07-15-the-clips-extension-quick-actions-toolbar-can-be-moved-out-o.md +6 -0
  56. package/corpus/templates/clips/changelog/2026-07-15-the-editor-now-skips-cut-sections-during-preview-and-keeps-t.md +6 -0
  57. package/corpus/templates/clips/changelog/2026-07-15-thumbnail-capture-now-updates-previews-while-scrubbing-and-l.md +6 -0
  58. package/corpus/templates/clips/changelog/2026-07-15-windows-permission-links-open-the-matching-settings-page.md +6 -0
  59. package/corpus/templates/clips/chrome-extension/src/content-script.ts +117 -2
  60. package/corpus/templates/clips/chrome-extension/src/overlay.ts +27 -0
  61. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -1
  62. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +139 -101
  63. package/corpus/templates/clips/server/lib/recording-page-access.ts +35 -0
  64. package/corpus/templates/content/app/components/editor/DescriptionField.tsx +10 -2
  65. package/corpus/templates/content/changelog/2026-07-15-page-and-database-descriptions-now-use-a-clean-single-line-f.md +6 -0
  66. package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +7 -3
  67. package/corpus/templates/design/changelog/2026-07-15-prototype-print-and-download-buttons-now-work-in-preview.md +6 -0
  68. package/corpus/templates/slides/actions/extract-pdf.ts +1 -1
  69. package/corpus/templates/slides/actions/import-file.ts +4 -1
  70. package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +1 -1
  71. package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +9 -12
  72. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -21
  73. package/corpus/templates/slides/app/i18n/ar-SA.ts +1 -0
  74. package/corpus/templates/slides/app/i18n/de-DE.ts +1 -0
  75. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  76. package/corpus/templates/slides/app/i18n/es-ES.ts +1 -0
  77. package/corpus/templates/slides/app/i18n/fr-FR.ts +1 -0
  78. package/corpus/templates/slides/app/i18n/hi-IN.ts +1 -0
  79. package/corpus/templates/slides/app/i18n/ja-JP.ts +1 -0
  80. package/corpus/templates/slides/app/i18n/ko-KR.ts +1 -0
  81. package/corpus/templates/slides/app/i18n/pt-BR.ts +1 -0
  82. package/corpus/templates/slides/app/i18n/zh-CN.ts +1 -0
  83. package/corpus/templates/slides/app/i18n/zh-TW.ts +1 -0
  84. package/corpus/templates/slides/changelog/2026-07-15-image-search-and-generation-controls-now-use-the-registered-.md +6 -0
  85. package/corpus/templates/slides/changelog/2026-07-15-pdf-deck-references-can-be-imported-for-slide-inspiration-ag.md +6 -0
  86. package/corpus/templates/slides/changelog/2026-07-15-stable-style-inspector-dock.md +6 -0
  87. package/dist/client/AgentPanel.d.ts.map +1 -1
  88. package/dist/client/AgentPanel.js +5 -0
  89. package/dist/client/AgentPanel.js.map +1 -1
  90. package/dist/client/AssistantChat.d.ts.map +1 -1
  91. package/dist/client/AssistantChat.js +7 -0
  92. package/dist/client/AssistantChat.js.map +1 -1
  93. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  94. package/dist/client/agent-chat-adapter.js +8 -1
  95. package/dist/client/agent-chat-adapter.js.map +1 -1
  96. package/dist/client/agent-page/AgentTabsPage.js +1 -1
  97. package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
  98. package/dist/client/chat/message-components.d.ts +4 -0
  99. package/dist/client/chat/message-components.d.ts.map +1 -1
  100. package/dist/client/chat/message-components.js +10 -3
  101. package/dist/client/chat/message-components.js.map +1 -1
  102. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  103. package/dist/client/composer/ComposerPlusMenu.js +58 -6
  104. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  105. package/dist/client/composer/asset-picker-url.d.ts +16 -0
  106. package/dist/client/composer/asset-picker-url.d.ts.map +1 -0
  107. package/dist/client/composer/asset-picker-url.js +64 -0
  108. package/dist/client/composer/asset-picker-url.js.map +1 -0
  109. package/dist/client/error-format.d.ts.map +1 -1
  110. package/dist/client/error-format.js +17 -0
  111. package/dist/client/error-format.js.map +1 -1
  112. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  113. package/dist/client/resources/McpIntegrationDialog.js +6 -1
  114. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  115. package/dist/collab/routes.d.ts +1 -1
  116. package/dist/observability/routes.d.ts +1 -1
  117. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  118. package/package.json +1 -1
@@ -4,6 +4,7 @@ import {
4
4
  IconPhotoEdit,
5
5
  IconUpload,
6
6
  IconLoader2,
7
+ IconX,
7
8
  } from "@tabler/icons-react";
8
9
  import { useEffect, useRef, useState } from "react";
9
10
  import { toast } from "sonner";
@@ -55,6 +56,10 @@ export function ThumbnailPicker({
55
56
  const [gifDataUrl, setGifDataUrl] = useState<string | null>(null);
56
57
 
57
58
  const videoRef = useRef<HTMLVideoElement | null>(null);
59
+ const framePreviewRef = useRef<HTMLVideoElement | null>(null);
60
+ const gifVideoRef = useRef<HTMLVideoElement | null>(null);
61
+ const gifPreviewRef = useRef<HTMLVideoElement | null>(null);
62
+ const uploadInputRef = useRef<HTMLInputElement | null>(null);
58
63
 
59
64
  const mutation = useActionMutation("set-thumbnail");
60
65
 
@@ -65,6 +70,7 @@ export function ThumbnailPicker({
65
70
  setFrameDataUrl(null);
66
71
  setGifDataUrl(null);
67
72
  setGifProgress(null);
73
+ if (uploadInputRef.current) uploadInputRef.current.value = "";
68
74
  }
69
75
  }, [open]);
70
76
 
@@ -98,6 +104,18 @@ export function ThumbnailPicker({
98
104
  }
99
105
  };
100
106
 
107
+ const seekPreview = (video: HTMLVideoElement | null, timeMs: number) => {
108
+ if (!video || !Number.isFinite(timeMs)) return;
109
+ const apply = () => {
110
+ video.currentTime = Math.max(0, timeMs) / 1000;
111
+ };
112
+ if (video.readyState === HTMLMediaElement.HAVE_NOTHING) {
113
+ video.addEventListener("loadedmetadata", apply, { once: true });
114
+ } else {
115
+ apply();
116
+ }
117
+ };
118
+
101
119
  const handleGifGenerate = async () => {
102
120
  if (!videoUrl) return;
103
121
  try {
@@ -187,6 +205,7 @@ export function ThumbnailPicker({
187
205
  <TabsContent value="upload" className="py-4 space-y-3">
188
206
  <Label>{t("thumbnailPicker.uploadImage")}</Label>
189
207
  <input
208
+ ref={uploadInputRef}
190
209
  type="file"
191
210
  accept="image/*"
192
211
  onChange={(e) => {
@@ -196,14 +215,39 @@ export function ThumbnailPicker({
196
215
  reader.onload = () => setUploadDataUrl(reader.result as string);
197
216
  reader.readAsDataURL(file);
198
217
  }}
199
- className="text-sm"
218
+ className="sr-only"
200
219
  />
220
+ <Button
221
+ type="button"
222
+ variant="secondary"
223
+ size="sm"
224
+ onClick={() => uploadInputRef.current?.click()}
225
+ >
226
+ <IconUpload className="mr-1 h-4 w-4" />
227
+ {t("shareDialog.chooseFile")}
228
+ </Button>
201
229
  {uploadDataUrl ? (
202
- <img
203
- src={uploadDataUrl}
204
- alt={t("thumbnailPicker.uploadedPreview")}
205
- className="max-h-60 rounded border border-border"
206
- />
230
+ <div className="relative w-fit">
231
+ <img
232
+ src={uploadDataUrl}
233
+ alt={t("thumbnailPicker.uploadedPreview")}
234
+ className="max-h-60 rounded border border-border"
235
+ />
236
+ <Button
237
+ type="button"
238
+ variant="secondary"
239
+ size="icon"
240
+ className="absolute right-1 top-1 h-7 w-7"
241
+ aria-label={t("shareDialog.remove")}
242
+ onClick={() => {
243
+ setUploadDataUrl(null);
244
+ if (uploadInputRef.current)
245
+ uploadInputRef.current.value = "";
246
+ }}
247
+ >
248
+ <IconX className="h-4 w-4" />
249
+ </Button>
250
+ </div>
207
251
  ) : currentThumbnailUrl ? (
208
252
  <img
209
253
  src={currentThumbnailUrl}
@@ -235,7 +279,13 @@ export function ThumbnailPicker({
235
279
  max={Math.max(1000, durationMs)}
236
280
  step={100}
237
281
  value={[frameTime]}
238
- onValueChange={([v]) => setFrameTime(v)}
282
+ onValueChange={([v]) => {
283
+ const next = v ?? 0;
284
+ setFrameTime(next);
285
+ setFrameDataUrl(null);
286
+ seekPreview(videoRef.current, next);
287
+ seekPreview(framePreviewRef.current, next);
288
+ }}
239
289
  />
240
290
  <Button
241
291
  size="sm"
@@ -256,6 +306,15 @@ export function ThumbnailPicker({
256
306
  alt={t("thumbnailPicker.capturedFrame")}
257
307
  className="w-full rounded border border-border mt-1"
258
308
  />
309
+ ) : videoUrl ? (
310
+ <video
311
+ ref={framePreviewRef}
312
+ src={videoUrl}
313
+ className="w-full rounded border border-border bg-black mt-1"
314
+ crossOrigin="anonymous"
315
+ preload="auto"
316
+ muted
317
+ />
259
318
  ) : (
260
319
  <div className="w-full aspect-video rounded border border-dashed border-border flex items-center justify-center text-xs text-muted-foreground mt-1">
261
320
  {t("thumbnailPicker.capturePreview")}
@@ -269,6 +328,7 @@ export function ThumbnailPicker({
269
328
  <div className="grid grid-cols-2 gap-4">
270
329
  <div>
271
330
  <video
331
+ ref={gifVideoRef}
272
332
  src={videoUrl ?? undefined}
273
333
  className="w-full rounded border border-border bg-black"
274
334
  crossOrigin="anonymous"
@@ -287,7 +347,13 @@ export function ThumbnailPicker({
287
347
  max={Math.max(0, durationMs - gifDuration)}
288
348
  step={100}
289
349
  value={[gifStart]}
290
- onValueChange={([v]) => setGifStart(v)}
350
+ onValueChange={([v]) => {
351
+ const next = v ?? 0;
352
+ setGifStart(next);
353
+ setGifDataUrl(null);
354
+ seekPreview(gifVideoRef.current, next);
355
+ seekPreview(gifPreviewRef.current, next);
356
+ }}
291
357
  />
292
358
  </div>
293
359
  <div>
@@ -301,7 +367,10 @@ export function ThumbnailPicker({
301
367
  max={10000}
302
368
  step={100}
303
369
  value={[gifDuration]}
304
- onValueChange={([v]) => setGifDuration(v)}
370
+ onValueChange={([v]) => {
371
+ setGifDuration(v ?? 500);
372
+ setGifDataUrl(null);
373
+ }}
305
374
  />
306
375
  </div>
307
376
  <Button
@@ -331,6 +400,15 @@ export function ThumbnailPicker({
331
400
  alt={t("thumbnailPicker.animatedPreview")}
332
401
  className="w-full rounded border border-border mt-1"
333
402
  />
403
+ ) : videoUrl ? (
404
+ <video
405
+ ref={gifPreviewRef}
406
+ src={videoUrl}
407
+ className="w-full rounded border border-border bg-black mt-1"
408
+ crossOrigin="anonymous"
409
+ preload="auto"
410
+ muted
411
+ />
334
412
  ) : (
335
413
  <div className="w-full aspect-video rounded border border-dashed border-border flex items-center justify-center text-xs text-muted-foreground mt-1">
336
414
  {t("thumbnailPicker.generateGifPreview")}
@@ -13,6 +13,7 @@ export interface TrimHandlesProps {
13
13
  /** Fires when the user releases the mouse after drag. */
14
14
  onCommit?: (value: { startMs: number; endMs: number }) => void;
15
15
  durationMs: number;
16
+ splitPoints?: number[];
16
17
  className?: string;
17
18
  }
18
19
 
@@ -44,6 +45,7 @@ export function TrimHandles({
44
45
  onChange,
45
46
  onCommit,
46
47
  durationMs,
48
+ splitPoints = [],
47
49
  className,
48
50
  }: TrimHandlesProps) {
49
51
  const rootRef = useRef<HTMLDivElement | null>(null);
@@ -111,6 +113,9 @@ export function TrimHandles({
111
113
 
112
114
  const startX = (value.startMs / Math.max(durationMs, 1)) * width;
113
115
  const endX = (value.endMs / Math.max(durationMs, 1)) * width;
116
+ const visibleSplitPoints = splitPoints.filter(
117
+ (ms) => ms > value.startMs && ms < value.endMs, // i18n-ignore numeric range predicate, not visible UI copy
118
+ );
114
119
 
115
120
  return (
116
121
  <div
@@ -133,6 +138,16 @@ export function TrimHandles({
133
138
  <div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-full bg-foreground text-background text-[10px] px-1.5 py-0.5 rounded font-mono whitespace-nowrap">
134
139
  {formatMs(value.endMs - value.startMs)}
135
140
  </div>
141
+ {visibleSplitPoints.map((splitMs) => (
142
+ <div
143
+ key={splitMs}
144
+ className="absolute top-0 h-full w-0.5 -translate-x-1/2 bg-rose-500/95"
145
+ style={{
146
+ left: `${((splitMs - value.startMs) / Math.max(value.endMs - value.startMs, 1)) * 100}%`,
147
+ }}
148
+ aria-hidden="true"
149
+ />
150
+ ))}
136
151
  </div>
137
152
 
138
153
  {/* Left handle */}
@@ -18,6 +18,8 @@ export interface WaveformProps {
18
18
  durationMs: number;
19
19
  /** Excluded ranges (original time) — drawn as striped overlays. */
20
20
  excludedRanges?: Array<{ startMs: number; endMs: number }>;
21
+ /** Split markers (original time) — drawn over the active selection. */
22
+ splitPoints?: number[];
21
23
  /** Optional selection range (original time) highlighted in brand color. */
22
24
  selectionRange?: { startMs: number; endMs: number } | null;
23
25
  /** Transcript-backed activity ranges used when browser audio decoding fails. */
@@ -87,6 +89,7 @@ export function Waveform({
87
89
  playheadMs,
88
90
  durationMs,
89
91
  excludedRanges,
92
+ splitPoints = [],
90
93
  selectionRange,
91
94
  activityRanges = [],
92
95
  onSeek,
@@ -220,19 +223,28 @@ export function Waveform({
220
223
 
221
224
  // Selection overlay
222
225
  if (selectionRange) {
223
- const xStart =
224
- (Math.min(selectionRange.startMs, selectionRange.endMs) /
225
- Math.max(durationMs, 1)) *
226
- totalWidth;
227
- const xEnd =
228
- (Math.max(selectionRange.startMs, selectionRange.endMs) /
229
- Math.max(durationMs, 1)) *
230
- totalWidth;
226
+ const startMs = Math.min(selectionRange.startMs, selectionRange.endMs);
227
+ const endMs = Math.max(selectionRange.startMs, selectionRange.endMs);
228
+ const xStart = (startMs / Math.max(durationMs, 1)) * totalWidth;
229
+ const xEnd = (endMs / Math.max(durationMs, 1)) * totalWidth;
231
230
  ctx.fillStyle = getBrandColorAlpha(0.28);
232
231
  ctx.fillRect(xStart, 0, xEnd - xStart, height);
233
232
  ctx.strokeStyle = getBrandColor();
234
233
  ctx.lineWidth = 1;
235
234
  ctx.strokeRect(xStart + 0.5, 0.5, xEnd - xStart - 1, height - 1);
235
+
236
+ // Keep split markers visible on the selected track as well as on the
237
+ // ruler so a split is visibly actionable within the selection.
238
+ for (const splitMs of splitPoints) {
239
+ if (splitMs <= startMs || splitMs >= endMs) continue;
240
+ const splitX = (splitMs / Math.max(durationMs, 1)) * totalWidth;
241
+ ctx.strokeStyle = "rgba(244, 63, 94, 0.95)";
242
+ ctx.lineWidth = 2;
243
+ ctx.beginPath();
244
+ ctx.moveTo(splitX, 0);
245
+ ctx.lineTo(splitX, height);
246
+ ctx.stroke();
247
+ }
236
248
  }
237
249
  }, [
238
250
  peaks,
@@ -240,6 +252,7 @@ export function Waveform({
240
252
  height,
241
253
  excludedRanges,
242
254
  selectionRange,
255
+ splitPoints,
243
256
  durationMs,
244
257
  activityRanges,
245
258
  ]);
@@ -6,6 +6,7 @@ import {
6
6
  IconFolderPlus,
7
7
  IconTrash,
8
8
  IconEdit,
9
+ IconDots,
9
10
  } from "@tabler/icons-react";
10
11
  import { useMemo, useState } from "react";
11
12
  import { NavLink } from "react-router";
@@ -33,6 +34,13 @@ import {
33
34
  ContextMenuSeparator,
34
35
  ContextMenuTrigger,
35
36
  } from "@/components/ui/context-menu";
37
+ import {
38
+ DropdownMenu,
39
+ DropdownMenuContent,
40
+ DropdownMenuItem,
41
+ DropdownMenuSeparator,
42
+ DropdownMenuTrigger,
43
+ } from "@/components/ui/dropdown-menu";
36
44
  import {
37
45
  useCreateFolder,
38
46
  useDeleteFolder,
@@ -186,6 +194,50 @@ function FolderItem({
186
194
  {node.name}
187
195
  </span>
188
196
  </NavLink>
197
+ <DropdownMenu>
198
+ <DropdownMenuTrigger asChild>
199
+ <button
200
+ type="button"
201
+ aria-label={`${node.name}: ${t("root.commandActions")}`}
202
+ title={`${node.name}: ${t("root.commandActions")}`}
203
+ className="rounded p-0.5 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring group-hover:opacity-100 group-focus-within:opacity-100 data-[state=open]:opacity-100"
204
+ >
205
+ <IconDots className="h-3.5 w-3.5" />
206
+ </button>
207
+ </DropdownMenuTrigger>
208
+ <DropdownMenuContent align="start" side="right">
209
+ <DropdownMenuItem
210
+ onSelect={() => {
211
+ setTimeout(() => {
212
+ setRenameValue(node.name);
213
+ setRenameOpen(true);
214
+ }, 0);
215
+ }}
216
+ >
217
+ <IconEdit className="h-3.5 w-3.5 me-2" />{" "}
218
+ {t("folderTree.rename")}
219
+ </DropdownMenuItem>
220
+ <DropdownMenuItem
221
+ onSelect={() => {
222
+ setTimeout(() => {
223
+ setNewValue("");
224
+ setNewOpen(true);
225
+ }, 0);
226
+ }}
227
+ >
228
+ <IconFolderPlus className="h-3.5 w-3.5 me-2" />{" "}
229
+ {t("folderTree.newSubfolder")}
230
+ </DropdownMenuItem>
231
+ <DropdownMenuSeparator />
232
+ <DropdownMenuItem
233
+ onSelect={() => setTimeout(() => setConfirmDelete(true), 0)}
234
+ className="text-destructive"
235
+ >
236
+ <IconTrash className="h-3.5 w-3.5 me-2" />{" "}
237
+ {t("folderTree.delete")}
238
+ </DropdownMenuItem>
239
+ </DropdownMenuContent>
240
+ </DropdownMenu>
189
241
  </div>
190
242
  </ContextMenuTrigger>
191
243
  <ContextMenuContent>
@@ -361,6 +361,7 @@ function CtaEditor({
361
361
  // editing this card, so an agent edit to the CTA shows up live. `editing`
362
362
  // flips true while focus is anywhere inside the card.
363
363
  const editing = useRef(false);
364
+ const [collapsed, setCollapsed] = useState(true);
364
365
  const [label, setLabel] = useReconciledState(cta.label, {
365
366
  active: editing.current,
366
367
  });
@@ -387,52 +388,91 @@ function CtaEditor({
387
388
  }
388
389
  }}
389
390
  >
390
- <Input
391
- value={label}
392
- onChange={(e) => setLabel(e.target.value)}
393
- placeholder={t("playerSettings.buttonLabelPlaceholder")}
394
- />
395
- <Input
396
- value={url}
397
- onChange={(e) => setUrl(e.target.value)}
398
- placeholder="https://…"
399
- />
400
- <div className="flex gap-2">
401
- <input
402
- type="color"
403
- value={color}
404
- onChange={(e) => setColor(e.target.value)}
405
- className="h-9 w-12 rounded cursor-pointer border border-border"
406
- />
407
- <Select
408
- value={placement}
409
- onValueChange={(v) => setPlacement(v as "end" | "throughout")}
410
- >
411
- <SelectTrigger className="flex-1">
412
- <SelectValue />
413
- </SelectTrigger>
414
- <SelectContent>
415
- <SelectItem value="throughout">
416
- {t("playerSettings.placementThroughout")}
417
- </SelectItem>
418
- <SelectItem value="end">
419
- {t("playerSettings.placementEnd")}
420
- </SelectItem>
421
- </SelectContent>
422
- </Select>
423
- </div>
424
- <div className="flex gap-2">
425
- <Button
426
- size="sm"
427
- className="bg-primary hover:bg-primary/90 text-primary-foreground"
428
- onClick={() => onSave({ label, url, color, placement })}
429
- >
430
- {t("common.save")}
431
- </Button>
432
- <Button size="sm" variant="ghost" onClick={onDelete}>
433
- {t("playerSettings.delete")}
434
- </Button>
435
- </div>
391
+ {collapsed ? (
392
+ <div className="flex items-center gap-2">
393
+ <div className="min-w-0 flex-1">
394
+ <p className="truncate text-sm font-medium">{cta.label}</p>
395
+ <p className="text-xs text-muted-foreground">
396
+ {cta.placement === "throughout"
397
+ ? t("playerSettings.placementThroughout")
398
+ : t("playerSettings.placementEnd")}
399
+ </p>
400
+ </div>
401
+ <Button
402
+ type="button"
403
+ size="sm"
404
+ variant="ghost"
405
+ title={t("recordingPage.edit")}
406
+ onClick={() => setCollapsed(false)}
407
+ >
408
+ {t("recordingPage.edit")}
409
+ </Button>
410
+ <Button type="button" size="sm" variant="ghost" onClick={onDelete}>
411
+ {t("playerSettings.delete")}
412
+ </Button>
413
+ </div>
414
+ ) : (
415
+ <>
416
+ <Input
417
+ value={label}
418
+ onChange={(e) => setLabel(e.target.value)}
419
+ placeholder={t("playerSettings.buttonLabelPlaceholder")}
420
+ />
421
+ <Input
422
+ value={url}
423
+ onChange={(e) => setUrl(e.target.value)}
424
+ placeholder="https://…"
425
+ />
426
+ <div className="flex gap-2">
427
+ <input
428
+ type="color"
429
+ value={color}
430
+ onChange={(e) => setColor(e.target.value)}
431
+ className="h-9 w-12 rounded cursor-pointer border border-border"
432
+ />
433
+ <Select
434
+ value={placement}
435
+ onValueChange={(v) => setPlacement(v as "end" | "throughout")}
436
+ >
437
+ <SelectTrigger className="flex-1">
438
+ <SelectValue />
439
+ </SelectTrigger>
440
+ <SelectContent>
441
+ <SelectItem value="throughout">
442
+ {t("playerSettings.placementThroughout")}
443
+ </SelectItem>
444
+ <SelectItem value="end">
445
+ {t("playerSettings.placementEnd")}
446
+ </SelectItem>
447
+ </SelectContent>
448
+ </Select>
449
+ </div>
450
+ <div className="flex gap-2">
451
+ <Button
452
+ type="button"
453
+ size="sm"
454
+ className="bg-primary text-primary-foreground hover:bg-primary/90"
455
+ onClick={() => {
456
+ onSave({ label, url, color, placement });
457
+ setCollapsed(true);
458
+ }}
459
+ >
460
+ {t("common.save")}
461
+ </Button>
462
+ <Button
463
+ type="button"
464
+ size="sm"
465
+ variant="ghost"
466
+ onClick={() => setCollapsed(true)}
467
+ >
468
+ {t("recordingPage.done")}
469
+ </Button>
470
+ <Button type="button" size="sm" variant="ghost" onClick={onDelete}>
471
+ {t("playerSettings.delete")}
472
+ </Button>
473
+ </div>
474
+ </>
475
+ )}
436
476
  </div>
437
477
  );
438
478
  }
@@ -380,49 +380,55 @@ function LinkTab({
380
380
  )}
381
381
 
382
382
  <CopyField
383
- label={t("shareDialog.shareWithHumans")}
383
+ label={
384
+ isPublic
385
+ ? t("shareDialog.shareLink")
386
+ : t("shareDialog.shareWithHumans")
387
+ }
384
388
  value={shareUrl}
385
389
  disabled={
386
390
  visibilityPending || !sharesLoaded || (!isPublic && canManage)
387
391
  }
388
392
  />
389
393
 
390
- <div className="space-y-2">
391
- <CopyField
392
- label={t("shareDialog.shareWithAgents")}
393
- value={agentLink}
394
- disabled={agentShareDisabled}
395
- />
396
- {sharesLoaded && !isPublic ? (
397
- <>
398
- <p className="text-xs text-muted-foreground">
399
- {t("shareDialog.agentTokenDescription")}
400
- </p>
401
- {agentLinkError ? (
402
- <div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
403
- <p className="text-xs text-muted-foreground">
404
- {t("shareDialog.agentLinkUnavailable")}
405
- </p>
406
- <Button
407
- type="button"
408
- variant="ghost"
409
- size="sm"
410
- className="h-7"
411
- onClick={() => void loadAgentContextUrl()}
412
- disabled={createAgentLink.isPending}
413
- >
414
- {t("shareDialog.retryAgentLink")}
415
- </Button>
416
- </div>
417
- ) : null}
418
- <CopyField
419
- label={t("shareDialog.copyAgentPrompt")}
420
- value={agentPrompt}
421
- disabled={agentShareDisabled}
422
- />
423
- </>
424
- ) : null}
425
- </div>
394
+ {!isPublic ? (
395
+ <div className="space-y-2">
396
+ <CopyField
397
+ label={t("shareDialog.shareWithAgents")}
398
+ value={agentLink}
399
+ disabled={agentShareDisabled}
400
+ />
401
+ {sharesLoaded ? (
402
+ <>
403
+ <p className="text-xs text-muted-foreground">
404
+ {t("shareDialog.agentTokenDescription")}
405
+ </p>
406
+ {agentLinkError ? (
407
+ <div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
408
+ <p className="text-xs text-muted-foreground">
409
+ {t("shareDialog.agentLinkUnavailable")}
410
+ </p>
411
+ <Button
412
+ type="button"
413
+ variant="ghost"
414
+ size="sm"
415
+ className="h-7"
416
+ onClick={() => void loadAgentContextUrl()}
417
+ disabled={createAgentLink.isPending}
418
+ >
419
+ {t("shareDialog.retryAgentLink")}
420
+ </Button>
421
+ </div>
422
+ ) : null}
423
+ <CopyField
424
+ label={t("shareDialog.copyAgentPrompt")}
425
+ value={agentPrompt}
426
+ disabled={agentShareDisabled}
427
+ />
428
+ </>
429
+ ) : null}
430
+ </div>
431
+ ) : null}
426
432
 
427
433
  {sharesLoaded && !isPublic && canManage ? (
428
434
  <MakePublicCard
@@ -592,6 +592,14 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
592
592
  pauseVideo();
593
593
  return;
594
594
  }
595
+ if (v.ended) {
596
+ try {
597
+ v.currentTime = 0;
598
+ setCurrentMs(0);
599
+ } catch {
600
+ // Let the normal play attempt report a media error if the seek fails.
601
+ }
602
+ }
595
603
  requestPlay();
596
604
  }, [isPlaying, pauseVideo, requestPlay]);
597
605
 
@@ -1423,6 +1431,21 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
1423
1431
  </div>
1424
1432
  )}
1425
1433
 
1434
+ {thumbnailUrl &&
1435
+ !autoPlay &&
1436
+ !hasPlaybackStarted &&
1437
+ (!startMs || startMs <= 0) ? (
1438
+ <img
1439
+ src={resolveLocalUrl(thumbnailUrl)}
1440
+ alt=""
1441
+ aria-hidden="true"
1442
+ className={cn(
1443
+ "pointer-events-none absolute inset-0 z-[1] h-full w-full",
1444
+ cover ? "object-cover" : "object-contain",
1445
+ )}
1446
+ />
1447
+ ) : null}
1448
+
1426
1449
  {centerOverlayMode ? (
1427
1450
  <CenterPlaybackOverlay
1428
1451
  mode={centerOverlayMode}
@@ -1462,7 +1485,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
1462
1485
 
1463
1486
  {/* Floating CTA (throughout placement) */}
1464
1487
  {showThroughoutCta ? (
1465
- <div data-player-ui className="absolute bottom-16 right-4 z-20">
1488
+ <div data-player-ui className="absolute bottom-16 right-4 z-30">
1466
1489
  <CtaButton
1467
1490
  cta={cta!}
1468
1491
  onClick={() => onCtaClick?.(cta!.id)}
@@ -1484,6 +1507,27 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
1484
1507
  onClick={() => onCtaClick?.(cta!.id)}
1485
1508
  large
1486
1509
  />
1510
+ <button
1511
+ type="button"
1512
+ data-player-ui
1513
+ aria-label={t("videoPlayer.playClip")}
1514
+ onClick={(e) => {
1515
+ e.stopPropagation();
1516
+ const v = videoRef.current;
1517
+ if (!v) return;
1518
+ try {
1519
+ v.currentTime = 0;
1520
+ setCurrentMs(0);
1521
+ } catch {
1522
+ // The regular play path will surface any media error.
1523
+ }
1524
+ requestPlay();
1525
+ }}
1526
+ className="pointer-events-auto inline-flex items-center gap-2 rounded-md border border-white/30 bg-white/10 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
1527
+ >
1528
+ <IconPlayerPlay className="h-4 w-4 fill-current" />
1529
+ {t("videoPlayer.playClip")}
1530
+ </button>
1487
1531
  </div>
1488
1532
  </div>
1489
1533
  ) : null}