@agent-native/core 0.70.3 → 0.72.0

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 (221) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +51 -0
  3. package/corpus/core/docs/content/actions.md +23 -0
  4. package/corpus/core/docs/content/audit-log.md +111 -0
  5. package/corpus/core/docs/content/multi-app-workspace.md +2 -2
  6. package/corpus/core/docs/design/durable-agent-runs.md +458 -4
  7. package/corpus/core/package.json +2 -1
  8. package/corpus/core/src/action.ts +80 -1
  9. package/corpus/core/src/agent/durable-background.ts +192 -0
  10. package/corpus/core/src/agent/production-agent.ts +357 -13
  11. package/corpus/core/src/agent/run-manager.ts +66 -3
  12. package/corpus/core/src/agent/run-store.ts +143 -23
  13. package/corpus/core/src/agent/types.ts +20 -0
  14. package/corpus/core/src/application-state/store.ts +5 -0
  15. package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
  16. package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
  17. package/corpus/core/src/audit/cleanup-job.ts +100 -0
  18. package/corpus/core/src/audit/config.ts +91 -0
  19. package/corpus/core/src/audit/index.ts +43 -0
  20. package/corpus/core/src/audit/record.ts +143 -0
  21. package/corpus/core/src/audit/redact.ts +109 -0
  22. package/corpus/core/src/audit/store.ts +244 -0
  23. package/corpus/core/src/audit/types.ts +125 -0
  24. package/corpus/core/src/chat-threads/store.ts +10 -0
  25. package/corpus/core/src/client/AgentPanel.tsx +12 -9
  26. package/corpus/core/src/client/AssistantChat.tsx +6 -0
  27. package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
  28. package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
  29. package/corpus/core/src/db/client.ts +4 -0
  30. package/corpus/core/src/db/widen-columns.ts +75 -0
  31. package/corpus/core/src/deploy/build.ts +96 -0
  32. package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
  33. package/corpus/core/src/mcp/build-server.ts +23 -13
  34. package/corpus/core/src/oauth-tokens/store.ts +6 -0
  35. package/corpus/core/src/provider-api/custom-registry.ts +8 -0
  36. package/corpus/core/src/resources/store.ts +10 -0
  37. package/corpus/core/src/scripts/runner.ts +6 -3
  38. package/corpus/core/src/server/action-discovery.ts +6 -0
  39. package/corpus/core/src/server/action-routes.ts +1 -0
  40. package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
  41. package/corpus/core/src/server/auth.ts +5 -0
  42. package/corpus/core/src/server/core-routes-plugin.ts +14 -0
  43. package/corpus/core/src/server/security-headers.ts +9 -6
  44. package/corpus/core/src/settings/store.ts +6 -0
  45. package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  46. package/corpus/core/src/usage/store.ts +6 -0
  47. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
  48. package/corpus/templates/analytics/AGENTS.md +8 -0
  49. package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
  50. package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
  51. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
  52. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
  53. package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
  54. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
  55. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
  56. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
  57. package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
  58. package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
  59. package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
  60. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
  61. package/corpus/templates/clips/chrome-extension/src/background.ts +163 -32
  62. package/corpus/templates/clips/chrome-extension/src/content-script.ts +301 -24
  63. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +245 -67
  64. package/corpus/templates/clips/chrome-extension/src/overlay.css +77 -43
  65. package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
  66. package/corpus/templates/clips/chrome-extension/src/overlay.ts +102 -38
  67. package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
  68. package/corpus/templates/clips/chrome-extension/src/popup.ts +275 -4
  69. package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
  70. package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
  71. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
  72. package/corpus/templates/clips/desktop/src/lib/recorder.ts +10 -17
  73. package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
  74. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
  75. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
  76. package/corpus/templates/clips/desktop/src/styles.css +33 -5
  77. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
  78. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  79. package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
  80. package/corpus/templates/clips/shared/recording-audio.ts +62 -0
  81. package/corpus/templates/clips/shared/recording-core.ts +94 -0
  82. package/dist/action.d.ts +31 -0
  83. package/dist/action.d.ts.map +1 -1
  84. package/dist/action.js +45 -1
  85. package/dist/action.js.map +1 -1
  86. package/dist/agent/context-xray/schema.d.ts +1 -1
  87. package/dist/agent/durable-background.d.ts +60 -0
  88. package/dist/agent/durable-background.d.ts.map +1 -0
  89. package/dist/agent/durable-background.js +144 -0
  90. package/dist/agent/durable-background.js.map +1 -0
  91. package/dist/agent/observational-memory/schema.d.ts +1 -1
  92. package/dist/agent/production-agent.d.ts +20 -0
  93. package/dist/agent/production-agent.d.ts.map +1 -1
  94. package/dist/agent/production-agent.js +320 -14
  95. package/dist/agent/production-agent.js.map +1 -1
  96. package/dist/agent/run-manager.d.ts +48 -0
  97. package/dist/agent/run-manager.d.ts.map +1 -1
  98. package/dist/agent/run-manager.js +45 -3
  99. package/dist/agent/run-manager.js.map +1 -1
  100. package/dist/agent/run-store.d.ts +30 -1
  101. package/dist/agent/run-store.d.ts.map +1 -1
  102. package/dist/agent/run-store.js +138 -24
  103. package/dist/agent/run-store.js.map +1 -1
  104. package/dist/agent/types.d.ts +20 -0
  105. package/dist/agent/types.d.ts.map +1 -1
  106. package/dist/agent/types.js.map +1 -1
  107. package/dist/application-state/store.d.ts.map +1 -1
  108. package/dist/application-state/store.js +5 -0
  109. package/dist/application-state/store.js.map +1 -1
  110. package/dist/audit/actions/get-audit-event.d.ts +11 -0
  111. package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
  112. package/dist/audit/actions/get-audit-event.js +22 -0
  113. package/dist/audit/actions/get-audit-event.js.map +1 -0
  114. package/dist/audit/actions/list-audit-events.d.ts +22 -0
  115. package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
  116. package/dist/audit/actions/list-audit-events.js +61 -0
  117. package/dist/audit/actions/list-audit-events.js.map +1 -0
  118. package/dist/audit/cleanup-job.d.ts +12 -0
  119. package/dist/audit/cleanup-job.d.ts.map +1 -0
  120. package/dist/audit/cleanup-job.js +93 -0
  121. package/dist/audit/cleanup-job.js.map +1 -0
  122. package/dist/audit/config.d.ts +30 -0
  123. package/dist/audit/config.d.ts.map +1 -0
  124. package/dist/audit/config.js +65 -0
  125. package/dist/audit/config.js.map +1 -0
  126. package/dist/audit/index.d.ts +13 -0
  127. package/dist/audit/index.d.ts.map +1 -0
  128. package/dist/audit/index.js +6 -0
  129. package/dist/audit/index.js.map +1 -0
  130. package/dist/audit/record.d.ts +24 -0
  131. package/dist/audit/record.d.ts.map +1 -0
  132. package/dist/audit/record.js +106 -0
  133. package/dist/audit/record.js.map +1 -0
  134. package/dist/audit/redact.d.ts +28 -0
  135. package/dist/audit/redact.d.ts.map +1 -0
  136. package/dist/audit/redact.js +109 -0
  137. package/dist/audit/redact.js.map +1 -0
  138. package/dist/audit/store.d.ts +14 -0
  139. package/dist/audit/store.d.ts.map +1 -0
  140. package/dist/audit/store.js +219 -0
  141. package/dist/audit/store.js.map +1 -0
  142. package/dist/audit/types.d.ts +114 -0
  143. package/dist/audit/types.d.ts.map +1 -0
  144. package/dist/audit/types.js +15 -0
  145. package/dist/audit/types.js.map +1 -0
  146. package/dist/chat-threads/store.d.ts.map +1 -1
  147. package/dist/chat-threads/store.js +10 -0
  148. package/dist/chat-threads/store.js.map +1 -1
  149. package/dist/client/AgentPanel.d.ts.map +1 -1
  150. package/dist/client/AgentPanel.js +2 -2
  151. package/dist/client/AgentPanel.js.map +1 -1
  152. package/dist/client/AssistantChat.d.ts.map +1 -1
  153. package/dist/client/AssistantChat.js +6 -1
  154. package/dist/client/AssistantChat.js.map +1 -1
  155. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
  156. package/dist/client/blocks/library/FileTreeBlock.js +37 -4
  157. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
  158. package/dist/client/chat/repo-helpers.d.ts +16 -0
  159. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  160. package/dist/client/chat/repo-helpers.js +40 -0
  161. package/dist/client/chat/repo-helpers.js.map +1 -1
  162. package/dist/db/client.d.ts.map +1 -1
  163. package/dist/db/client.js +3 -0
  164. package/dist/db/client.js.map +1 -1
  165. package/dist/db/widen-columns.d.ts +39 -0
  166. package/dist/db/widen-columns.d.ts.map +1 -0
  167. package/dist/db/widen-columns.js +73 -0
  168. package/dist/db/widen-columns.js.map +1 -0
  169. package/dist/deploy/build.d.ts +29 -0
  170. package/dist/deploy/build.d.ts.map +1 -1
  171. package/dist/deploy/build.js +85 -0
  172. package/dist/deploy/build.js.map +1 -1
  173. package/dist/deploy/workspace-deploy.d.ts.map +1 -1
  174. package/dist/deploy/workspace-deploy.js +108 -0
  175. package/dist/deploy/workspace-deploy.js.map +1 -1
  176. package/dist/mcp/build-server.d.ts.map +1 -1
  177. package/dist/mcp/build-server.js +24 -9
  178. package/dist/mcp/build-server.js.map +1 -1
  179. package/dist/oauth-tokens/store.d.ts.map +1 -1
  180. package/dist/oauth-tokens/store.js +6 -0
  181. package/dist/oauth-tokens/store.js.map +1 -1
  182. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  183. package/dist/provider-api/custom-registry.js +8 -0
  184. package/dist/provider-api/custom-registry.js.map +1 -1
  185. package/dist/resources/store.d.ts.map +1 -1
  186. package/dist/resources/store.js +9 -0
  187. package/dist/resources/store.js.map +1 -1
  188. package/dist/scripts/runner.js +4 -3
  189. package/dist/scripts/runner.js.map +1 -1
  190. package/dist/server/action-discovery.d.ts.map +1 -1
  191. package/dist/server/action-discovery.js +6 -0
  192. package/dist/server/action-discovery.js.map +1 -1
  193. package/dist/server/action-routes.d.ts.map +1 -1
  194. package/dist/server/action-routes.js +1 -0
  195. package/dist/server/action-routes.js.map +1 -1
  196. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  197. package/dist/server/agent-chat-plugin.js +68 -13
  198. package/dist/server/agent-chat-plugin.js.map +1 -1
  199. package/dist/server/auth.d.ts.map +1 -1
  200. package/dist/server/auth.js +5 -0
  201. package/dist/server/auth.js.map +1 -1
  202. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  203. package/dist/server/core-routes-plugin.js +13 -0
  204. package/dist/server/core-routes-plugin.js.map +1 -1
  205. package/dist/server/security-headers.d.ts +8 -5
  206. package/dist/server/security-headers.d.ts.map +1 -1
  207. package/dist/server/security-headers.js +9 -6
  208. package/dist/server/security-headers.js.map +1 -1
  209. package/dist/settings/store.d.ts.map +1 -1
  210. package/dist/settings/store.js +6 -0
  211. package/dist/settings/store.js.map +1 -1
  212. package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  213. package/dist/usage/store.d.ts.map +1 -1
  214. package/dist/usage/store.js +5 -0
  215. package/dist/usage/store.js.map +1 -1
  216. package/docs/content/actions.md +23 -0
  217. package/docs/content/audit-log.md +111 -0
  218. package/docs/content/multi-app-workspace.md +2 -2
  219. package/docs/design/durable-agent-runs.md +458 -4
  220. package/package.json +2 -1
  221. package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
