@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
@@ -1,12 +1,49 @@
1
- import { useEffect } from "react";
1
+ import { useCallback, useEffect, useState } from "react";
2
2
  import {
3
3
  AgentChatSurface,
4
4
  getBrowserTabId,
5
5
  markAgentChatHomeHandoff,
6
+ readClientAppState,
7
+ sendToAgentChat,
8
+ writeClientAppState,
6
9
  } from "@agent-native/core/client";
7
10
  import { IconPhoto, IconSparkles, IconVideo } from "@tabler/icons-react";
8
11
  import { ASSETS_CHAT_STORAGE_KEY } from "@/lib/chat";
9
12
 
13
+ // The composer's model picker shows the chat LLM (Claude/OpenAI/Gemini). The
14
+ // Assets app also drives a separate *image* model, so we surface it in the same
15
+ // menu — otherwise "Claude" reads as the image generator, which it isn't. The
16
+ // choice persists in per-user application state so the generate-image action
17
+ // (server-side) can read it as the default model. Values must be valid
18
+ // IMAGE_MODELS ids from shared/api.
19
+ const IMAGE_MODEL_STATE_KEY = "imageGenerationModel";
20
+ const DEFAULT_IMAGE_MODEL = "gemini-3.1-flash-image";
21
+ const IMAGE_MODEL_OPTIONS = [
22
+ { value: "gemini-3-pro-image", label: "Gemini 3 Pro · best quality" },
23
+ { value: "gemini-3.1-flash-image", label: "Gemini 3.1 Flash · fast" },
24
+ { value: "gemini-2.5-flash-image", label: "Gemini 2.5 Flash" },
25
+ ] as const;
26
+
27
+ // Empty-state starters. Clicking one prefills the composer (without sending) so
28
+ // the user can finish the thought instead of staring at a chip that does
29
+ // nothing. `submit: false` = prefill only; `openSidebar: false` keeps focus on
30
+ // the page-level Create surface.
31
+ const CHAT_STARTERS = [
32
+ {
33
+ key: "image",
34
+ Icon: IconPhoto,
35
+ label: "image",
36
+ prompt: "Create an image of ",
37
+ },
38
+ {
39
+ key: "video",
40
+ Icon: IconVideo,
41
+ label: "video",
42
+ prompt: "Create a video of ",
43
+ },
44
+ { key: "refine", Icon: IconSparkles, label: "refine", prompt: "Refine " },
45
+ ] as const;
46
+
10
47
  const SEO_TITLE =
11
48
  "Agent-Native Assets - Open Source AI asset library for brand-safe images and video";
12
49
  const SEO_DESCRIPTION =
@@ -25,6 +62,8 @@ export function meta() {
25
62
  }
26
63
 
