@agent-native/core 0.70.0 → 0.70.1

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 (167) hide show
  1. package/README.md +15 -15
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +61 -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/client/AgentPanel.tsx +5 -3
  11. package/corpus/core/src/client/AssistantChat.tsx +8 -0
  12. package/corpus/core/src/client/client-surface.ts +41 -0
  13. package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
  14. package/corpus/core/src/client/feedback-context.ts +4 -0
  15. package/corpus/core/src/client/index.ts +1 -0
  16. package/corpus/core/src/client/sse-event-processor.ts +7 -1
  17. package/corpus/core/src/mcp/build-server.ts +57 -1
  18. package/corpus/core/src/mcp/embed-app.ts +86 -2
  19. package/corpus/core/src/mcp/oauth-route.ts +109 -13
  20. package/corpus/core/src/server/core-routes-plugin.ts +78 -0
  21. package/corpus/core/src/server/onboarding-html.ts +35 -0
  22. package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  23. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
  24. package/corpus/templates/analytics/AGENTS.md +8 -1
  25. package/corpus/templates/analytics/actions/install-dashboard-template.ts +97 -2
  26. package/corpus/templates/analytics/actions/update-dashboard.ts +36 -7
  27. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
  28. package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
  29. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -4
  30. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -3
  31. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
  32. package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
  34. package/corpus/templates/assets/actions/generate-image.ts +24 -0
  35. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
  36. package/corpus/templates/assets/app/global.css +17 -1
  37. package/corpus/templates/assets/app/routes/_index.tsx +94 -14
  38. package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
  39. package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
  40. package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
  41. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
  42. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
  43. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
  44. package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
  50. package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
  51. package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
  52. package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
  53. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
  54. package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
  55. package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
  56. package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
  57. package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
  58. package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
  59. package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
  60. package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
  61. package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
  62. package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
  63. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
  64. package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
  65. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
  66. package/corpus/templates/clips/desktop/src/styles.css +70 -2
  67. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +73 -0
  68. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +34 -5
  69. package/corpus/templates/content/AGENTS.md +20 -3
  70. package/corpus/templates/content/actions/_database-utils.ts +15 -0
  71. package/corpus/templates/content/actions/_property-utils.ts +371 -4
  72. package/corpus/templates/content/actions/configure-document-property.ts +35 -1
  73. package/corpus/templates/content/actions/create-content-database.ts +5 -1
  74. package/corpus/templates/content/actions/delete-document-property.ts +52 -2
  75. package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
  76. package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
  77. package/corpus/templates/content/actions/set-document-property.ts +39 -0
  78. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
  79. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
  80. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
  81. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
  82. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
  83. package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
  84. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
  85. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
  86. package/corpus/templates/content/app/global.css +9 -0
  87. package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
  88. package/corpus/templates/content/server/db/schema.ts +30 -0
  89. package/corpus/templates/content/server/plugins/db.ts +87 -0
  90. package/corpus/templates/content/shared/api.ts +7 -0
  91. package/corpus/templates/content/shared/properties.ts +109 -0
  92. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
  93. package/corpus/templates/forms/actions/export-responses.ts +6 -0
  94. package/corpus/templates/forms/actions/list-responses.ts +2 -0
  95. package/corpus/templates/forms/actions/response-insights.ts +2 -0
  96. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
  97. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
  98. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
  99. package/corpus/templates/forms/server/db/schema.ts +7 -0
  100. package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
  101. package/corpus/templates/forms/server/lib/integrations.ts +76 -2
  102. package/corpus/templates/forms/server/plugins/db.ts +20 -0
  103. package/corpus/templates/forms/shared/types.ts +12 -0
  104. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
  105. package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
  106. package/dist/action.js +87 -0
  107. package/dist/action.js.map +1 -1
  108. package/dist/agent/engine/builder-engine.d.ts +1 -1
  109. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  110. package/dist/agent/model-config.d.ts +2 -2
  111. package/dist/agent/model-config.d.ts.map +1 -1
  112. package/dist/agent/model-config.js +0 -2
  113. package/dist/agent/model-config.js.map +1 -1
  114. package/dist/agent/run-loop-with-resume.d.ts +13 -0
  115. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  116. package/dist/agent/run-loop-with-resume.js +44 -0
  117. package/dist/agent/run-loop-with-resume.js.map +1 -1
  118. package/dist/cli/skills.js +2 -2
  119. package/dist/cli/skills.js.map +1 -1
  120. package/dist/client/AgentPanel.d.ts.map +1 -1
  121. package/dist/client/AgentPanel.js +5 -3
  122. package/dist/client/AgentPanel.js.map +1 -1
  123. package/dist/client/AssistantChat.d.ts +6 -1
  124. package/dist/client/AssistantChat.d.ts.map +1 -1
  125. package/dist/client/AssistantChat.js +2 -2
  126. package/dist/client/AssistantChat.js.map +1 -1
  127. package/dist/client/client-surface.d.ts +17 -0
  128. package/dist/client/client-surface.d.ts.map +1 -0
  129. package/dist/client/client-surface.js +24 -0
  130. package/dist/client/client-surface.js.map +1 -0
  131. package/dist/client/composer/TiptapComposer.d.ts +26 -1
  132. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  133. package/dist/client/composer/TiptapComposer.js +27 -5
  134. package/dist/client/composer/TiptapComposer.js.map +1 -1
  135. package/dist/client/feedback-context.d.ts +3 -0
  136. package/dist/client/feedback-context.d.ts.map +1 -1
  137. package/dist/client/feedback-context.js +2 -0
  138. package/dist/client/feedback-context.js.map +1 -1
  139. package/dist/client/index.d.ts +1 -0
  140. package/dist/client/index.d.ts.map +1 -1
  141. package/dist/client/index.js +1 -0
  142. package/dist/client/index.js.map +1 -1
  143. package/dist/client/sse-event-processor.d.ts.map +1 -1
  144. package/dist/client/sse-event-processor.js +7 -1
  145. package/dist/client/sse-event-processor.js.map +1 -1
  146. package/dist/mcp/build-server.d.ts +21 -0
  147. package/dist/mcp/build-server.d.ts.map +1 -1
  148. package/dist/mcp/build-server.js +43 -1
  149. package/dist/mcp/build-server.js.map +1 -1
  150. package/dist/mcp/embed-app.d.ts.map +1 -1
  151. package/dist/mcp/embed-app.js +86 -2
  152. package/dist/mcp/embed-app.js.map +1 -1
  153. package/dist/mcp/oauth-route.d.ts.map +1 -1
  154. package/dist/mcp/oauth-route.js +95 -12
  155. package/dist/mcp/oauth-route.js.map +1 -1
  156. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  157. package/dist/server/core-routes-plugin.js +63 -0
  158. package/dist/server/core-routes-plugin.js.map +1 -1
  159. package/dist/server/onboarding-html.d.ts.map +1 -1
  160. package/dist/server/onboarding-html.js +35 -0
  161. package/dist/server/onboarding-html.js.map +1 -1
  162. package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  163. package/docs/design/durable-agent-runs.md +217 -0
  164. package/package.json +1 -1
  165. package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  166. package/corpus/templates/analytics/app/pages/About.tsx +0 -108
  167. package/corpus/templates/analytics/app/routes/about.tsx +0 -9