@@ -1,10 +1,15 @@
1
- // Loom-style in-page overlay host. This content script is injected into every
2
- // page (declared for <all_urls>) and is wrapped in an IIFE so it emits a single
3
- // self-contained classic script with no module imports/exports and leaks no
4
- // names into the shared global scope. Its only job is to mount/unmount the
5
- // overlay iframes; all UI and control logic lives inside the extension-origin
6
- // overlay pages (src/overlay.html). The background service worker is the source
7
- // of truth for which "parts" are visible and pushes them here.
1
+ // Loom-style in-page overlay host. The background service worker injects this
2
+ // into the LAUNCH TAB on demand via chrome.scripting.executeScript (covered by
3
+ // the activeTab permission the user grants when they click the extension), NOT
4
+ // declaratively on every page that would need broad "<all_urls>" host access
5
+ // and Chrome's in-depth review. So the overlay lives on the tab the recording
6
+ // was started from; it does not follow across tabs unless CROSS_TAB_FOLLOW is
7
+ // re-enabled in background.ts (see PERMISSIONS.md). Wrapped in an IIFE so it
8
+ // emits a single self-contained classic script with no module imports/exports
9
+ // and leaks no names into the shared global scope. Its only job is to
10
+ // mount/unmount the overlay iframes; all UI and control logic lives inside the
11
+ // extension-origin overlay pages (src/overlay.html). The worker is the source of
12
+ // truth for which "parts" are visible and pushes them here.
8
13
 
