@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
@@ -205,6 +205,10 @@ function parseTimeParam(raw: string | null): number {
205
205
  return (hours * 3600 + minutes * 60 + seconds) * 1000;
206
206
  }
207
207
 
208
+ function buildSignInHref(returnTo: string): string {
209
+ return `${agentNativePath("/_agent-native/sign-in")}?return=${encodeURIComponent(returnTo)}`;
210
+ }
211
+
208
212
  export default function RecordingPage() {
209
213
  const t = useT();
210
214
  useAutoTitleBridge();
@@ -256,6 +260,7 @@ export default function RecordingPage() {
256
260
  );
257
261
  const lastPlayerStateWriteRef = useRef(0);
258
262
  const readyMediaPollRef = useRef<{ key: string; until: number } | null>(null);
263
+ const [metadataRefreshUntil, setMetadataRefreshUntil] = useState(0);
259
264
 
260
265
  useEffect(() => {
261
266
  if (
@@ -325,6 +330,7 @@ export default function RecordingPage() {
325
330
  // `update-recording` and we want the skeleton to swap in promptly.
326
331
  if (shouldShowGeneratedTitleSkeleton(rec, data?.transcript?.status))
327
332
  return 3000;
333
+ if (Date.now() < metadataRefreshUntil) return 2000;
328
334
  return false;
329
335
  },
330
336
  },
@@ -542,6 +548,8 @@ export default function RecordingPage() {
542
548
  });
543
549
  const regenerateTitle = useActionMutation("regenerate-title" as any, {
544
550
  onSuccess: (result: any) => {
551
+ setMetadataRefreshUntil(Date.now() + 60_000);
552
+ void playerDataQ.refetch();
545
553
  if (result?.updated) {
546
554
  toast.success(t("recordingPage.titleUpdated"));
547
555
  } else if (result?.reason === "builder_credits_paused") {
@@ -559,7 +567,11 @@ export default function RecordingPage() {
559
567
  onError: handleAiError,
560
568
  });
561
569
  const regenerateSummary = useActionMutation("regenerate-summary" as any, {
562
- onSuccess: () => toast.success(t("recordingPage.descriptionQueued")),
570
+ onSuccess: () => {
571
+ setMetadataRefreshUntil(Date.now() + 60_000);
572
+ void playerDataQ.refetch();
573
+ toast.success(t("recordingPage.descriptionQueued"));
574
+ },
563
575
  onError: handleAiError,
564
576
  });
565
577
  const regenerateChapters = useActionMutation("regenerate-chapters" as any, {
@@ -614,17 +626,22 @@ export default function RecordingPage() {
614
626
  }
615
627
  function handleGenerateWorkflow(kind: WorkflowKind) {
616
628
  if (!recording) return;
629
+ if (generatedWorkflow?.status === "generating") return;
617
630
  setEditing(false);
618
631
  setPanel("agent");
619
632
  window.dispatchEvent(
620
633
  new CustomEvent("agent-panel:set-mode", { detail: { mode: "chat" } }),
621
634
  );
635
+ window.dispatchEvent(new Event("agent-panel:open"));
622
636
  generateWorkflow.mutate({
623
637
  recordingId: recording.id,
624
638
  kind,
625
639
  } as any);
626
640
  }
627
641
 
642
+ const workflowBusy =
643
+ generateWorkflow.isPending || generatedWorkflow?.status === "generating";
644
+
628
645
  useEffect(() => {
629
646
  if (recording && panel === "settings" && !canEdit) setPanel("agent");
630
647
  }, [canEdit, panel, recording]);
@@ -709,18 +726,36 @@ export default function RecordingPage() {
709
726
  }
710
727
 
711
728
  if (playerDataQ.isError || !recording) {
729
+ const needsSignIn = !session;
730
+ const returnTo =
731
+ typeof window === "undefined"
732
+ ? `/r/${recordingId}`
733
+ : window.location.pathname + window.location.search;
712
734
  return (
713
735
  <div className="flex flex-col items-center justify-center h-screen w-full bg-background px-6">
714
736
  <h1 className="text-xl font-semibold mb-2">
715
- {t("recordingPage.recordingNotFound")}
737
+ {needsSignIn
738
+ ? t("sharePage.clipUnavailable")
739
+ : t("recordingPage.recordingNotFound")}
716
740
  </h1>
717
741
  <p className="text-sm text-muted-foreground mb-4">
718
- {(playerDataQ.error as Error | undefined)?.message ??
719
- t("recordingPage.noAccess")}
742
+ {needsSignIn
743
+ ? t("sharePage.clipUnavailableMessage")
744
+ : ((playerDataQ.error as Error | undefined)?.message ??
745
+ t("recordingPage.noAccess"))}
720
746
  </p>
721
- <Button asChild variant="outline">
722
- <Link to="/">{t("recordingPage.backToLibrary")}</Link>
723
- </Button>
747
+ <div className="flex flex-wrap items-center justify-center gap-2">
748
+ {needsSignIn ? (
749
+ <Button asChild>
750
+ <a href={buildSignInHref(returnTo)}>{t("sharePage.signIn")}</a>
751
+ </Button>
752
+ ) : null}
753
+ <Button asChild variant="outline">
754
+ <Link to="/library" replace>
755
+ {t("recordingPage.backToLibrary")}
756
+ </Link>
757
+ </Button>
758
+ </div>
724
759
  </div>
725
760
  );
726
761
  }
@@ -784,7 +819,7 @@ export default function RecordingPage() {
784
819
  className="shrink-0"
785
820
  aria-label={t("recordingPage.back")}
786
821
  >
787
- <Link to="/">
822
+ <Link to="/library" replace>
788
823
  <IconArrowLeft className="h-4 w-4 rtl:-scale-x-100" />
789
824
  </Link>
790
825
  </Button>
@@ -956,7 +991,9 @@ export default function RecordingPage() {
956
991
  : t("recordingPage.checkAgain")}
957
992
  </Button>
958
993
  <Button asChild variant="ghost" size="sm">
959
- <Link to="/">{t("recordingPage.backToLibrary")}</Link>
994
+ <Link to="/library" replace>
995
+ {t("recordingPage.backToLibrary")}
996
+ </Link>
960
997
  </Button>
961
998
  </div>
962
999
  </div>
@@ -1136,7 +1173,7 @@ export default function RecordingPage() {
1136
1173
  className="shrink-0"
1137
1174
  aria-label={t("recordingPage.back")}
1138
1175
  >
1139
- <Link to="/">
1176
+ <Link to="/library" replace>
1140
1177
  <IconArrowLeft className="h-4 w-4 rtl:-scale-x-100" />
1141
1178
  </Link>
1142
1179
  </Button>
@@ -1174,7 +1211,7 @@ export default function RecordingPage() {
1174
1211
  </Button>
1175
1212
  ) : null}
1176
1213
 
1177
- {canEdit ? (
1214
+ {canEdit && !editing ? (
1178
1215
  <DropdownMenu>
1179
1216
  <DropdownMenuTrigger asChild>
1180
1217
  <Button
@@ -1221,6 +1258,7 @@ export default function RecordingPage() {
1221
1258
  onSelect={() =>
1222
1259
  regenerateSummary.mutate({
1223
1260
  recordingId: recording.id,
1261
+ openInChat: true,
1224
1262
  } as any)
1225
1263
  }
1226
1264
  >
@@ -1264,7 +1302,7 @@ export default function RecordingPage() {
1264
1302
  const menuItem = (
1265
1303
  <DropdownMenuItem
1266
1304
  key={item.kind}
1267
- disabled={generateWorkflow.isPending}
1305
+ disabled={workflowBusy}
1268
1306
  onSelect={() => handleGenerateWorkflow(item.kind)}
1269
1307
  className={
1270
1308
  item.tooltipKey ? "justify-between gap-3" : undefined
@@ -1319,24 +1357,26 @@ export default function RecordingPage() {
1319
1357
  </DropdownMenu>
1320
1358
  ) : null}
1321
1359
 
1322
- <ShareRecordingPopover
1323
- recordingId={recording.id}
1324
- recordingTitle={recording.title}
1325
- initialVisibility={recording.visibility}
1326
- initialRole={role}
1327
- videoUrl={recording.videoUrl}
1328
- animatedThumbnailUrl={recording.animatedThumbnailUrl}
1329
- isLoomRecording={isLoomEmbedBacked}
1330
- hasPassword={Boolean(recording.hasPassword)}
1331
- >
1332
- <Button
1333
- className="shrink-0 gap-1.5 bg-primary text-primary-foreground hover:bg-primary/90"
1334
- size="sm"
1360
+ {!editing ? (
1361
+ <ShareRecordingPopover
1362
+ recordingId={recording.id}
1363
+ recordingTitle={recording.title}
1364
+ initialVisibility={recording.visibility}
1365
+ initialRole={role}
1366
+ videoUrl={recording.videoUrl}
1367
+ animatedThumbnailUrl={recording.animatedThumbnailUrl}
1368
+ isLoomRecording={isLoomEmbedBacked}
1369
+ hasPassword={Boolean(recording.hasPassword)}
1335
1370
  >
1336
- <IconShare3 className="h-4 w-4" />
1337
- {t("recordingPage.share")}
1338
- </Button>
1339
- </ShareRecordingPopover>
1371
+ <Button
1372
+ className="shrink-0 gap-1.5 bg-primary text-primary-foreground hover:bg-primary/90"
1373
+ size="sm"
1374
+ >
1375
+ <IconShare3 className="h-4 w-4" />
1376
+ {t("recordingPage.share")}
1377
+ </Button>
1378
+ </ShareRecordingPopover>
1379
+ ) : null}
1340
1380
 
1341
1381
  {canDelete || canDownloadRecording ? (
1342
1382
  <RecordingOptionsMenu
@@ -1486,7 +1526,7 @@ export default function RecordingPage() {
1486
1526
  ) : null}
1487
1527
  </div>
1488
1528
  {recording.description ? (
1489
- <p className="mt-3 line-clamp-2 text-sm text-muted-foreground">
1529
+ <p className="mt-3 whitespace-pre-wrap break-words text-sm text-muted-foreground">
1490
1530
  {recording.description}
1491
1531
  </p>
1492
1532
  ) : null}
@@ -1528,7 +1568,7 @@ function GeneratedWorkflowNotice({
1528
1568
  const status = workflow.status ?? (workflow.content ? "ready" : "generating");
1529
1569
  const content =
1530
1570
  typeof workflow.content === "string" ? workflow.content.trim() : "";
1531
- const isReady = status === "ready" && content.length > 0;
1571
+ const isReady = status !== "failed" && content.length > 0;
1532
1572
  const isFailed = status === "failed";
1533
1573
  const title = workflowTitle(workflow.kind);
1534
1574
 
@@ -855,6 +855,8 @@ export default function ShareRoute() {
855
855
  <Button variant="ghost" size="sm" asChild>
856
856
  <a
857
857
  href={appPath("/")}
858
+ target="_blank"
859
+ rel="noopener noreferrer"
858
860
  className="gap-1.5"
859
861
  onClick={() => fireShareCtaClick("try_clips")}
860
862
  >
@@ -965,7 +967,7 @@ export default function ShareRoute() {
965
967
  </h2>
966
968
  )}
967
969
  {recording.description ? (
968
- <p className="text-sm text-muted-foreground line-clamp-2">
970
+ <p className="whitespace-pre-wrap break-words text-sm text-muted-foreground">
969
971
  {recording.description}
970
972
  </p>
971
973
  ) : null}
@@ -1063,6 +1065,7 @@ export default function ShareRoute() {
1063
1065
  t("recordingPage.draftQuestions"),
1064
1066
  ]}
1065
1067
  browserTabId={getBrowserTabId()}
1068
+ showHeader={false}
1066
1069
  />
1067
1070
  ) : (
1068
1071
  <PublicAgentEmptyState
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ AI workflow generation now stays visible, avoids duplicate requests, and reports completion correctly.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ Clip playback and CTA controls remain interactive through editing and replay.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ Clip titles no longer describe the product as a Loom alternative.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ Description regeneration now opens the agent chat so progress is easy to follow.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: security
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ Direct recording URLs now respect share-link and password access controls
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ Disconnecting a calendar now clears unrecorded synced meeting placeholders.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ Folders now expose rename and delete actions directly in the library sidebar
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ Recording pages now explain sign-in access and avoid showing the same public link twice.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ The Clips extension quick-actions toolbar can be moved out of the way.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ The editor now skips cut sections during preview and keeps the full clip selection visible.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ Thumbnail capture now updates previews while scrubbing and lets uploaded files be removed.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ Windows permission links now open the matching system settings page.
@@ -83,6 +83,17 @@
83
83
  let bubbleDragLayer: HTMLDivElement | null = null;
84
84
  let bubblePersistTimer: ReturnType<typeof setTimeout> | undefined;
85
85
 
86
+ // The quick-actions toolbar is also draggable. Keep its geometry in the
87
+ // content script because the iframe cannot position itself on the host page.
88
+ const TOOLBAR_WIDTH = 68;
89
+ const TOOLBAR_COLLAPSED_HEIGHT = 154;
90
+ const toolbarGeom: { left: number | null; top: number | null } = {
91
+ left: null,
92
+ top: null,
93
+ };
94
+ let toolbarDragLayer: HTMLDivElement | null = null;
95
+ let toolbarPersistTimer: ReturnType<typeof setTimeout> | undefined;
96
+
86
97
  function bubbleSizePx(): number {
87
98
  return BUBBLE_SIZES[bubbleGeom.size] ?? BUBBLE_SIZES.lg;
88
99
  }
@@ -130,6 +141,92 @@
130
141
  }, 200);
131
142
  }
132
143
 
144
+ function clampToolbar(
145
+ left: number,
146
+ top: number,
147
+ height: number,
148
+ ): { left: number; top: number } {
149
+ return {
150
+ left: Math.max(8, Math.min(left, window.innerWidth - TOOLBAR_WIDTH - 8)),
151
+ top: Math.max(8, Math.min(top, window.innerHeight - height - 8)),
152
+ };
153
+ }
154
+
155
+ function applyToolbarGeom(): void {
156
+ const frame = document.getElementById(
157
+ partFrameId("toolbar"),
158
+ ) as HTMLIFrameElement | null;
159
+ if (!frame) return;
160
+ const height =
161
+ frame.getBoundingClientRect().height || TOOLBAR_COLLAPSED_HEIGHT;
162
+ const base = clampToolbar(
163
+ toolbarGeom.left ?? 16,
164
+ toolbarGeom.top ?? window.innerHeight / 2 - TOOLBAR_COLLAPSED_HEIGHT / 2,
165
+ height,
166
+ );
167
+ Object.assign(frame.style, {
168
+ left: `${base.left}px`,
169
+ top: `${base.top}px`,
170
+ right: "auto",
171
+ });
172
+ }
173
+
174
+ function persistToolbarGeom(): void {
175
+ clearTimeout(toolbarPersistTimer);
176
+ toolbarPersistTimer = setTimeout(() => {
177
+ try {
178
+ chrome.storage.local.set({ toolbarGeom });
179
+ } catch {
180
+ /* ignore */
181
+ }
182
+ }, 200);
183
+ }
184
+
185
+ function startToolbarDrag(): void {
186
+ if (toolbarDragLayer) return;
187
+ const frame = document.getElementById(
188
+ partFrameId("toolbar"),
189
+ ) as HTMLIFrameElement | null;
190
+ if (!frame) return;
191
+
192
+ const layer = document.createElement("div");
193
+ Object.assign(layer.style, {
194
+ position: "fixed",
195
+ inset: "0",
196
+ zIndex: "2147483647",
197
+ cursor: "grabbing",
198
+ });
199
+ (document.documentElement || document.body).appendChild(layer);
200
+ toolbarDragLayer = layer;
201
+
202
+ const onMove = (e: PointerEvent): void => {
203
+ const rect = frame.getBoundingClientRect();
204
+ const next = clampToolbar(
205
+ rect.left + e.movementX,
206
+ rect.top + e.movementY,
207
+ rect.height || TOOLBAR_COLLAPSED_HEIGHT,
208
+ );
209
+ toolbarGeom.left = next.left;
210
+ toolbarGeom.top = next.top;
211
+ Object.assign(frame.style, {
212
+ left: `${next.left}px`,
213
+ top: `${next.top}px`,
214
+ right: "auto",
215
+ });
216
+ };
217
+ const onUp = (): void => {
218
+ layer.removeEventListener("pointermove", onMove);
219
+ layer.removeEventListener("pointerup", onUp);
220
+ layer.removeEventListener("pointercancel", onUp);
221
+ layer.remove();
222
+ toolbarDragLayer = null;
223
+ persistToolbarGeom();
224
+ };
225
+ layer.addEventListener("pointermove", onMove);
226
+ layer.addEventListener("pointerup", onUp);
227
+ layer.addEventListener("pointercancel", onUp);
228
+ }
229
+
133
230
  function startBubbleDrag(): void {
134
231
  if (bubbleDragLayer) return;
135
232
  const frame = document.getElementById(
@@ -177,7 +274,7 @@
177
274
  }
178
275
 
179
276
  try {
180
- chrome.storage.local.get("bubbleGeom", (value) => {
277
+ chrome.storage.local.get(["bubbleGeom", "toolbarGeom"], (value) => {
181
278
  if (chrome.runtime.lastError) return;
182
279
  const g = value.bubbleGeom as
183
280
  | { size?: unknown; left?: unknown; top?: unknown }
@@ -188,12 +285,24 @@
188
285
  bubbleGeom.top = typeof g.top === "number" ? g.top : null;
189
286
  }
190
287
  applyBubbleGeom();
288
+ const toolbar = value.toolbarGeom as
289
+ | { left?: unknown; top?: unknown }
290
+ | undefined;
291
+ if (toolbar && typeof toolbar === "object") {
292
+ toolbarGeom.left =
293
+ typeof toolbar.left === "number" ? toolbar.left : null;
294
+ toolbarGeom.top = typeof toolbar.top === "number" ? toolbar.top : null;
295
+ }
296
+ applyToolbarGeom();
191
297
  });
192
298
  } catch {
193
299
  /* ignore */
194
300
  }
195
301
 
196
- window.addEventListener("resize", () => applyBubbleGeom());
302
+ window.addEventListener("resize", () => {
303
+ applyBubbleGeom();
304
+ applyToolbarGeom();
305
+ });
197
306
 
198
307
  function readCurrentParts(
199
308
  callback: (parts: OverlayPart[] | null) => void,
@@ -338,6 +447,7 @@
338
447
  styleFrame(frame, part);
339
448
  container.appendChild(frame);
340
449
  if (part === "bubble") applyBubbleGeom();
450
+ if (part === "toolbar") applyToolbarGeom();
341
451
  }
342
452
 
343
453
  // Camera-ready gating + connecting spinner: while the camera connects we show a
@@ -520,6 +630,7 @@
520
630
  const frame = document.getElementById(partFrameId("toolbar"));
521
631
  if (frame && typeof data.height === "number") {
522
632
  frame.style.height = `${Math.round(data.height)}px`;
633
+ applyToolbarGeom();
523
634
  }
524
635
  return;
525
636
  }
@@ -537,6 +648,10 @@
537
648
  startBubbleDrag();
538
649
  return;
539
650
  }
651
+ if (data.kind === "toolbar-drag-start") {
652
+ startToolbarDrag();
653
+ return;
654
+ }
540
655
  if (data.kind === "bubble-size") {
541
656
  bubbleGeom.size = data.size === "sm" ? "sm" : "lg";
542
657
  applyBubbleGeom();
@@ -84,6 +84,17 @@ function postToolbarSize(height: number): void {
84
84
  }
85
85
  }
86
86
 
87
+ function postToolbarDragStart(): void {
88
+ try {
89
+ window.parent.postMessage(
90
+ { source: "clips-overlay", kind: "toolbar-drag-start", part: "toolbar" },
91
+ "*",
92
+ );
93
+ } catch {
94
+ /* parent gone */
95
+ }
96
+ }
97
+
87
98
  function postCountdownFinished(): void {
88
99
  try {
89
100
  window.parent.postMessage(
@@ -406,6 +417,22 @@ function initCountdown(): void {
406
417
  function initToolbar(): void {
407
418
  const pill = document.createElement("div");
408
419
  pill.className = "toolbar-v";
420
+ pill.style.cursor = "grab";
421
+
422
+ // The content script owns iframe geometry, so ask it to capture the pointer
423
+ // page-wide when the user drags the toolbar outside its current bounds.
424
+ pill.addEventListener("pointerdown", (event) => {
425
+ if (event.button !== 0) return;
426
+ if ((event.target as HTMLElement).closest("button")) return;
427
+ event.preventDefault();
428
+ pill.style.cursor = "grabbing";
429
+ postToolbarDragStart();
430
+ const restore = (): void => {
431
+ pill.style.cursor = "grab";
432
+ window.removeEventListener("pointerup", restore);
433
+ };
434
+ window.addEventListener("pointerup", restore);
435
+ });
409
436
 
410
437
  const makeBtn = (
411
438
  cls: string,
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "plugins": {
42
42
  "shell": {
43
- "open": "(((mailto:\\w+)|(tel:\\w+)|(https?://\\w+)).+|(zoommtg://([A-Za-z0-9-]+\\.)*zoom\\.us/join\\?.+))"
43
+ "open": "(((mailto:\\w+)|(tel:\\w+)|(https?://\\w+)).+|(zoommtg://([A-Za-z0-9-]+\\.)*zoom\\.us/join\\?.+)|(ms-settings:[A-Za-z0-9._?=&-]+))"
44
44
  },
45
45
  "updater": {
46
46
  "active": true,