@agent-native/core 0.102.1 → 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 (113) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +9 -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/chat/message-components.tsx +17 -3
  8. package/corpus/core/src/client/composer/ComposerPlusMenu.tsx +96 -6
  9. package/corpus/core/src/client/composer/asset-picker-url.ts +80 -0
  10. package/corpus/core/src/client/error-format.ts +24 -0
  11. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +8 -1
  12. package/corpus/templates/assets/app/routes/library.tsx +69 -0
  13. package/corpus/templates/assets/changelog/2026-07-15-fixed-top-level-image-picker-selections-returning-to-the-com.md +6 -0
  14. package/corpus/templates/clips/actions/disconnect-calendar.ts +57 -9
  15. package/corpus/templates/clips/actions/generate-workflow.ts +102 -59
  16. package/corpus/templates/clips/actions/get-recording-player-data.ts +64 -1
  17. package/corpus/templates/clips/actions/regenerate-summary.ts +7 -0
  18. package/corpus/templates/clips/actions/regenerate-title.ts +29 -11
  19. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +22 -11
  20. package/corpus/templates/clips/app/components/editor/thumbnail-picker.tsx +87 -9
  21. package/corpus/templates/clips/app/components/editor/trim-handles.tsx +15 -0
  22. package/corpus/templates/clips/app/components/editor/waveform.tsx +21 -8
  23. package/corpus/templates/clips/app/components/library/folder-tree.tsx +52 -0
  24. package/corpus/templates/clips/app/components/player/settings-panel.tsx +86 -46
  25. package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -37
  26. package/corpus/templates/clips/app/components/player/video-player.tsx +45 -1
  27. package/corpus/templates/clips/app/i18n/ar-SA.ts +2 -0
  28. package/corpus/templates/clips/app/i18n/de-DE.ts +2 -0
  29. package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
  30. package/corpus/templates/clips/app/i18n/es-ES.ts +2 -0
  31. package/corpus/templates/clips/app/i18n/fr-FR.ts +2 -0
  32. package/corpus/templates/clips/app/i18n/hi-IN.ts +2 -0
  33. package/corpus/templates/clips/app/i18n/ja-JP.ts +2 -0
  34. package/corpus/templates/clips/app/i18n/ko-KR.ts +2 -0
  35. package/corpus/templates/clips/app/i18n/pt-BR.ts +2 -0
  36. package/corpus/templates/clips/app/i18n/zh-CN.ts +2 -0
  37. package/corpus/templates/clips/app/i18n/zh-TW.ts +2 -0
  38. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +14 -0
  39. package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -2
  40. package/corpus/templates/clips/app/routes/_index.tsx +1 -2
  41. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +71 -31
  42. package/corpus/templates/clips/app/routes/share.$shareId.tsx +4 -1
  43. package/corpus/templates/clips/changelog/2026-07-15-ai-workflow-generation-now-stays-visible-avoids-duplicate-re.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-15-clip-playback-and-cta-controls-remain-interactive-through-ed.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-07-15-clip-titles-no-longer-describe-the-product-as-a-loom-alterna.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-15-description-regeneration-now-opens-the-agent-chat-so-progres.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-07-15-direct-recording-urls-now-respect-share-link-and-password-ac.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-15-disconnecting-a-calendar-now-clears-unrecorded-synced-meetin.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-15-folders-now-expose-rename-and-delete-actions-directly-in-the.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-15-recording-pages-now-explain-sign-in-access-and-avoid-showing.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-15-the-clips-extension-quick-actions-toolbar-can-be-moved-out-o.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-15-the-editor-now-skips-cut-sections-during-preview-and-keeps-t.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-15-thumbnail-capture-now-updates-previews-while-scrubbing-and-l.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-15-windows-permission-links-open-the-matching-settings-page.md +6 -0
  55. package/corpus/templates/clips/chrome-extension/src/content-script.ts +117 -2
  56. package/corpus/templates/clips/chrome-extension/src/overlay.ts +27 -0
  57. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -1
  58. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +139 -101
  59. package/corpus/templates/clips/server/lib/recording-page-access.ts +35 -0
  60. package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +7 -3
  61. package/corpus/templates/design/changelog/2026-07-15-prototype-print-and-download-buttons-now-work-in-preview.md +6 -0
  62. package/corpus/templates/slides/actions/extract-pdf.ts +1 -1
  63. package/corpus/templates/slides/actions/import-file.ts +4 -1
  64. package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +1 -1
  65. package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +9 -12
  66. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -21
  67. package/corpus/templates/slides/app/i18n/ar-SA.ts +1 -0
  68. package/corpus/templates/slides/app/i18n/de-DE.ts +1 -0
  69. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  70. package/corpus/templates/slides/app/i18n/es-ES.ts +1 -0
  71. package/corpus/templates/slides/app/i18n/fr-FR.ts +1 -0
  72. package/corpus/templates/slides/app/i18n/hi-IN.ts +1 -0
  73. package/corpus/templates/slides/app/i18n/ja-JP.ts +1 -0
  74. package/corpus/templates/slides/app/i18n/ko-KR.ts +1 -0
  75. package/corpus/templates/slides/app/i18n/pt-BR.ts +1 -0
  76. package/corpus/templates/slides/app/i18n/zh-CN.ts +1 -0
  77. package/corpus/templates/slides/app/i18n/zh-TW.ts +1 -0
  78. package/corpus/templates/slides/changelog/2026-07-15-image-search-and-generation-controls-now-use-the-registered-.md +6 -0
  79. package/corpus/templates/slides/changelog/2026-07-15-pdf-deck-references-can-be-imported-for-slide-inspiration-ag.md +6 -0
  80. package/corpus/templates/slides/changelog/2026-07-15-stable-style-inspector-dock.md +6 -0
  81. package/dist/client/AgentPanel.d.ts.map +1 -1
  82. package/dist/client/AgentPanel.js +5 -0
  83. package/dist/client/AgentPanel.js.map +1 -1
  84. package/dist/client/AssistantChat.d.ts.map +1 -1
  85. package/dist/client/AssistantChat.js +7 -0
  86. package/dist/client/AssistantChat.js.map +1 -1
  87. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  88. package/dist/client/agent-chat-adapter.js +8 -1
  89. package/dist/client/agent-chat-adapter.js.map +1 -1
  90. package/dist/client/chat/message-components.d.ts +4 -0
  91. package/dist/client/chat/message-components.d.ts.map +1 -1
  92. package/dist/client/chat/message-components.js +10 -3
  93. package/dist/client/chat/message-components.js.map +1 -1
  94. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  95. package/dist/client/composer/ComposerPlusMenu.js +58 -6
  96. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  97. package/dist/client/composer/asset-picker-url.d.ts +16 -0
  98. package/dist/client/composer/asset-picker-url.d.ts.map +1 -0
  99. package/dist/client/composer/asset-picker-url.js +64 -0
  100. package/dist/client/composer/asset-picker-url.js.map +1 -0
  101. package/dist/client/error-format.d.ts.map +1 -1
  102. package/dist/client/error-format.js +17 -0
  103. package/dist/client/error-format.js.map +1 -1
  104. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  105. package/dist/client/resources/McpIntegrationDialog.js +6 -1
  106. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  107. package/dist/observability/routes.d.ts +5 -5
  108. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  109. package/dist/resources/handlers.d.ts +2 -2
  110. package/dist/secrets/routes.d.ts +9 -9
  111. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  112. package/dist/server/transcribe-voice.d.ts +1 -1
  113. package/package.json +1 -1