27
64
  export default function CreatePage() {
65
+ const [imageModel, setImageModel] = useState<string>(DEFAULT_IMAGE_MODEL);
66
+
28
67
  useEffect(() => {
29
68
  function handleChatRunning(event: Event) {
30
69
  const detail = (event as CustomEvent).detail;
@@ -38,6 +77,34 @@ export default function CreatePage() {
38
77
  window.removeEventListener("agentNative.chatRunning", handleChatRunning);
39
78
  }, []);
40
79
 
80
+ // Hydrate the saved image-model default so the picker reflects the user's
81
+ // last choice across sessions.
82
+ useEffect(() => {
83
+ let cancelled = false;
84
+ void readClientAppState<{ model?: string }>(IMAGE_MODEL_STATE_KEY)
85
+ .then((state) => {
86
+ const stored = state?.model;
87
+ if (
88
+ !cancelled &&
89
+ stored &&
90
+ IMAGE_MODEL_OPTIONS.some((option) => option.value === stored)
91
+ ) {
92
+ setImageModel(stored);
93
+ }
94
+ })
95
+ .catch(() => {});
96
+ return () => {
97
+ cancelled = true;
98
+ };
99
+ }, []);
100
+
101
+ const handleImageModelChange = useCallback((value: string) => {
102
+ setImageModel(value);
103
+ void writeClientAppState(IMAGE_MODEL_STATE_KEY, { model: value }).catch(
104
+ () => {},
105
+ );
106
+ }, []);
107
+
41
108
  return (
42
109
  <div className="flex h-full min-h-0 flex-col bg-background">
43
110
  <AgentChatSurface
@@ -47,6 +114,15 @@ export default function CreatePage() {
47
114
  defaultMode="chat"
48
115
  storageKey={ASSETS_CHAT_STORAGE_KEY}
49
116
  browserTabId={getBrowserTabId()}
117
+ imageModelMenu={{
118
+ value: imageModel,
119
+ options: IMAGE_MODEL_OPTIONS.map((option) => ({
120
+ value: option.value,
121
+ label: option.label,
122
+ })),
123
+ onChange: handleImageModelChange,
124
+ label: "Image model",
125
+ }}
50
126
  showHeader={false}
51
127
  showTabBar={false}
52
128
  dynamicSuggestions={false}
@@ -63,19 +139,23 @@ export default function CreatePage() {
63
139
  Start with a hero image, product reveal, reference edit, or a
64
140
  direction you want to explore.
65
141
  </p>
66
- <div className="assets-create-chat-pill-row" aria-hidden="true">
67
- <span>
68
- <IconPhoto className="size-3.5" />
69
- image
70
- </span>
71
- <span>
72
- <IconVideo className="size-3.5" />
73
- video
74
- </span>
75
- <span>
76
- <IconSparkles className="size-3.5" />
77
- refine
78
- </span>
142
+ <div className="assets-create-chat-pill-row">
143
+ {CHAT_STARTERS.map(({ key, Icon, label, prompt }) => (
144
+ <button
145
+ key={key}
146
+ type="button"
147
+ onClick={() =>
148
+ sendToAgentChat({
149
+ message: prompt,
150
+ submit: false,
151
+ openSidebar: false,
152
+ })
153
+ }
154
+ >
155
+ <Icon className="size-3.5" />
156
+ {label}
157
+ </button>
158
+ ))}
79
159
  </div>
80
160
  </div>
81
161
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Chat dates in the sidebar no longer wrap onto two lines.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Clicking image, video, or refine on the start screen now drops a starter prompt into the composer instead of doing nothing.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Pick your image-generation model right from the chat model menu — it's shown alongside the chat model so it's clear which is which.
@@ -1,6 +1,5 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { toast } from "sonner";
3
- import { cn } from "@/lib/utils";
4
3
  import {
5
4
  useFolders,
6
5
  useRecordings,
@@ -34,7 +33,6 @@ import {
34
33
  DialogTitle,
35
34
  DialogFooter,
36
35
  } from "@/components/ui/dialog";
37
- import { IconChecks } from "@tabler/icons-react";
38
36
  import { ShareRecordingDialog } from "@/components/player/share-dialog";
39
37
 
40
38
  interface LibraryGridProps {
@@ -107,7 +105,7 @@ export function LibraryGrid({
107
105
  }: LibraryGridProps) {
108
106
  const [sort, setSort] = useState<SortKey>("recent");
109
107
  const [selected, setSelected] = useState<Set<string>>(new Set());
110
- const [selectionMode, setSelectionMode] = useState(false);
108
+ const selectionMode = selected.size > 0;
111
109
  const [renamingRec, setRenamingRec] = useState<RecordingSummary | null>(null);
112
110
  const [renameValue, setRenameValue] = useState("");
113
111
  const [sharingRec, setSharingRec] = useState<RecordingSummary | null>(null);
@@ -202,7 +200,6 @@ export function LibraryGrid({
202
200
 
203
201
  const clearSelection = () => {
204
202
  setSelected(new Set());
205
- setSelectionMode(false);
206
203
  };
207
204
 
208
205
  const moveSelected = async (targetFolderId: string | null) => {
@@ -337,22 +334,6 @@ export function LibraryGrid({
337
334
  </div>
338
335
  <div className="ml-auto flex shrink-0 items-center gap-2">
339
336
  {extraActions}
340
- <Button
341
- variant={selectionMode ? "default" : "outline"}
342
- size="sm"
343
- className={cn(
344
- "h-8 gap-1.5",
345
- selectionMode &&
346
- "bg-primary text-primary-foreground hover:bg-primary/90",
347
- )}
348
- onClick={() => {
349
- setSelectionMode((v) => !v);
350
- if (selectionMode) setSelected(new Set());
351
- }}
352
- >
353
- <IconChecks className="h-3.5 w-3.5" />
354
- Select
355
- </Button>
356
337
  <SortMenu value={sort} onChange={setSort} />
357
338
  </div>
358
339
  </PageHeader>
@@ -1,13 +1,12 @@
1
- import { useEffect, useRef, useState } from "react";
2
- import { IconPlayerPause, IconPlayerPlay, IconX } from "@tabler/icons-react";
3
- import { Button } from "@/components/ui/button";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { IconPlayerSkipForward, IconX } from "@tabler/icons-react";
4
3
 
5
4
  export interface CountdownOverlayProps {
6
5
  /** Total seconds to count down from. Default 3. */
7
6
  seconds?: number;
8
- /** Called when the countdown reaches 1. */
7
+ /** Called when the countdown reaches 1 (drives the recording-start cue). */
9
8
  onOneSecond?: () => void;
10
- /** Called when the countdown reaches 0. */
9
+ /** Called when the countdown reaches 0 (or the user skips). */
11
10
  onComplete: () => void;
12
11
  /** Called when the user cancels before recording begins. */
13
12
  onCancel: () => void;
@@ -20,77 +19,102 @@ export function CountdownOverlay({
20
19
  onCancel,
21
20
  }: CountdownOverlayProps) {
22
21
  const [remaining, setRemaining] = useState(seconds);
23
- const [paused, setPaused] = useState(false);
24
- // Ensure onComplete fires exactly once for the lifetime of the countdown,
25
- // even if its identity changes while `remaining` is already 0 (which would
26
- // otherwise re-run this effect and call it again).
22
+ // Ensure each callback fires exactly once for the lifetime of the countdown,
23
+ // even if identities change or the user skips while the timer is mid-flight.
27
24
  const hasCompletedRef = useRef(false);
28
25
  const hasPlayedOneSecondCueRef = useRef(false);
29
26
 
30
- useEffect(() => {
31
- if (remaining !== 1 || hasPlayedOneSecondCueRef.current) return;
27
+ const playOneSecondCue = useCallback(() => {
28
+ if (hasPlayedOneSecondCueRef.current) return;
32
29
  hasPlayedOneSecondCueRef.current = true;
33
30
  onOneSecond?.();
34
- }, [remaining, onOneSecond]);
31
+ }, [onOneSecond]);
32
+
33
+ const complete = useCallback(() => {
34
+ if (hasCompletedRef.current) return;
35
+ hasCompletedRef.current = true;
36
+ onComplete();
37
+ }, [onComplete]);
38
+
39
+ // Skip the rest of the countdown and start recording immediately. Still play
40
+ // the start cue so the user gets the same audible confirmation they'd get if
41
+ // the countdown had run to zero.
42
+ const handleSkip = useCallback(() => {
43
+ playOneSecondCue();
44
+ complete();
45
+ }, [playOneSecondCue, complete]);
46
+
47
+ useEffect(() => {
48
+ if (remaining === 1) playOneSecondCue();
49
+ }, [remaining, playOneSecondCue]);
35
50
 
36
51
  useEffect(() => {
37
52
  if (remaining <= 0) {
38
- if (!hasCompletedRef.current) {
39
- hasCompletedRef.current = true;
40
- onComplete();
41
- }
53
+ complete();
42
54
  return;
43
55
  }
44
- if (paused) return;
45
56
  const id = window.setTimeout(() => setRemaining((v) => v - 1), 1000);
46
57
  return () => window.clearTimeout(id);
47
- }, [remaining, onComplete, paused]);
58
+ }, [remaining, complete]);
59
+
60
+ // Keyboard parity with the desktop overlay: Enter starts now, Esc cancels.
61
+ useEffect(() => {
62
+ const onKeyDown = (event: KeyboardEvent) => {
63
+ if (event.key === "Enter") {
64
+ event.preventDefault();
65
+ handleSkip();
66
+ } else if (event.key === "Escape") {
67
+ event.preventDefault();
68
+ onCancel();
69
+ }
70
+ };
71
+ window.addEventListener("keydown", onKeyDown);
72
+ return () => window.removeEventListener("keydown", onKeyDown);
73
+ }, [handleSkip, onCancel]);
74
+
75
+ const controlClasses =
76
+ "flex h-16 w-16 items-center justify-center rounded-full border border-white/25 bg-white/5 text-white/90 shadow-lg backdrop-blur transition-colors hover:border-white/60 hover:bg-white/15 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70";
48
77
 
49
78
  return (
50
79
  <div
51
- className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm"
80
+ className="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-black/70 backdrop-blur-sm"
52
81
  aria-live="polite"
53
82
  aria-label={`Recording starts in ${remaining}`}
54
83
  >
55
- <div className="flex flex-col items-center gap-6">
84
+ <div className="flex items-center gap-10 sm:gap-14">
85
+ <button
86
+ type="button"
87
+ onClick={onCancel}
88
+ aria-label="Cancel recording"
89
+ className={controlClasses}
90
+ >
91
+ <IconX className="h-7 w-7" stroke={1.75} />
92
+ </button>
93
+
56
94
  <div
57
95
  key={remaining}
58
- className="flex h-48 w-48 items-center justify-center rounded-full text-[120px] font-bold text-white shadow-2xl"
96
+ className="flex h-44 w-44 items-center justify-center rounded-full text-[112px] font-bold leading-none text-white shadow-2xl duration-200 animate-in zoom-in-75 fade-in"
59
97
  style={{
60
98
  background:
61
- "radial-gradient(circle at 30% 30%, rgba(255,255,255,0.15), transparent 60%), hsl(var(--primary))",
99
+ "radial-gradient(circle at 30% 30%, rgba(255,255,255,0.18), transparent 60%), hsl(var(--primary))",
62
100
  }}
63
101
  >
64
102
  {remaining > 0 ? remaining : "Go"}
65
103
  </div>
66
104
 
67
- <div className="flex items-center gap-2 rounded-full border border-white/15 bg-black/40 p-1.5 shadow-2xl backdrop-blur">
68
- <Button
69
- type="button"
70
- variant="secondary"
71
- size="sm"
72
- className="rounded-full"
73
- onClick={() => setPaused((value) => !value)}
74
- >
75
- {paused ? (
76
- <IconPlayerPlay className="h-4 w-4" />
77
- ) : (
78
- <IconPlayerPause className="h-4 w-4" />
79
- )}
80
- {paused ? "Resume" : "Pause"}
81
- </Button>
82
- <Button
83
- type="button"
84
- variant="outline"
85
- size="sm"
86
- className="rounded-full border-white/20 bg-white/10 text-white hover:bg-white/20 hover:text-white"
87
- onClick={onCancel}
88
- >
89
- <IconX className="h-4 w-4" />
90
- Cancel
91
- </Button>
92
- </div>
105
+ <button
106
+ type="button"
107
+ onClick={handleSkip}
108
+ aria-label="Skip countdown and start recording now"
109
+ className={controlClasses}
110
+ >
111
+ <IconPlayerSkipForward className="h-7 w-7" stroke={1.75} />
112
+ </button>
93
113
  </div>
114
+
115
+ <p className="absolute bottom-16 left-1/2 -translate-x-1/2 rounded-full bg-black/50 px-3 py-1 text-xs font-medium text-white/70 backdrop-blur">
116
+ Esc to cancel · Enter to start now
117
+ </p>
94
118
  </div>
95
119
  );
96
120
  }
@@ -8,35 +8,53 @@ const noopCountdownAudioCue: CountdownAudioCue = {
8
8
  cleanup() {},
9
9
  };
10
10
 
11
+ /**
12
+ * A soft, modern "ready" chime: a rising two-note (D5 → A5) with a faint high
13
+ * shimmer. Each voice has a fast attack and a smooth exponential tail so it
14
+ * reads as a gentle confirmation rather than a harsh beep. Kept under ~380ms so
15
+ * it lands cleanly just before capture.
16
+ */
11
17
  function scheduleCountdownTone(ctx: AudioContext): Promise<void> {
12
18
  return new Promise<void>((resolve) => {
13
- const startedAt = ctx.currentTime + 0.005;
14
- const oscillator = ctx.createOscillator();
15
- const gain = ctx.createGain();
19
+ const t0 = ctx.currentTime + 0.005;
20
+ const voices = [
21
+ { freq: 587.33, at: 0.0, dur: 0.22, peak: 0.06 }, // D5
22
+ { freq: 880.0, at: 0.06, dur: 0.26, peak: 0.075 }, // A5
23
+ { freq: 1760.0, at: 0.065, dur: 0.14, peak: 0.02 }, // A6 shimmer
24
+ ];
16
25
 
17
- oscillator.type = "sine";
18
- oscillator.frequency.setValueAtTime(880, startedAt);
19
- oscillator.frequency.exponentialRampToValueAtTime(660, startedAt + 0.14);
26
+ let lastStop = t0;
27
+ for (const voice of voices) {
28
+ const startAt = t0 + voice.at;
29
+ const stopAt = startAt + voice.dur;
30
+ lastStop = Math.max(lastStop, stopAt);
20
31
 
21
- gain.gain.setValueAtTime(0.0001, startedAt);
22
- gain.gain.exponentialRampToValueAtTime(0.07, startedAt + 0.018);
23
- gain.gain.exponentialRampToValueAtTime(0.0001, startedAt + 0.18);
32
+ const oscillator = ctx.createOscillator();
33
+ oscillator.type = "sine";
34
+ oscillator.frequency.setValueAtTime(voice.freq, startAt);
24
35
 
25
- oscillator.connect(gain);
26
- gain.connect(ctx.destination);
36
+ const gain = ctx.createGain();
37
+ gain.gain.setValueAtTime(0.0001, startAt);
38
+ gain.gain.exponentialRampToValueAtTime(voice.peak, startAt + 0.012);
39
+ gain.gain.exponentialRampToValueAtTime(0.0001, stopAt);
40
+
41
+ oscillator.connect(gain);
42
+ gain.connect(ctx.destination);
43
+
44
+ oscillator.start(startAt);
45
+ oscillator.stop(stopAt + 0.02);
46
+ }
27
47
 
28
48
  let resolved = false;
29
49
  const finish = () => {
30
50
  if (resolved) return;
31
51
  resolved = true;
32
- window.clearTimeout(timer);
33
52
  resolve();
34
53
  };
35
- const timer = window.setTimeout(finish, 500);
36
- oscillator.addEventListener("ended", finish, { once: true });
37
-
38
- oscillator.start(startedAt);
39
- oscillator.stop(startedAt + 0.2);
54
+ window.setTimeout(
55
+ finish,
56
+ Math.ceil((lastStop - ctx.currentTime) * 1000) + 60,
57
+ );
40
58
  });
41
59
  }
42
60
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Removed Select button from library — hover a clip to select it
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Shared video links now play for anyone without signing in
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Skip or cancel the recording countdown with buttons beside the number
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ The desktop camera bubble now looks sharp the instant it opens instead of staying blurry for the first several seconds
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ The desktop recording widget now grows downward only when you hover it, so its buttons stay in place instead of shifting up under your cursor
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ The recording-start sound is now a softer, more modern chime
@@ -18,18 +18,27 @@
18
18
  "32": "icons/icon-32.png",
19
19
  "128": "icons/icon-128.png"
20
20
  },
21
- "permissions": [
22
- "activeTab",
23
- "debugger",
24
- "offscreen",
25
- "storage",
26
- "tabCapture"
27
- ],
21
+ "permissions": ["activeTab", "debugger", "offscreen", "scripting", "storage"],
28
22
  "host_permissions": [
29
23
  "https://clips.agent-native.com/*",
30
24
  "https://forms.agent-native.com/*",
31
25
  "http://localhost/*",
32
- "http://127.0.0.1/*"
26
+ "http://127.0.0.1/*",
27
+ "<all_urls>"
28
+ ],
29
+ "content_scripts": [
30
+ {
31
+ "matches": ["<all_urls>"],
32
+ "js": ["assets/content-script.js"],
33
+ "run_at": "document_idle",
34
+ "all_frames": false
35
+ }
36
+ ],
37
+ "web_accessible_resources": [
38
+ {
39
+ "resources": ["src/overlay.html", "assets/*", "icons/*"],
40
+ "matches": ["<all_urls>"]
41
+ }
33
42
  ],
34
43
  "externally_connectable": {
35
44
  "matches": [