@agent-native/core 0.70.0 → 0.70.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 (172) hide show
  1. package/README.md +15 -15
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +67 -0
  4. package/corpus/core/docs/design/durable-agent-runs.md +217 -0
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/action.ts +89 -0
  7. package/corpus/core/src/agent/model-config.ts +0 -2
  8. package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
  9. package/corpus/core/src/cli/skills.ts +2 -2
  10. package/corpus/core/src/cli/sync-builder-starter-manifest.ts +275 -0
  11. package/corpus/core/src/client/AgentPanel.tsx +5 -3
  12. package/corpus/core/src/client/AssistantChat.tsx +8 -0
  13. package/corpus/core/src/client/client-surface.ts +41 -0
  14. package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
  15. package/corpus/core/src/client/feedback-context.ts +4 -0
  16. package/corpus/core/src/client/index.ts +1 -0
  17. package/corpus/core/src/client/sse-event-processor.ts +7 -1
  18. package/corpus/core/src/mcp/build-server.ts +57 -1
  19. package/corpus/core/src/mcp/embed-app.ts +86 -2
  20. package/corpus/core/src/mcp/oauth-route.ts +109 -13
  21. package/corpus/core/src/server/core-routes-plugin.ts +78 -0
  22. package/corpus/core/src/server/onboarding-html.ts +35 -0
  23. package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  24. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
  25. package/corpus/templates/analytics/AGENTS.md +8 -1
  26. package/corpus/templates/analytics/actions/install-dashboard-template.ts +97 -2
  27. package/corpus/templates/analytics/actions/update-dashboard.ts +36 -7
  28. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
  29. package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
  30. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -4
  31. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -3
  32. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
  33. package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
  35. package/corpus/templates/assets/actions/generate-image.ts +24 -0
  36. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
  37. package/corpus/templates/assets/app/global.css +17 -1
  38. package/corpus/templates/assets/app/routes/_index.tsx +94 -14
  39. package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
  40. package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
  41. package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
  42. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
  43. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
  44. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
  45. package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
  51. package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
  52. package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
  53. package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
  54. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
  55. package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
  56. package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
  57. package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
  58. package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
  59. package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
  60. package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
  61. package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
  62. package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
  63. package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
  64. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
  65. package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
  66. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
  67. package/corpus/templates/clips/desktop/src/styles.css +70 -2
  68. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +73 -0
  69. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +34 -5
  70. package/corpus/templates/content/AGENTS.md +20 -3
  71. package/corpus/templates/content/actions/_database-utils.ts +15 -0
  72. package/corpus/templates/content/actions/_property-utils.ts +371 -4
  73. package/corpus/templates/content/actions/configure-document-property.ts +35 -1
  74. package/corpus/templates/content/actions/create-content-database.ts +5 -1
  75. package/corpus/templates/content/actions/delete-document-property.ts +52 -2
  76. package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
  77. package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
  78. package/corpus/templates/content/actions/set-document-property.ts +39 -0
  79. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
  80. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
  81. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
  82. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
  83. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
  84. package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
  85. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
  86. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
  87. package/corpus/templates/content/app/global.css +9 -0
  88. package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
  89. package/corpus/templates/content/server/db/schema.ts +30 -0
  90. package/corpus/templates/content/server/plugins/db.ts +87 -0
  91. package/corpus/templates/content/shared/api.ts +7 -0
  92. package/corpus/templates/content/shared/properties.ts +109 -0
  93. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
  94. package/corpus/templates/forms/actions/export-responses.ts +6 -0
  95. package/corpus/templates/forms/actions/list-responses.ts +2 -0
  96. package/corpus/templates/forms/actions/response-insights.ts +2 -0
  97. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
  98. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
  99. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
  100. package/corpus/templates/forms/server/db/schema.ts +7 -0
  101. package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
  102. package/corpus/templates/forms/server/lib/integrations.ts +76 -2
  103. package/corpus/templates/forms/server/plugins/db.ts +20 -0
  104. package/corpus/templates/forms/shared/types.ts +12 -0
  105. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
  106. package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
  107. package/dist/action.js +87 -0
  108. package/dist/action.js.map +1 -1
  109. package/dist/agent/engine/builder-engine.d.ts +1 -1
  110. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  111. package/dist/agent/model-config.d.ts +2 -2
  112. package/dist/agent/model-config.d.ts.map +1 -1
  113. package/dist/agent/model-config.js +0 -2
  114. package/dist/agent/model-config.js.map +1 -1
  115. package/dist/agent/run-loop-with-resume.d.ts +13 -0
  116. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  117. package/dist/agent/run-loop-with-resume.js +44 -0
  118. package/dist/agent/run-loop-with-resume.js.map +1 -1
  119. package/dist/cli/skills.js +2 -2
  120. package/dist/cli/skills.js.map +1 -1
  121. package/dist/cli/sync-builder-starter-manifest.d.ts +32 -0
  122. package/dist/cli/sync-builder-starter-manifest.d.ts.map +1 -0
  123. package/dist/cli/sync-builder-starter-manifest.js +179 -0
  124. package/dist/cli/sync-builder-starter-manifest.js.map +1 -0
  125. package/dist/client/AgentPanel.d.ts.map +1 -1
  126. package/dist/client/AgentPanel.js +5 -3
  127. package/dist/client/AgentPanel.js.map +1 -1
  128. package/dist/client/AssistantChat.d.ts +6 -1
  129. package/dist/client/AssistantChat.d.ts.map +1 -1
  130. package/dist/client/AssistantChat.js +2 -2
  131. package/dist/client/AssistantChat.js.map +1 -1
  132. package/dist/client/client-surface.d.ts +17 -0
  133. package/dist/client/client-surface.d.ts.map +1 -0
  134. package/dist/client/client-surface.js +24 -0
  135. package/dist/client/client-surface.js.map +1 -0
  136. package/dist/client/composer/TiptapComposer.d.ts +26 -1
  137. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  138. package/dist/client/composer/TiptapComposer.js +27 -5
  139. package/dist/client/composer/TiptapComposer.js.map +1 -1
  140. package/dist/client/feedback-context.d.ts +3 -0
  141. package/dist/client/feedback-context.d.ts.map +1 -1
  142. package/dist/client/feedback-context.js +2 -0
  143. package/dist/client/feedback-context.js.map +1 -1
  144. package/dist/client/index.d.ts +1 -0
  145. package/dist/client/index.d.ts.map +1 -1
  146. package/dist/client/index.js +1 -0
  147. package/dist/client/index.js.map +1 -1
  148. package/dist/client/sse-event-processor.d.ts.map +1 -1
  149. package/dist/client/sse-event-processor.js +7 -1
  150. package/dist/client/sse-event-processor.js.map +1 -1
  151. package/dist/mcp/build-server.d.ts +21 -0
  152. package/dist/mcp/build-server.d.ts.map +1 -1
  153. package/dist/mcp/build-server.js +43 -1
  154. package/dist/mcp/build-server.js.map +1 -1
  155. package/dist/mcp/embed-app.d.ts.map +1 -1
  156. package/dist/mcp/embed-app.js +86 -2
  157. package/dist/mcp/embed-app.js.map +1 -1
  158. package/dist/mcp/oauth-route.d.ts.map +1 -1
  159. package/dist/mcp/oauth-route.js +95 -12
  160. package/dist/mcp/oauth-route.js.map +1 -1
  161. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  162. package/dist/server/core-routes-plugin.js +63 -0
  163. package/dist/server/core-routes-plugin.js.map +1 -1
  164. package/dist/server/onboarding-html.d.ts.map +1 -1
  165. package/dist/server/onboarding-html.js +35 -0
  166. package/dist/server/onboarding-html.js.map +1 -1
  167. package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  168. package/docs/design/durable-agent-runs.md +217 -0
  169. package/package.json +1 -1
  170. package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  171. package/corpus/templates/analytics/app/pages/About.tsx +0 -108
  172. package/corpus/templates/analytics/app/routes/about.tsx +0 -9