@@ -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}
@@ -455,6 +455,8 @@ const messages = {
455
455
  retryAgentLink: "إعادة المحاولة",
456
456
  gifPreview: "معاينة GIF",
457
457
  openPlayer: "مشغل مفتوح",
458
+ chooseFile: "اختيار ملف",
459
+ remove: "إزالة",
458
460
  downloadMp4: "تحميل MP4",
459
461
  embedsNeedPublic: "التضمينات تحتاج إلى مقطع عام",
460
462
  embedPublicDescription:
@@ -468,6 +468,8 @@ const messages = {
468
468
  retryAgentLink: "Erneut versuchen",
469
469
  gifPreview: "GIF-Vorschau",
470
470
  openPlayer: "Spieler öffnen",
471
+ chooseFile: "Datei auswählen",
472
+ remove: "Entfernen",
471
473
  downloadMp4: "Laden Sie MP4 herunter",
472
474
  embedsNeedPublic: "Für Einbettungen ist ein öffentlicher Clip erforderlich",
473
475
  embedPublicDescription:
@@ -451,6 +451,8 @@ const messages = {
451
451
  retryAgentLink: "Retry",
452
452
  gifPreview: "GIF preview",
453
453
  openPlayer: "Open player",
454
+ chooseFile: "Choose file",
455
+ remove: "Remove",
454
456
  downloadMp4: "Download MP4",
455
457
  embedsNeedPublic: "Embeds need a public clip",
456
458
  embedPublicDescription:
@@ -465,6 +465,8 @@ const messages = {
465
465
  retryAgentLink: "Reintentar",
466
466
  gifPreview: "vista previa de GIF",
467
467
  openPlayer: "jugador abierto",
468
+ chooseFile: "Elige el archivo",
469
+ remove: "Eliminar",
468
470
  downloadMp4: "Descargar MP4",
469
471
  embedsNeedPublic: "Los elementos insertados necesitan un clip público",
470
472
  embedPublicDescription:
@@ -465,6 +465,8 @@ const messages = {
465
465
  retryAgentLink: "Réessayer",
466
466
  gifPreview: "aperçu de GIF",
467
467
  openPlayer: "Joueur ouvert",
468
+ chooseFile: "Choisir un fichier",
469
+ remove: "Retirer",
468
470
  downloadMp4: "Télécharger MP4",
469
471
  embedsNeedPublic: "Les intégrations nécessitent un clip public",
470
472
  embedPublicDescription:
@@ -448,6 +448,8 @@ const messages = {
448
448
  retryAgentLink: "फिर से प्रयास करें",
449
449
  gifPreview: "GIF पूर्वावलोकन",
450
450
  openPlayer: "खुला खिलाड़ी",
451
+ chooseFile: "फ़ाइल चुनें",
452
+ remove: "हटाएं",
451
453
  downloadMp4: "MP4 डाउनलोड करें",
452
454
  embedsNeedPublic: "एंबेड को एक सार्वजनिक क्लिप की आवश्यकता है",
453
455
  embedPublicDescription:
@@ -460,6 +460,8 @@ const messages = {
460
460
  retryAgentLink: "再試行",
461
461
  gifPreview: "GIF プレビュー",
462
462
  openPlayer: "プレーヤーを開く",
463
+ chooseFile: "ファイルを選択してください",
464
+ remove: "取り除く",
463
465
  downloadMp4: "ダウンロード",
464
466
  embedsNeedPublic: "埋め込みにはパブリッククリップが必要です",
465
467
  embedPublicDescription:
@@ -453,6 +453,8 @@ const messages = {
453
453
  retryAgentLink: "다시 시도",
454
454
  gifPreview: "GIF 미리보기",
455
455
  openPlayer: "플레이어 열기",
456
+ chooseFile: "파일 선택",
457
+ remove: "제거",
456
458
  downloadMp4: "MP4 다운로드",
457
459
  embedsNeedPublic: "퍼가기에는 공개 클립이 필요합니다",
458
460
  embedPublicDescription:
@@ -462,6 +462,8 @@ const messages = {
462
462
  retryAgentLink: "Tentar novamente",
463
463
  gifPreview: "visualização de GIF",
464
464
  openPlayer: "Jogador aberto",
465
+ chooseFile: "Escolher arquivo",
466
+ remove: "Remover",
465
467
  downloadMp4: "Baixar MP4",
466
468
  embedsNeedPublic: "As incorporações precisam de um clipe público",
467
469
  embedPublicDescription:
@@ -435,6 +435,8 @@ const messages = {
435
435
  retryAgentLink: "重试",
436
436
  gifPreview: "GIF 预览",
437
437
  openPlayer: "开放玩家",
438
+ chooseFile: "选择文件",
439
+ remove: "消除",
438
440
  downloadMp4: "下载MP4",
439
441
  embedsNeedPublic: "嵌入需要公共剪辑",
440
442
  embedPublicDescription:
@@ -435,6 +435,8 @@ const messages = {
435
435
  retryAgentLink: "重試",
436
436
  gifPreview: "GIF 預覽",
437
437
  openPlayer: "開啟播放器",
438
+ chooseFile: "選取檔案",
439
+ remove: "消除",
438
440
  downloadMp4: "下載 MP4",
439
441
  embedsNeedPublic: "嵌入需要公開剪輯",
440
442
  embedPublicDescription:
@@ -198,6 +198,20 @@ export function getKeptRanges(
198
198
  return out;
199
199
  }
200
200
 
201
+ /** Move a source timestamp to the first visible timestamp after a cut. */
202
+ export function skipExcludedRange(
203
+ ms: number,
204
+ excludedRanges: Pick<TrimRange, "startMs" | "endMs">[],
205
+ durationMs: number,
206
+ ): number {
207
+ const range = excludedRanges.find(
208
+ (candidate) => ms >= candidate.startMs && ms < candidate.endMs,
209
+ );
210
+ if (!range) return ms;
211
+ const next = Math.max(ms, range.endMs);
212
+ return durationMs > 0 ? Math.min(next, durationMs) : next;
213
+ }
214
+
201
215
  /**
202
216
  * Merge a new excluded range into the edits, collapsing adjacent/overlapping
203
217
  * entries. Preserves existing non-excluded (split) markers as-is.
@@ -6,8 +6,7 @@ import { NavLink } from "react-router";
6
6
  import { LibraryGrid } from "@/components/library/library-grid";
7
7
  import { usePageHeaderLayout } from "@/components/library/page-header";
8
8
 
9
- const SEO_TITLE =
10
- "Agent-Native Clips - Open Source, agent-friendly Loom alternative";
9
+ const SEO_TITLE = "Agent-Native Clips - Open Source screen recorder";
11
10
  const SEO_DESCRIPTION =
12
11
  "Open Source screen recorder and meeting-notes app with AI transcripts, summaries, search, dictation, and agent-readable share links.";
13
12
 
@@ -1,8 +1,7 @@
1
1
  import { DefaultSpinner } from "@agent-native/core/client";
2
2
  import { redirect, type LoaderFunctionArgs } from "react-router";
3
3
 
4
- const SEO_TITLE =
5
- "Agent-Native Clips - Open Source, agent-friendly Loom alternative";
4
+ const SEO_TITLE = "Agent-Native Clips - Open Source screen recorder";
6
5
  const SEO_DESCRIPTION =
7
6
  "Open Source screen recorder and meeting-notes app with AI transcripts, summaries, search, dictation, and agent-readable share links.";
8
7