@@ -0,0 +1,227 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Enable camera & microphone — Clips</title>
7
+ <style>
8
+ /* Design tokens mirrored from the popup/options styles.css so this page
9
+ matches the rest of the extension and the Clips app. */
10
+ :root {
11
+ color-scheme: light dark;
12
+ --bg: #ffffff;
13
+ --surface: #f5f6f9;
14
+ --surface-hover: #eef0f5;
15
+ --fg: #0f172a;
16
+ --fg-muted: #64748b;
17
+ --fg-subtle: #94a3b8;
18
+ --border: hsl(220 10% 90%);
19
+ --border-strong: #d4d9e4;
20
+ --brand: #18181b;
21
+ --brand-hover: #27272a;
22
+ --brand-ring: rgba(24, 24, 27, 0.28);
23
+ --status-ok: #22c55e;
24
+ --danger: #b42318;
25
+ --radius: 12px;
26
+ --radius-sm: 8px;
27
+ --shadow-md:
28
+ 0 4px 16px rgba(15, 23, 42, 0.08), 0 2px 4px rgba(15, 23, 42, 0.04);
29
+ font-family:
30
+ -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto,
31
+ ui-sans-serif, system-ui, sans-serif;
32
+ font-feature-settings: "cv02", "cv03", "cv04", "cv11";
33
+ }
34
+ @media (prefers-color-scheme: dark) {
35
+ :root {
36
+ --bg: #0f1115;
37
+ --surface: #181b23;
38
+ --surface-hover: #1e2230;
39
+ --fg: #f1f5f9;
40
+ --fg-muted: #94a3b8;
41
+ --fg-subtle: #64748b;
42
+ --border: #252938;
43
+ --border-strong: #323747;
44
+ --brand: #f4f4f5;
45
+ --brand-hover: #e4e4e7;
46
+ --brand-ring: rgba(244, 244, 245, 0.28);
47
+ --danger: hsl(6 88% 70%);
48
+ --shadow-md:
49
+ 0 8px 24px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
50
+ }
51
+ }
52
+ * {
53
+ box-sizing: border-box;
54
+ }
55
+ body {
56
+ margin: 0;
57
+ min-height: 100vh;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ padding: 24px;
62
+ background: var(--bg);
63
+ color: var(--fg);
64
+ -webkit-font-smoothing: antialiased;
65
+ -moz-osx-font-smoothing: grayscale;
66
+ font-size: 14px;
67
+ line-height: 1.45;
68
+ }
69
+ .card {
70
+ width: 100%;
71
+ max-width: 420px;
72
+ background: var(--bg);
73
+ border: 1px solid var(--border);
74
+ border-radius: var(--radius);
75
+ box-shadow: var(--shadow-md);
76
+ padding: 28px;
77
+ }
78
+ .glyph {
79
+ width: 44px;
80
+ height: 44px;
81
+ border-radius: var(--radius-sm);
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ background: var(--surface);
86
+ border: 1px solid var(--border);
87
+ color: var(--fg);
88
+ margin-bottom: 18px;
89
+ }
90
+ .glyph svg {
91
+ width: 22px;
92
+ height: 22px;
93
+ display: block;
94
+ }
95
+ h1 {
96
+ font-size: 18px;
97
+ font-weight: 600;
98
+ margin: 0 0 8px;
99
+ letter-spacing: -0.01em;
100
+ }
101
+ p {
102
+ font-size: 13.5px;
103
+ color: var(--fg-muted);
104
+ margin: 0 0 20px;
105
+ }
106
+ .rows {
107
+ display: grid;
108
+ gap: 8px;
109
+ margin: 0 0 22px;
110
+ }
111
+ .row {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 11px;
115
+ padding: 11px 12px;
116
+ border: 1px solid var(--border);
117
+ border-radius: var(--radius-sm);
118
+ background: var(--surface);
119
+ font-size: 13px;
120
+ color: var(--fg);
121
+ }
122
+ .row .check {
123
+ width: 20px;
124
+ height: 20px;
125
+ border-radius: 50%;
126
+ border: 1.5px solid var(--border-strong);
127
+ display: inline-flex;
128
+ align-items: center;
129
+ justify-content: center;
130
+ font-size: 12px;
131
+ color: var(--fg-subtle);
132
+ flex: none;
133
+ }
134
+ .row.granted {
135
+ border-color: var(--status-ok);
136
+ }
137
+ .row.granted .check {
138
+ border-color: var(--status-ok);
139
+ background: rgba(34, 197, 94, 0.14);
140
+ color: var(--status-ok);
141
+ }
142
+ .row-sub {
143
+ color: var(--fg-subtle);
144
+ font-size: 12px;
145
+ }
146
+ button {
147
+ font: inherit;
148
+ appearance: none;
149
+ border: 0;
150
+ border-radius: var(--radius-sm);
151
+ background: var(--brand);
152
+ color: var(--bg);
153
+ font-size: 14px;
154
+ font-weight: 600;
155
+ padding: 11px 18px;
156
+ width: 100%;
157
+ cursor: pointer;
158
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
159
+ transition:
160
+ background 120ms,
161
+ transform 80ms;
162
+ }
163
+ button:hover:not(:disabled) {
164
+ background: var(--brand-hover);
165
+ }
166
+ button:active:not(:disabled) {
167
+ transform: translateY(0.5px);
168
+ }
169
+ button:disabled {
170
+ opacity: 0.55;
171
+ cursor: default;
172
+ }
173
+ .status {
174
+ margin-top: 14px;
175
+ font-size: 12.5px;
176
+ min-height: 16px;
177
+ color: var(--fg-muted);
178
+ text-align: center;
179
+ }
180
+ .status.error {
181
+ color: var(--danger);
182
+ }
183
+ </style>
184
+ </head>
185
+ <body>
186
+ <div class="card">
187
+ <div class="glyph">
188
+ <svg
189
+ viewBox="0 0 24 24"
190
+ fill="none"
191
+ stroke="currentColor"
192
+ stroke-width="2"
193
+ stroke-linecap="round"
194
+ stroke-linejoin="round"
195
+ >
196
+ <path d="M23 7l-7 5 7 5V7z" />
197
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
198
+ </svg>
199
+ </div>
200
+ <h1>Enable camera &amp; microphone</h1>
201
+ <p>
202
+ Clips needs access once so it can show your camera bubble and record
203
+ your voice. Grant it below — Chrome still asks before each recording
204
+ starts.
205
+ </p>
206
+ <div class="rows">
207
+ <div class="row" id="row-mic">
208
+ <span class="check" id="check-mic">●</span>
209
+ <span
210
+ >Microphone
211
+ <span class="row-sub">— record your narration</span></span
212
+ >
213
+ </div>
214
+ <div class="row" id="row-cam">
215
+ <span class="check" id="check-cam">●</span>
216
+ <span
217
+ >Camera
218
+ <span class="row-sub">— show your face in recordings</span></span
219
+ >
220
+ </div>
221
+ </div>
222
+ <button id="enable">Enable camera &amp; microphone</button>
223
+ <div class="status" id="status"></div>
224
+ </div>
225
+ <script type="module" src="./permission.ts"></script>
226
+ </body>
227
+ </html>
@@ -0,0 +1,105 @@
1
+ // Camera/microphone permission onboarding. Opened in a tab when the user hits
2
+ // Record without having granted access yet. Requesting getUserMedia from a real
3
+ // extension page (not the headless offscreen document, not a focus-stealing
4
+ // popup) is what makes Chrome show the standard permission dialog and persist
5
+ // the grant for the whole chrome-extension:// origin — so the offscreen recorder
6
+ // (mic) and the camera-bubble iframe both work afterward.
7
+
8
+ const enableBtn = document.getElementById("enable") as HTMLButtonElement;
9
+ const statusEl = document.getElementById("status") as HTMLDivElement;
10
+ const rowMic = document.getElementById("row-mic") as HTMLDivElement;
11
+ const rowCam = document.getElementById("row-cam") as HTMLDivElement;
12
+ const checkMic = document.getElementById("check-mic") as HTMLSpanElement;
13
+ const checkCam = document.getElementById("check-cam") as HTMLSpanElement;
14
+
15
+ const CHECK = "✓";
16
+ const DOT = "●";
17
+
18
+ function setStatus(text: string, isError = false): void {
19
+ statusEl.textContent = text;
20
+ statusEl.classList.toggle("error", isError);
21
+ }
22
+
23
+ function markRow(kind: "mic" | "cam", granted: boolean): void {
24
+ const row = kind === "mic" ? rowMic : rowCam;
25
+ const check = kind === "mic" ? checkMic : checkCam;
26
+ row.classList.toggle("granted", granted);
27
+ check.textContent = granted ? CHECK : DOT;
28
+ }
29
+
30
+ async function permissionState(
31
+ name: "camera" | "microphone",
32
+ ): Promise<PermissionState | "unknown"> {
33
+ try {
34
+ const status = await navigator.permissions.query({
35
+ name: name as PermissionName,
36
+ });
37
+ return status.state;
38
+ } catch {
39
+ return "unknown";
40
+ }
41
+ }
42
+
43
+ async function requestOne(kind: "mic" | "cam"): Promise<boolean> {
44
+ try {
45
+ const constraints: MediaStreamConstraints =
46
+ kind === "mic" ? { audio: true } : { video: true };
47
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
48
+ for (const track of stream.getTracks()) track.stop();
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ function finish(camOk: boolean, micOk: boolean): void {
56
+ void chrome.storage.local.set({
57
+ clipsMediaPermission: { camera: camOk, microphone: micOk },
58
+ });
59
+ markRow("mic", micOk);
60
+ markRow("cam", camOk);
61
+ if (camOk || micOk) {
62
+ enableBtn.textContent = "All set — you can close this tab";
63
+ enableBtn.disabled = true;
64
+ setStatus(
65
+ camOk && micOk
66
+ ? "Camera and microphone are ready. Click the Clips icon to record."
67
+ : "Saved. Click the Clips icon to record.",
68
+ );
69
+ } else {
70
+ enableBtn.disabled = false;
71
+ enableBtn.textContent = "Try again";
72
+ setStatus(
73
+ "Access was blocked. Click the camera icon in Chrome's address bar to allow it, then try again.",
74
+ true,
75
+ );
76
+ }
77
+ }
78
+
79
+ async function enable(): Promise<void> {
80
+ enableBtn.disabled = true;
81
+ setStatus("Waiting for Chrome's permission prompt…");
82
+ // Request separately so a camera denial doesn't also block the microphone.
83
+ const micOk = await requestOne("mic");
84
+ const camOk = await requestOne("cam");
85
+ finish(camOk, micOk);
86
+ }
87
+
88
+ enableBtn.addEventListener("click", () => void enable());
89
+
90
+ // If both are already granted (returning here later), reflect that immediately.
91
+ void (async () => {
92
+ const [cam, mic] = await Promise.all([
93
+ permissionState("camera"),
94
+ permissionState("microphone"),
95
+ ]);
96
+ markRow("mic", mic === "granted");
97
+ markRow("cam", cam === "granted");
98
+ if (cam === "granted" && mic === "granted") {
99
+ enableBtn.textContent = "Already enabled — you can close this tab";
100
+ enableBtn.disabled = true;
101
+ setStatus(
102
+ "Camera and microphone are ready. Click the Clips icon to record.",
103
+ );
104
+ }
105
+ })();
@@ -72,6 +72,7 @@
72
72
  aria-checked="false"
73
73
  data-mode="screen"
74
74
  aria-label="Screen only"
75
+ data-tooltip="Screen only"
75
76
  >
76
77
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
77
78
  <rect
@@ -98,6 +99,7 @@
98
99
  aria-checked="true"
99
100
  data-mode="screen-camera"
100
101
  aria-label="Screen and camera"
102
+ data-tooltip="Screen and camera"
101
103
  >
102
104
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
103
105
  <rect
@@ -126,6 +128,7 @@
126
128
  aria-checked="false"
127
129
  data-mode="camera"
128
130
  aria-label="Camera only"
131
+ data-tooltip="Camera only"
129
132
  >
130
133
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
131
134
  <rect
@@ -203,7 +203,7 @@ function readStoredAuth(
203
203
  settings: ExtensionSettings,
204
204
  ): Promise<StoredAuth | null> {
205
205
  return new Promise((resolve) => {
206
- chrome.storage.session.get("clipsAuth", (value) => {
206
+ chrome.storage.local.get("clipsAuth", (value) => {
207
207
  const auth = value.clipsAuth as Partial<StoredAuth> | undefined;
208
208
  if (
209
209
  auth &&
@@ -228,7 +228,7 @@ function readStoredAuth(
228
228
 
229
229
  function clearStoredAuth(): Promise<void> {
230
230
  return new Promise((resolve) => {
231
- chrome.storage.session.remove("clipsAuth", () => resolve());
231
+ chrome.storage.local.remove("clipsAuth", () => resolve());
232
232
  });
233
233
  }
234
234
 
@@ -290,6 +290,33 @@ function createTab(url: string): Promise<void> {
290
290
  });
291
291
  }
292
292
 
293
+ async function permissionGranted(
294
+ name: "camera" | "microphone",
295
+ ): Promise<boolean> {
296
+ try {
297
+ const status = await navigator.permissions.query({
298
+ name: name as PermissionName,
299
+ });
300
+ return status.state === "granted";
301
+ } catch {
302
+ // Older Chrome can't query these names — don't block; getUserMedia will ask.
303
+ return true;
304
+ }
305
+ }
306
+
307
+ // True when every device the chosen mode needs is already granted to the
308
+ // extension. If not, the caller routes the user to the permission page.
309
+ async function ensureMediaPermission(
310
+ settings: ExtensionSettings,
311
+ ): Promise<boolean> {
312
+ const needsCamera =
313
+ settings.captureSurface === "camera" || settings.includeCamera;
314
+ const needsMic = settings.includeMicrophone;
315
+ if (needsCamera && !(await permissionGranted("camera"))) return false;
316
+ if (needsMic && !(await permissionGranted("microphone"))) return false;
317
+ return true;
318
+ }
319
+
293
320
  async function readAuthStatus(
294
321
  settings: ExtensionSettings,
295
322
  ): Promise<AuthStatus> {
@@ -511,6 +538,16 @@ function renderActiveRecording(recording: NativeRecording | null): void {
511
538
 
512
539
  async function init(): Promise<void> {
513
540
  const settings = await readSettings();
541
+ // Warm the offscreen recorder so the native screen picker opens promptly when
542
+ // the user presses Record (keeps getDisplayMedia close to the click).
543
+ try {
544
+ chrome.runtime.sendMessage(
545
+ { type: "CLIPS_PREWARM" },
546
+ () => void chrome.runtime.lastError,
547
+ );
548
+ } catch {
549
+ /* ignore */
550
+ }
514
551
  const sourceButton = byId<HTMLButtonElement>("source-button");
515
552
  const sourceMenu = byId<HTMLDivElement>("source-menu");
516
553
  const includeCamera = byId<HTMLButtonElement>("include-camera");
@@ -775,6 +812,17 @@ async function init(): Promise<void> {
775
812
  window.close();
776
813
  return;
777
814
  }
815
+ // Gate at record time: if the user wants camera/mic but hasn't granted the
816
+ // extension access yet, send them to the onboarding page first. Requesting
817
+ // there (a real extension page) is the only place Chrome reliably shows the
818
+ // permission dialog and persists the grant for the offscreen recorder + bubble.
819
+ if (!(await ensureMediaPermission(settings))) {
820
+ start.disabled = false;
821
+ setStatus("Allow camera & microphone, then start recording.", "error");
822
+ await createTab(chrome.runtime.getURL("src/permission.html"));
823
+ window.close();
824
+ return;
825
+ }
778
826
  setStatus("Starting recording...");
779
827
  await saveSettings(settings);
780
828
  const response = await sendStartMessage(settings);
@@ -217,6 +217,56 @@ svg {
217
217
  box-shadow: var(--shadow-sm);
218
218
  }
219
219
 
220
+ /* Tooltip (shadcn-style, CSS-only to mirror the desktop app's Radix tooltip) */
221
+ [data-tooltip] {
222
+ position: relative;
223
+ }
224
+
225
+ [data-tooltip]::after,
226
+ [data-tooltip]::before {
227
+ position: absolute;
228
+ top: calc(100% + 6px);
229
+ left: 50%;
230
+ z-index: 60;
231
+ opacity: 0;
232
+ pointer-events: none;
233
+ transform: translate(-50%, -2px);
234
+ transition:
235
+ opacity 90ms ease-in,
236
+ transform 90ms ease-in;
237
+ }
238
+
239
+ [data-tooltip]::after {
240
+ content: attr(data-tooltip);
241
+ padding: 6px 10px;
242
+ border-radius: var(--radius-sm);
243
+ background: var(--bg);
244
+ color: var(--fg);
245
+ box-shadow: var(--shadow-md);
246
+ font-size: 12px;
247
+ font-weight: 500;
248
+ line-height: 1.45;
249
+ white-space: nowrap;
250
+ }
251
+
252
+ [data-tooltip]::before {
253
+ content: "";
254
+ top: calc(100% + 1px);
255
+ border: 5px solid transparent;
256
+ border-bottom-color: var(--bg);
257
+ }
258
+
259
+ [data-tooltip]:hover::after,
260
+ [data-tooltip]:hover::before,
261
+ [data-tooltip]:focus-visible::after,
262
+ [data-tooltip]:focus-visible::before {
263
+ opacity: 1;
264
+ transform: translate(-50%, 0);
265
+ transition:
266
+ opacity 120ms ease-out 150ms,
267
+ transform 120ms ease-out 150ms;
268
+ }
269
+
220
270
  .panel {
221
271
  display: flex;
222
272
  flex-direction: column;
@@ -15,7 +15,10 @@ export default defineConfig({
15
15
  rollupOptions: {
16
16
  input: {
17
17
  background: resolve(root, "src/background.ts"),
18
+ "content-script": resolve(root, "src/content-script.ts"),
18
19
  offscreen: resolve(root, "src/offscreen.html"),
20
+ overlay: resolve(root, "src/overlay.html"),
21
+ permission: resolve(root, "src/permission.html"),
19
22
  popup: resolve(root, "src/popup.html"),
20
23
  options: resolve(root, "src/options.html"),
21
24
  },
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Recording-start audio cue.
3
3
  *
4
- * A short descending "boop" played the instant capture begins so the user
4
+ * A short, soft "ready" chime played the instant capture begins so the user
5
5
  * gets audible confirmation. Two design constraints shape this module:
6
6
  *
7
7
  * 1. Browser audio needs a user gesture to start. We therefore build the
@@ -67,37 +67,54 @@ async function playBeforeCapture(
67
67
  await wait(CUE_SETTLE_MS);
68
68
  }
69
69
 
70
- /** Schedule the descending-chirp tone on an already-running context. */
70
+ /**
71
+ * Schedule the recording-start chime on an already-running context: a soft
72
+ * rising two-note (D5 → A5) with a faint high shimmer. Fast attack, smooth
73
+ * exponential tail — a gentle confirmation rather than a harsh beep. Kept under
74
+ * ~380ms so it fits inside `CUE_PLAY_TIMEOUT_MS` and lands just before capture.
75
+ */
71
76
  function scheduleTone(ctx: AudioContext): Promise<void> {
72
77
  return new Promise<void>((resolve) => {
73
- const startedAt = ctx.currentTime + 0.005;
74
- const oscillator = ctx.createOscillator();
75
- const gain = ctx.createGain();
76
-
77
- oscillator.type = "sine";
78
- oscillator.frequency.setValueAtTime(880, startedAt);
79
- oscillator.frequency.exponentialRampToValueAtTime(660, startedAt + 0.14);
80
-
81
- gain.gain.setValueAtTime(0.0001, startedAt);
82
- gain.gain.exponentialRampToValueAtTime(0.07, startedAt + 0.018);
83
- gain.gain.exponentialRampToValueAtTime(0.0001, startedAt + 0.18);
84
-
85
- oscillator.connect(gain);
86
- gain.connect(ctx.destination);
87
-
88
- // Resolve on `ended`, with a timeout fallback in case it never fires.
78
+ const t0 = ctx.currentTime + 0.005;
79
+ const voices = [
80
+ { freq: 587.33, at: 0.0, dur: 0.22, peak: 0.06 }, // D5
81
+ { freq: 880.0, at: 0.06, dur: 0.26, peak: 0.075 }, // A5
82
+ { freq: 1760.0, at: 0.065, dur: 0.14, peak: 0.02 }, // A6 shimmer
83
+ ];
84
+
85
+ let lastStop = t0;
86
+ for (const voice of voices) {
87
+ const startAt = t0 + voice.at;
88
+ const stopAt = startAt + voice.dur;
89
+ lastStop = Math.max(lastStop, stopAt);
90
+
91
+ const oscillator = ctx.createOscillator();
92
+ oscillator.type = "sine";
93
+ oscillator.frequency.setValueAtTime(voice.freq, startAt);
94
+
95
+ const gain = ctx.createGain();
96
+ gain.gain.setValueAtTime(0.0001, startAt);
97
+ gain.gain.exponentialRampToValueAtTime(voice.peak, startAt + 0.012);
98
+ gain.gain.exponentialRampToValueAtTime(0.0001, stopAt);
99
+
100
+ oscillator.connect(gain);
101
+ gain.connect(ctx.destination);
102
+
103
+ oscillator.start(startAt);
104
+ oscillator.stop(stopAt + 0.02);
105
+ }
106
+
107
+ // Resolve a touch after the final voice ends.
89
108
  let resolved = false;
90
109
  const finish = () => {
91
110
  if (resolved) return;
92
111
  resolved = true;
93
- window.clearTimeout(timer);
94
112
  resolve();
95
113
  };
96
- const timer = window.setTimeout(finish, 500);
97
- oscillator.addEventListener("ended", finish, { once: true });
98
-
99
- oscillator.start(startedAt);
100
- oscillator.stop(startedAt + 0.2);
114
+ window.setTimeout(
115
+ finish,
116
+ Math.ceil((lastStop - ctx.currentTime) * 1000) + 60,
117
+ );
101
118
  });
102
119
  }
103
120