9
14
  (function clipsOverlayHost() {
10
15
  type OverlayPart = "bubble" | "countdown" | "toolbar" | "saving";
@@ -13,6 +18,128 @@
13
18
  const ALL_PARTS: OverlayPart[] = ["bubble", "countdown", "toolbar", "saving"];
14
19
  const flags = window as unknown as { __clipsOverlayHostReady?: boolean };
15
20
 
21
+ // ----- Draggable, resizable camera bubble ---------------------------------
22
+ // Size + position persist in storage so the bubble stays where the user put it
23
+ // across pages and recordings (like the desktop app). The iframe can't move or
24
+ // resize itself, so the content script owns its geometry.
25
+ const BUBBLE_SIZES: Record<string, number> = { sm: 184, lg: 280 };
26
+ const bubbleGeom: { size: string; left: number | null; top: number | null } =
27
+ { size: "lg", left: null, top: null };
28
+ let bubbleDragLayer: HTMLDivElement | null = null;
29
+ let bubblePersistTimer: ReturnType<typeof setTimeout> | undefined;
30
+
31
+ function bubbleSizePx(): number {
32
+ return BUBBLE_SIZES[bubbleGeom.size] ?? BUBBLE_SIZES.lg;
33
+ }
34
+
35
+ function clampBubble(
36
+ left: number,
37
+ top: number,
38
+ size: number,
39
+ ): { left: number; top: number } {
40
+ return {
41
+ left: Math.max(8, Math.min(left, window.innerWidth - size - 8)),
42
+ top: Math.max(8, Math.min(top, window.innerHeight - size - 8)),
43
+ };
44
+ }
45
+
46
+ function applyBubbleGeom(): void {
47
+ const frame = document.getElementById(
48
+ partFrameId("bubble"),
49
+ ) as HTMLIFrameElement | null;
50
+ if (!frame) return;
51
+ const size = bubbleSizePx();
52
+ const margin = 24;
53
+ const base = clampBubble(
54
+ bubbleGeom.left ?? margin,
55
+ bubbleGeom.top ?? window.innerHeight - size - margin,
56
+ size,
57
+ );
58
+ Object.assign(frame.style, {
59
+ left: `${base.left}px`,
60
+ top: `${base.top}px`,
61
+ bottom: "auto",
62
+ width: `${size}px`,
63
+ height: `${size}px`,
64
+ });
65
+ }
66
+
67
+ function persistBubbleGeom(): void {
68
+ clearTimeout(bubblePersistTimer);
69
+ bubblePersistTimer = setTimeout(() => {
70
+ try {
71
+ chrome.storage.local.set({ bubbleGeom });
72
+ } catch {
73
+ /* ignore */
74
+ }
75
+ }, 200);
76
+ }
77
+
78
+ function startBubbleDrag(): void {
79
+ if (bubbleDragLayer) return;
80
+ const frame = document.getElementById(
81
+ partFrameId("bubble"),
82
+ ) as HTMLIFrameElement | null;
83
+ if (!frame) return;
84
+ // Full-screen capture layer so the pointer keeps tracking after it leaves
85
+ // the small bubble iframe.
86
+ const layer = document.createElement("div");
87
+ Object.assign(layer.style, {
88
+ position: "fixed",
89
+ inset: "0",
90
+ zIndex: "2147483647",
91
+ cursor: "grabbing",
92
+ });
93
+ (document.documentElement || document.body).appendChild(layer);
94
+ bubbleDragLayer = layer;
95
+ const onMove = (e: PointerEvent): void => {
96
+ const size = bubbleSizePx();
97
+ const rect = frame.getBoundingClientRect();
98
+ const next = clampBubble(
99
+ rect.left + e.movementX,
100
+ rect.top + e.movementY,
101
+ size,
102
+ );
103
+ bubbleGeom.left = next.left;
104
+ bubbleGeom.top = next.top;
105
+ Object.assign(frame.style, {
106
+ left: `${next.left}px`,
107
+ top: `${next.top}px`,
108
+ bottom: "auto",
109
+ });
110
+ };
111
+ const onUp = (): void => {
112
+ layer.removeEventListener("pointermove", onMove);
113
+ layer.removeEventListener("pointerup", onUp);
114
+ layer.removeEventListener("pointercancel", onUp);
115
+ layer.remove();
116
+ bubbleDragLayer = null;
117
+ persistBubbleGeom();
118
+ };
119
+ layer.addEventListener("pointermove", onMove);
120
+ layer.addEventListener("pointerup", onUp);
121
+ layer.addEventListener("pointercancel", onUp);
122
+ }
123
+
124
+ try {
125
+ chrome.storage.local.get("bubbleGeom", (value) => {
126
+ if (chrome.runtime.lastError) return;
127
+ const g = value.bubbleGeom as
128
+ | { size?: unknown; left?: unknown; top?: unknown }
129
+ | undefined;
130
+ if (g && typeof g === "object") {
131
+ bubbleGeom.size = g.size === "sm" ? "sm" : "lg";
132
+ bubbleGeom.left = typeof g.left === "number" ? g.left : null;
133
+ bubbleGeom.top = typeof g.top === "number" ? g.top : null;
134
+ }
135
+ applyBubbleGeom();
136
+ });
137
+ } catch {
138
+ /* ignore */
139
+ }
140
+
141
+ window.addEventListener("resize", () => applyBubbleGeom());
142
+
16
143
  function requestState(): void {
17
144
  try {
18
145
  chrome.runtime.sendMessage(
@@ -72,40 +199,62 @@
72
199
  position: "absolute",
73
200
  border: "none",
74
201
  background: "transparent",
75
- colorScheme: "normal",
202
+ colorScheme: "dark",
76
203
  pointerEvents: "auto",
77
204
  });
78
205
  frame.setAttribute("allowtransparency", "true");
79
206
  if (part === "bubble") {
80
207
  frame.allow = "camera; microphone";
208
+ // Above the countdown so the face stays sharp over the dim/blur. Exact
209
+ // size/position are set by applyBubbleGeom() once mounted.
210
+ const size = bubbleSizePx();
81
211
  Object.assign(frame.style, {
82
212
  left: "24px",
83
213
  bottom: "24px",
84
- width: "148px",
85
- height: "148px",
214
+ width: `${size}px`,
215
+ height: `${size}px`,
216
+ // Clip the IFRAME itself to a circle so the iframe's opaque canvas (which
217
+ // a declared color-scheme always paints, dark or white) can never show as
218
+ // a square box around the bubble.
219
+ borderRadius: "50%",
220
+ overflow: "hidden",
221
+ zIndex: "3",
86
222
  });
87
223
  } else if (part === "toolbar") {
88
224
  // Left-edge vertical pill (desktop layout). Height grows on hover via the
89
- // resize message below.
225
+ // resize message below. Clipped to the pill's radius (the pill fills the
226
+ // iframe) so the opaque canvas can't show as a box; shadow on the iframe so
227
+ // the clip doesn't cut it.
90
228
  Object.assign(frame.style, {
91
229
  left: "16px",
92
- top: "50%",
93
- transform: "translateY(-50%)",
230
+ top: "calc(50% - 77px)",
94
231
  width: "68px",
95
232
  height: "154px",
233
+ borderRadius: "20px",
234
+ overflow: "hidden",
235
+ boxShadow: "0 10px 28px rgba(9, 9, 11, 0.45)",
236
+ zIndex: "2",
96
237
  });
97
238
  } else if (part === "saving") {
239
+ // Compact card: caption + a single indeterminate bar (no circular spinner).
240
+ // Clipped to the card radius (card fills the iframe) so no canvas box shows.
98
241
  Object.assign(frame.style, {
99
242
  left: "24px",
100
243
  bottom: "24px",
101
- width: "264px",
102
- height: "96px",
244
+ width: "240px",
245
+ height: "64px",
246
+ borderRadius: "14px",
247
+ overflow: "hidden",
248
+ boxShadow: "0 10px 28px rgba(9, 9, 11, 0.45)",
249
+ zIndex: "2",
103
250
  });
104
251
  } else {
252
+ // countdown — full-screen dim/blur, below the bubble.
105
253
  Object.assign(frame.style, {
106
254
  inset: "0",
107
255
  width: "100%",
108
256
  height: "100%",
257
+ zIndex: "1",
109
258
  });
110
259
  }
111
260
  }
@@ -114,29 +263,134 @@
114
263
  if (document.getElementById(partFrameId(part))) return;
115
264
  const frame = document.createElement("iframe");
116
265
  frame.id = partFrameId(part);
266
+ if (part === "bubble") frame.allow = "camera; microphone";
117
267
  const url = new URL(chrome.runtime.getURL("src/overlay.html"));
118
268
  url.searchParams.set("part", part);
119
269
  if (part === "countdown") url.searchParams.set("seconds", "3");
120
270
  frame.src = url.toString();
121
271
  styleFrame(frame, part);
122
272
  container.appendChild(frame);
273
+ if (part === "bubble") applyBubbleGeom();
274
+ }
275
+
276
+ // Camera-ready gating + connecting spinner: while the camera connects we show a
277
+ // simple centered spinner and keep the bubble hidden, then reveal the bubble and
278
+ // start the countdown once the feed is live — so the "3" never hangs and there's
279
+ // no half-loaded, un-draggable bubble during the wait. The bubble posts
280
+ // "camera-ready" when its video plays (or fails); a fallback timer proceeds
281
+ // anyway if the camera never connects.
282
+ const CONNECTING_ID = `${CONTAINER_ID}-connecting`;
283
+ let cameraReady = false;
284
+ let countdownDeferred = false;
285
+ let countdownFallbackTimer: ReturnType<typeof setTimeout> | undefined;
286
+
287
+ function showConnecting(container: HTMLDivElement): void {
288
+ if (document.getElementById(CONNECTING_ID)) return;
289
+ const wrap = document.createElement("div");
290
+ wrap.id = CONNECTING_ID;
291
+ Object.assign(wrap.style, {
292
+ position: "absolute",
293
+ inset: "0",
294
+ display: "flex",
295
+ alignItems: "center",
296
+ justifyContent: "center",
297
+ pointerEvents: "none",
298
+ zIndex: "4",
299
+ });
300
+ const chip = document.createElement("div");
301
+ Object.assign(chip.style, {
302
+ display: "flex",
303
+ alignItems: "center",
304
+ justifyContent: "center",
305
+ width: "84px",
306
+ height: "84px",
307
+ borderRadius: "20px",
308
+ background: "rgba(24, 24, 27, 0.92)",
309
+ boxShadow: "0 10px 28px rgba(9, 9, 11, 0.45)",
310
+ });
311
+ const spinner = document.createElement("div");
312
+ Object.assign(spinner.style, {
313
+ width: "40px",
314
+ height: "40px",
315
+ borderRadius: "50%",
316
+ border: "4px solid rgba(255, 255, 255, 0.18)",
317
+ borderTopColor: "rgba(255, 255, 255, 0.92)",
318
+ });
319
+ chip.appendChild(spinner);
320
+ wrap.appendChild(chip);
321
+ container.appendChild(wrap);
322
+ try {
323
+ spinner.animate(
324
+ [{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }],
325
+ { duration: 800, iterations: Infinity },
326
+ );
327
+ } catch {
328
+ /* Web Animations unavailable — a static ring is fine */
329
+ }
330
+ }
331
+
332
+ function hideConnecting(): void {
333
+ document.getElementById(CONNECTING_ID)?.remove();
334
+ }
335
+
336
+ function setBubbleHidden(hidden: boolean): void {
337
+ const frame = document.getElementById(partFrameId("bubble"));
338
+ if (frame) (frame as HTMLElement).style.visibility = hidden ? "hidden" : "";
339
+ }
340
+
341
+ function mountDeferredCountdown(): void {
342
+ countdownDeferred = false;
343
+ clearTimeout(countdownFallbackTimer);
344
+ hideConnecting();
345
+ setBubbleHidden(false);
346
+ const container = document.getElementById(
347
+ CONTAINER_ID,
348
+ ) as HTMLDivElement | null;
349
+ if (container && !document.getElementById(partFrameId("countdown"))) {
350
+ mountPart(container, "countdown");
351
+ }
123
352
  }
124
353
 
125
354
  function reconcile(parts: OverlayPart[]): void {
355
+ console.log("[clips-cs] reconcile parts:", parts, "on", location.href);
126
356
  const wanted = new Set(parts.filter((p) => ALL_PARTS.includes(p)));
357
+ // Leaving the countdown phase (recording / saving / idle): cancel any pending
358
+ // deferred countdown + spinner so a late fallback timer can't pop the "3-2-1"
359
+ // back up over a later overlay (this was the "countdown over the saving card"
360
+ // bug).
361
+ if (!wanted.has("countdown")) {
362
+ countdownDeferred = false;
363
+ clearTimeout(countdownFallbackTimer);
364
+ hideConnecting();
365
+ }
127
366
  if (wanted.size === 0) {
128
367
  document.getElementById(CONTAINER_ID)?.remove();
368
+ cameraReady = false;
129
369
  return;
130
370
  }
131
371
  const container = ensureContainer();
372
+ const gateCountdown =
373
+ wanted.has("countdown") && wanted.has("bubble") && !cameraReady;
132
374
  for (const part of ALL_PARTS) {
133
375
  const existing = document.getElementById(partFrameId(part));
134
376
  if (wanted.has(part)) {
377
+ // Hold the countdown (showing the spinner) until the camera feed is live.
378
+ if (part === "countdown" && gateCountdown) {
379
+ if (!existing && !countdownDeferred) {
380
+ countdownDeferred = true;
381
+ clearTimeout(countdownFallbackTimer);
382
+ countdownFallbackTimer = setTimeout(mountDeferredCountdown, 12000);
383
+ }
384
+ continue;
385
+ }
135
386
  if (!existing) mountPart(container, part);
387
+ // Keep the bubble hidden behind the spinner until the feed is live.
388
+ if (part === "bubble" && gateCountdown) setBubbleHidden(true);
136
389
  } else if (existing) {
137
390
  existing.remove();
138
391
  }
139
392
  }
393
+ if (gateCountdown) showConnecting(container);
140
394
  }
141
395
 
142
396
  // Guard against rare double-injection (SPA soft-reloads re-running the script).
@@ -157,20 +411,43 @@
157
411
  }