@@ -0,0 +1,377 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ }
4
+
5
+ * {
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ html,
10
+ body {
11
+ margin: 0;
12
+ padding: 0;
13
+ width: 100%;
14
+ height: 100%;
15
+ background: transparent;
16
+ overflow: hidden;
17
+ font-family:
18
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
19
+ sans-serif;
20
+ }
21
+
22
+ #root {
23
+ width: 100%;
24
+ height: 100%;
25
+ }
26
+
27
+ /* ---------- Camera bubble ---------- */
28
+
29
+ .bubble {
30
+ position: absolute;
31
+ inset: 0;
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ }
36
+
37
+ .bubble-ring {
38
+ width: 100%;
39
+ height: 100%;
40
+ border-radius: 50%;
41
+ overflow: hidden;
42
+ background: #0f172a;
43
+ box-shadow:
44
+ 0 10px 30px rgba(15, 23, 42, 0.35),
45
+ 0 0 0 4px rgba(255, 255, 255, 0.92);
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ }
50
+
51
+ .bubble-ring video {
52
+ width: 100%;
53
+ height: 100%;
54
+ object-fit: cover;
55
+ transform: scaleX(-1);
56
+ }
57
+
58
+ .bubble-empty {
59
+ color: rgba(255, 255, 255, 0.7);
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ }
64
+
65
+ .bubble-empty svg {
66
+ width: 38%;
67
+ height: 38%;
68
+ }
69
+
70
+ /* ---------- Countdown ---------- */
71
+
72
+ .countdown {
73
+ position: absolute;
74
+ inset: 0;
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ background: rgba(2, 6, 23, 0.55);
79
+ backdrop-filter: blur(2px);
80
+ }
81
+
82
+ .countdown-controls {
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ gap: clamp(28px, 8vmin, 72px);
87
+ }
88
+
89
+ .countdown-control {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ flex: none;
94
+ width: clamp(56px, 9vmin, 76px);
95
+ height: clamp(56px, 9vmin, 76px);
96
+ border: 1px solid rgba(255, 255, 255, 0.25);
97
+ border-radius: 999px;
98
+ background: rgba(255, 255, 255, 0.06);
99
+ color: #fff;
100
+ cursor: pointer;
101
+ -webkit-backdrop-filter: blur(6px);
102
+ backdrop-filter: blur(6px);
103
+ transition:
104
+ background 0.15s ease,
105
+ border-color 0.15s ease,
106
+ transform 0.12s ease;
107
+ }
108
+
109
+ .countdown-control:hover {
110
+ background: rgba(255, 255, 255, 0.16);
111
+ border-color: rgba(255, 255, 255, 0.6);
112
+ }
113
+
114
+ .countdown-control:active {
115
+ transform: scale(0.94);
116
+ }
117
+
118
+ .countdown-control svg {
119
+ width: clamp(26px, 4vmin, 32px);
120
+ height: clamp(26px, 4vmin, 32px);
121
+ }
122
+
123
+ .countdown-number {
124
+ min-width: clamp(120px, 26vmin, 280px);
125
+ text-align: center;
126
+ font-size: clamp(96px, 22vmin, 240px);
127
+ font-weight: 700;
128
+ color: #fff;
129
+ text-shadow: 0 6px 30px rgba(2, 6, 23, 0.6);
130
+ line-height: 1;
131
+ animation: countdown-pop 1s ease-out forwards;
132
+ }
133
+
134
+ .countdown-go {
135
+ font-size: clamp(64px, 14vmin, 160px);
136
+ letter-spacing: 0.04em;
137
+ }
138
+
139
+ @keyframes countdown-pop {
140
+ 0% {
141
+ transform: scale(0.45);
142
+ opacity: 0;
143
+ }
144
+ 25% {
145
+ transform: scale(1);
146
+ opacity: 1;
147
+ }
148
+ 70% {
149
+ transform: scale(1);
150
+ opacity: 1;
151
+ }
152
+ 100% {
153
+ transform: scale(1.25);
154
+ opacity: 0;
155
+ }
156
+ }
157
+
158
+ .countdown-hint {
159
+ position: absolute;
160
+ bottom: 14%;
161
+ left: 50%;
162
+ transform: translateX(-50%);
163
+ color: rgba(255, 255, 255, 0.85);
164
+ font-size: 15px;
165
+ font-weight: 500;
166
+ background: rgba(2, 6, 23, 0.45);
167
+ padding: 8px 16px;
168
+ border-radius: 999px;
169
+ }
170
+
171
+ /* ---------- Toolbar (vertical left pill, desktop layout) ---------- */
172
+
173
+ @keyframes rec-pulse {
174
+ 0%,
175
+ 100% {
176
+ opacity: 1;
177
+ }
178
+ 50% {
179
+ opacity: 0.3;
180
+ }
181
+ }
182
+
183
+ .toolbar-v {
184
+ position: absolute;
185
+ top: 50%;
186
+ left: 50%;
187
+ transform: translate(-50%, -50%);
188
+ display: flex;
189
+ flex-direction: column;
190
+ align-items: center;
191
+ gap: 8px;
192
+ padding: 9px;
193
+ border-radius: 20px;
194
+ background: rgba(15, 23, 42, 0.94);
195
+ border: 1px solid rgba(255, 255, 255, 0.1);
196
+ box-shadow: 0 10px 28px rgba(2, 6, 23, 0.45);
197
+ color: #fff;
198
+ -webkit-user-select: none;
199
+ user-select: none;
200
+ }
201
+
202
+ .toolbar-v button {
203
+ border: none;
204
+ cursor: pointer;
205
+ color: inherit;
206
+ display: inline-flex;
207
+ align-items: center;
208
+ justify-content: center;
209
+ transition:
210
+ background 0.12s ease,
211
+ transform 0.1s ease;
212
+ }
213
+
214
+ .toolbar-v button:active {
215
+ transform: scale(0.92);
216
+ }
217
+
218
+ .toolbar-v-stop {
219
+ width: 46px;
220
+ height: 46px;
221
+ border-radius: 50%;
222
+ background: #ef4444;
223
+ }
224
+
225
+ .toolbar-v-stop:hover {
226
+ background: #dc2626;
227
+ }
228
+
229
+ .toolbar-v-stop-square {
230
+ width: 16px;
231
+ height: 16px;
232
+ border-radius: 4px;
233
+ background: #fff;
234
+ display: block;
235
+ }
236
+
237
+ .toolbar-v-time {
238
+ display: flex;
239
+ align-items: center;
240
+ gap: 6px;
241
+ font-variant-numeric: tabular-nums;
242
+ font-size: 13px;
243
+ font-weight: 600;
244
+ color: rgba(255, 255, 255, 0.94);
245
+ }
246
+
247
+ .toolbar-v-dot {
248
+ width: 8px;
249
+ height: 8px;
250
+ border-radius: 50%;
251
+ background: #ef4444;
252
+ animation: rec-pulse 1.4s ease-in-out infinite;
253
+ }
254
+
255
+ .toolbar-v-paused .toolbar-v-dot {
256
+ background: #f59e0b;
257
+ animation: none;
258
+ }
259
+
260
+ .toolbar-v-pause {
261
+ width: 40px;
262
+ height: 40px;
263
+ border-radius: 50%;
264
+ background: rgba(255, 255, 255, 0.1);
265
+ }
266
+
267
+ .toolbar-v-pause:hover {
268
+ background: rgba(255, 255, 255, 0.18);
269
+ }
270
+
271
+ .toolbar-v-pause svg {
272
+ width: 18px;
273
+ height: 18px;
274
+ }
275
+
276
+ .toolbar-v-hover-actions {
277
+ display: flex;
278
+ flex-direction: column;
279
+ align-items: center;
280
+ gap: 8px;
281
+ overflow: hidden;
282
+ max-height: 0;
283
+ opacity: 0;
284
+ transition:
285
+ max-height 0.18s ease,
286
+ opacity 0.18s ease;
287
+ }
288
+
289
+ .toolbar-v:hover .toolbar-v-hover-actions {
290
+ max-height: 120px;
291
+ opacity: 1;
292
+ }
293
+
294
+ .toolbar-v-action {
295
+ width: 40px;
296
+ height: 40px;
297
+ border-radius: 50%;
298
+ background: rgba(255, 255, 255, 0.08);
299
+ }
300
+
301
+ .toolbar-v-action:hover {
302
+ background: rgba(255, 255, 255, 0.16);
303
+ }
304
+
305
+ .toolbar-v-action svg {
306
+ width: 20px;
307
+ height: 20px;
308
+ }
309
+
310
+ .toolbar-v-action-danger:hover {
311
+ background: rgba(239, 68, 68, 0.22);
312
+ color: #fca5a5;
313
+ }
314
+
315
+ /* ---------- Saving card (bottom-left, mirrors desktop Finalizing) ---------- */
316
+
317
+ .saving-card {
318
+ position: absolute;
319
+ inset: 0;
320
+ display: flex;
321
+ flex-direction: column;
322
+ justify-content: center;
323
+ gap: 9px;
324
+ padding: 14px 16px;
325
+ border-radius: 14px;
326
+ background: rgba(15, 23, 42, 0.95);
327
+ border: 1px solid rgba(255, 255, 255, 0.1);
328
+ box-shadow: 0 10px 28px rgba(2, 6, 23, 0.45);
329
+ color: #fff;
330
+ }
331
+
332
+ .saving-card .saving-spinner {
333
+ position: absolute;
334
+ top: 14px;
335
+ right: 14px;
336
+ width: 16px;
337
+ height: 16px;
338
+ border-radius: 50%;
339
+ border: 2px solid rgba(255, 255, 255, 0.25);
340
+ border-top-color: #fff;
341
+ animation: saving-spin 0.8s linear infinite;
342
+ }
343
+
344
+ @keyframes saving-spin {
345
+ to {
346
+ transform: rotate(360deg);
347
+ }
348
+ }
349
+
350
+ .saving-caption {
351
+ font-size: 14px;
352
+ font-weight: 600;
353
+ }
354
+
355
+ .saving-bar {
356
+ height: 5px;
357
+ border-radius: 999px;
358
+ background: rgba(255, 255, 255, 0.14);
359
+ overflow: hidden;
360
+ }
361
+
362
+ .saving-bar-fill {
363
+ height: 100%;
364
+ width: 40%;
365
+ border-radius: 999px;
366
+ background: #6366f1;
367
+ animation: saving-indeterminate 1.1s ease-in-out infinite;
368
+ }
369
+
370
+ @keyframes saving-indeterminate {
371
+ 0% {
372
+ margin-left: -40%;
373
+ }
374
+ 100% {
375
+ margin-left: 100%;
376
+ }
377
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="color-scheme" content="light dark" />
6
+ <title>Clips overlay</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="./overlay.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,330 @@
1
+ import "./overlay.css";
2
+
3
+ // The overlay runs as an extension-origin iframe injected into the page by the
4
+ // content script. Each iframe renders one "part" of the Loom-style recording UI
5
+ // (camera bubble, countdown, or control toolbar) selected via ?part=. Running at
6
+ // the chrome-extension:// origin gives us a persistent camera permission grant,
7
+ // CSS isolation from the host page, and direct chrome.runtime messaging with the
8
+ // background service worker.
9
+
10
+ type OverlayPhase = "idle" | "countdown" | "recording" | "paused" | "saving";
11
+
12
+ // Hover-expand heights for the vertical toolbar, posted to the content script
13
+ // which owns the iframe size (the iframe can't resize itself).
14
+ const TOOLBAR_COLLAPSED_H = 154;
15
+ const TOOLBAR_EXPANDED_H = 236;
16
+
17
+ function postToolbarSize(height: number): void {
18
+ try {
19
+ window.parent.postMessage(
20
+ { source: "clips-overlay", kind: "resize", part: "toolbar", height },
21
+ "*",
22
+ );
23
+ } catch {
24
+ /* parent gone */
25
+ }
26
+ }
27
+
28
+ type OverlayState = {
29
+ phase: OverlayPhase;
30
+ baseElapsedMs: number;
31
+ baseEpochMs: number;
32
+ countdownEndsAtMs: number;
33
+ };
34
+
35
+ const COUNTDOWN_FALLBACK = 3;
36
+ const params = new URLSearchParams(location.search);
37
+ const part = params.get("part");
38
+ const root = document.getElementById("root") as HTMLDivElement;
39
+
40
+ function icon(paths: string): string {
41
+ return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
42
+ }
43
+
44
+ const ICONS = {
45
+ pause: icon(
46
+ '<rect x="6" y="5" width="4" height="14" rx="1" fill="currentColor" stroke="none"/><rect x="14" y="5" width="4" height="14" rx="1" fill="currentColor" stroke="none"/>',
47
+ ),
48
+ resume: icon('<path d="M7 5l12 7-12 7z" fill="currentColor" stroke="none"/>'),
49
+ stop: icon(
50
+ '<rect x="6" y="6" width="12" height="12" rx="2.5" fill="currentColor" stroke="none"/>',
51
+ ),
52
+ restart: icon('<path d="M3 11a9 9 0 1 1 2.6 6.3"/><path d="M3 4v7h7"/>'),
53
+ trash: icon(
54
+ '<path d="M4 7h16"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M5 7l1 13a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-13"/><path d="M9 7V4h6v3"/>',
55
+ ),
56
+ cameraOff: icon(
57
+ '<path d="M2 2l20 20"/><path d="M7 7H4a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12"/><path d="M22 8l-6 4 6 4V8z" opacity="0.5"/>',
58
+ ),
59
+ cancel: icon('<path d="M6 6l12 12"/><path d="M18 6L6 18"/>'),
60
+ skipForward: icon(
61
+ '<path d="M6 5l10 7-10 7z" fill="currentColor" stroke="none"/><rect x="16" y="5" width="3" height="14" rx="1" fill="currentColor" stroke="none"/>',
62
+ ),
63
+ };
64
+
65
+ function send(type: string, extra: Record<string, unknown> = {}): void {
66
+ try {
67
+ chrome.runtime.sendMessage(
68
+ { type, ...extra },
69
+ () => void chrome.runtime.lastError,
70
+ );
71
+ } catch {
72
+ /* the background may be momentarily asleep; state will re-sync */
73
+ }
74
+ }
75
+
76
+ /* ---------------------------------------------------------------- bubble --- */
77
+
78
+ async function initBubble(): Promise<void> {
79
+ const bubble = document.createElement("div");
80
+ bubble.className = "bubble";
81
+ const ring = document.createElement("div");
82
+ ring.className = "bubble-ring";
83
+ bubble.appendChild(ring);
84
+ root.appendChild(bubble);
85
+
86
+ try {
87
+ const stream = await navigator.mediaDevices.getUserMedia({
88
+ video: {
89
+ width: { ideal: 640 },
90
+ height: { ideal: 640 },
91
+ facingMode: "user",
92
+ },
93
+ audio: false,
94
+ });
95
+ const video = document.createElement("video");
96
+ video.muted = true;
97
+ video.autoplay = true;
98
+ video.playsInline = true;
99
+ video.srcObject = stream;
100
+ ring.appendChild(video);
101
+ await video.play().catch(() => undefined);
102
+ } catch {
103
+ const empty = document.createElement("div");
104
+ empty.className = "bubble-empty";
105
+ empty.innerHTML = ICONS.cameraOff;
106
+ ring.appendChild(empty);
107
+ }
108
+ }
109
+
110
+ /* ------------------------------------------------------------- countdown --- */
111
+
112
+ // The countdown only *visualizes* the worker's clock (state.countdownEndsAtMs).
113
+ // The worker owns the real timer and starts the recorder, so this never *needs*
114
+ // to signal "done" — which is what lets recording work on pages where no overlay
115
+ // can be injected at all. The skip button is the one exception: it explicitly
116
+ // asks the worker to start now (CLIPS_OVERLAY_COUNTDOWN_DONE → beginNow), which
117
+ // is idempotent and harmless if the timer also fires.
118
+ function initCountdown(): void {
119
+ const wrap = document.createElement("div");
120
+ wrap.className = "countdown";
121
+
122
+ const controls = document.createElement("div");
123
+ controls.className = "countdown-controls";
124
+
125
+ const cancelBtn = document.createElement("button");
126
+ cancelBtn.type = "button";
127
+ cancelBtn.className = "countdown-control countdown-control-cancel";
128
+ cancelBtn.setAttribute("aria-label", "Cancel recording");
129
+ cancelBtn.innerHTML = ICONS.cancel;
130
+ cancelBtn.addEventListener("click", () => send("CLIPS_OVERLAY_CANCEL"));
131
+
132
+ const number = document.createElement("div");
133
+ number.className = "countdown-number";
134
+
135
+ const skipBtn = document.createElement("button");
136
+ skipBtn.type = "button";
137
+ skipBtn.className = "countdown-control countdown-control-skip";
138
+ skipBtn.setAttribute("aria-label", "Skip countdown and start recording now");
139
+ skipBtn.innerHTML = ICONS.skipForward;
140
+ skipBtn.addEventListener("click", () => send("CLIPS_OVERLAY_COUNTDOWN_DONE"));
141
+
142
+ controls.append(cancelBtn, number, skipBtn);
143
+
144
+ const hint = document.createElement("div");
145
+ hint.className = "countdown-hint";
146
+ hint.textContent = "Get ready…";
147
+ wrap.append(controls, hint);
148
+ root.appendChild(wrap);
149
+
150
+ let lastShown = "";
151
+ const render = (): void => {
152
+ const fallback =
153
+ Number(params.get("seconds") || String(COUNTDOWN_FALLBACK)) ||
154
+ COUNTDOWN_FALLBACK;
155
+ const endsAt =
156
+ state.countdownEndsAtMs > 0
157
+ ? state.countdownEndsAtMs
158
+ : Date.now() + fallback * 1000;
159
+ const remainingMs = endsAt - Date.now();
160
+ if (state.phase !== "countdown" || remainingMs <= 0) {
161
+ if (lastShown !== "Go") {
162
+ number.textContent = "Go";
163
+ number.classList.add("countdown-go");
164
+ lastShown = "Go";
165
+ }
166
+ return;
167
+ }
168
+ const text = String(Math.ceil(remainingMs / 1000));
169
+ if (text !== lastShown) {
170
+ number.textContent = text;
171
+ number.classList.remove("countdown-go");
172
+ number.style.animation = "none";
173
+ void number.offsetWidth;
174
+ number.style.animation = "";
175
+ lastShown = text;
176
+ }
177
+ };
178
+ countdownRender = render;
179
+ window.setInterval(render, 100);
180
+ render();
181
+ }
182
+
183
+ /* --------------------------------------------------------------- toolbar --- */
184
+
185
+ // Vertical pill anchored to the LEFT edge — mirrors the desktop app's toolbar.
186
+ // Big Stop on top, elapsed time, pause; on hover it grows to reveal restart +
187
+ // cancel. Pure command emitter; the background owns the recorder.
188
+ function initToolbar(): void {
189
+ const pill = document.createElement("div");
190
+ pill.className = "toolbar-v";
191
+
192
+ const makeBtn = (
193
+ cls: string,
194
+ title: string,
195
+ svg: string,
196
+ onClick: () => void,
197
+ ): HTMLButtonElement => {
198
+ const btn = document.createElement("button");
199
+ btn.type = "button";
200
+ btn.className = cls;
201
+ btn.title = title;
202
+ btn.setAttribute("aria-label", title);
203
+ btn.innerHTML = svg;
204
+ btn.addEventListener("click", onClick);
205
+ return btn;
206
+ };
207
+
208
+ const stopBtn = makeBtn(
209
+ "toolbar-v-stop",
210
+ "Stop & save",
211
+ '<span class="toolbar-v-stop-square"></span>',
212
+ () => send("CLIPS_OVERLAY_STOP"),
213
+ );
214
+
215
+ const time = document.createElement("div");
216
+ time.className = "toolbar-v-time";
217
+ const dot = document.createElement("span");
218
+ dot.className = "toolbar-v-dot";
219
+ const clock = document.createElement("span");
220
+ clock.textContent = "0:00";
221
+ time.append(dot, clock);
222
+
223
+ const pauseBtn = makeBtn("toolbar-v-pause", "Pause", ICONS.pause, () => {
224
+ if (state.phase === "paused") send("CLIPS_OVERLAY_RESUME");
225
+ else send("CLIPS_OVERLAY_PAUSE");
226
+ });
227
+
228
+ const hoverGroup = document.createElement("div");
229
+ hoverGroup.className = "toolbar-v-hover-actions";
230
+ const restartBtn = makeBtn("toolbar-v-action", "Restart", ICONS.restart, () =>
231
+ send("CLIPS_OVERLAY_RESTART"),
232
+ );
233
+ const cancelBtn = makeBtn(
234
+ "toolbar-v-action toolbar-v-action-danger",
235
+ "Discard",
236
+ ICONS.trash,
237
+ () => send("CLIPS_OVERLAY_CANCEL"),
238
+ );
239
+ hoverGroup.append(restartBtn, cancelBtn);
240
+
241
+ pill.append(stopBtn, time, pauseBtn, hoverGroup);
242
+ root.appendChild(pill);
243
+
244
+ // The iframe can't size itself, so ask the content script to grow/shrink it.
245
+ pill.addEventListener("mouseenter", () =>
246
+ postToolbarSize(TOOLBAR_EXPANDED_H),
247
+ );
248
+ pill.addEventListener("mouseleave", () =>
249
+ postToolbarSize(TOOLBAR_COLLAPSED_H),
250
+ );
251
+
252
+ const render = (): void => {
253
+ const paused = state.phase === "paused";
254
+ pill.classList.toggle("toolbar-v-paused", paused);
255
+ pauseBtn.title = paused ? "Resume" : "Pause";
256
+ pauseBtn.innerHTML = paused ? ICONS.resume : ICONS.pause;
257
+ const elapsed = paused
258
+ ? state.baseElapsedMs
259
+ : state.baseElapsedMs + Math.max(0, Date.now() - state.baseEpochMs);
260
+ clock.textContent = formatDuration(elapsed);
261
+ };
262
+
263
+ window.setInterval(render, 250);
264
+ toolbarRender = render;
265
+ render();
266
+ }
267
+
268
+ /* ---------------------------------------------------------------- saving --- */
269
+
270
+ // Bottom-left "Saving…" card shown from Stop until the clip opens, mirroring the
271
+ // desktop Finalizing overlay so the upload gap isn't a blank screen.
272
+ function initSaving(): void {
273
+ const card = document.createElement("div");
274
+ card.className = "saving-card";
275
+ const spinner = document.createElement("div");
276
+ spinner.className = "saving-spinner";
277
+ const caption = document.createElement("div");
278
+ caption.className = "saving-caption";
279
+ caption.textContent = "Saving clip…";
280
+ const bar = document.createElement("div");
281
+ bar.className = "saving-bar";
282
+ const fill = document.createElement("div");
283
+ fill.className = "saving-bar-fill";
284
+ bar.appendChild(fill);
285
+ card.append(spinner, caption, bar);
286
+ root.appendChild(card);
287
+ }
288
+
289
+ function formatDuration(ms: number): string {
290
+ const total = Math.max(0, Math.floor(ms / 1000));
291
+ const minutes = Math.floor(total / 60);
292
+ const seconds = total % 60;
293
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
294
+ }
295
+
296
+ /* ----------------------------------------------------------------- state --- */
297
+
298
+ const state: OverlayState = {
299
+ phase: "recording",
300
+ baseElapsedMs: 0,
301
+ baseEpochMs: Date.now(),
302
+ countdownEndsAtMs: 0,
303
+ };
304
+ let toolbarRender: (() => void) | null = null;
305
+ let countdownRender: (() => void) | null = null;
306
+
307
+ chrome.runtime.onMessage.addListener((message) => {
308
+ if (!message || typeof message !== "object") return;
309
+ if ((message as { type?: unknown }).type !== "CLIPS_OVERLAY_STATE") return;
310
+ const next = (message as { state?: Partial<OverlayState> }).state;
311
+ if (!next) return;
312
+ if (typeof next.phase === "string") state.phase = next.phase as OverlayPhase;
313
+ if (typeof next.baseElapsedMs === "number")
314
+ state.baseElapsedMs = next.baseElapsedMs;
315
+ if (typeof next.baseEpochMs === "number")
316
+ state.baseEpochMs = next.baseEpochMs;
317
+ if (typeof next.countdownEndsAtMs === "number")
318
+ state.countdownEndsAtMs = next.countdownEndsAtMs;
319
+ toolbarRender?.();
320
+ countdownRender?.();
321
+ });
322
+
323
+ if (part === "bubble") void initBubble();
324
+ else if (part === "countdown") initCountdown();
325
+ else if (part === "toolbar") initToolbar();
326
+ else if (part === "saving") initSaving();
327
+
328
+ // Ask the background for the current state so a freshly-injected toolbar (e.g.
329
+ // after the user navigated to a new page mid-recording) shows the right timer.
330
+ send("CLIPS_OVERLAY_HELLO", { part });