158
412
  });
159
413
 
160
- // The toolbar overlay asks to grow/shrink its own iframe on hover (it can't
161
- // resize itself). Only trust messages from our own extension-origin frames.
414
+ // Overlay iframes post layout requests (toolbar hover-resize, bubble drag and
415
+ // size). Only trust messages from our own extension-origin frames.
162
416
  window.addEventListener("message", (event) => {
163
417
  const data = event.data as
164
- | { source?: string; kind?: string; part?: string; height?: number }
418
+ | {
419
+ source?: string;
420
+ kind?: string;
421
+ part?: string;
422
+ height?: number;
423
+ size?: string;
424
+ }
165
425
  | undefined;
166
- if (!data || data.source !== "clips-overlay" || data.kind !== "resize") {
426
+ if (!data || data.source !== "clips-overlay") return;
427
+ if (event.origin !== chrome.runtime.getURL("").replace(/\/$/, "")) return;
428
+
429
+ if (data.kind === "resize" && data.part === "toolbar") {
430
+ const frame = document.getElementById(partFrameId("toolbar"));
431
+ if (frame && typeof data.height === "number") {
432
+ frame.style.height = `${Math.round(data.height)}px`;
433
+ }
434
+ return;
435
+ }
436
+ if (data.kind === "camera-ready") {
437
+ cameraReady = true;
438
+ if (countdownDeferred) mountDeferredCountdown();
439
+ return;
440
+ }
441
+ if (data.kind === "bubble-drag-start") {
442
+ startBubbleDrag();
443
+ return;
444
+ }
445
+ if (data.kind === "bubble-size") {
446
+ bubbleGeom.size = data.size === "sm" ? "sm" : "lg";
447
+ applyBubbleGeom();
448
+ persistBubbleGeom();
167
449
  return;
168
450
  }
169
- if (event.origin !== chrome.runtime.getURL("").replace(/\/$/, "")) return;
170
- const part = data.part === "toolbar" ? "toolbar" : null;
171
- if (!part || typeof data.height !== "number") return;
172
- const frame = document.getElementById(partFrameId(part));
173
- if (frame) frame.style.height = `${Math.round(data.height)}px`;
174
451
  });
175
452
 
176
453
  syncIfRecording();