@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.
- package/README.md +15 -15
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +67 -0
- package/corpus/core/docs/design/durable-agent-runs.md +217 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +89 -0
- package/corpus/core/src/agent/model-config.ts +0 -2
- package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
- package/corpus/core/src/cli/skills.ts +2 -2
- package/corpus/core/src/cli/sync-builder-starter-manifest.ts +275 -0
- package/corpus/core/src/client/AgentPanel.tsx +5 -3
- package/corpus/core/src/client/AssistantChat.tsx +8 -0
- package/corpus/core/src/client/client-surface.ts +41 -0
- package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
- package/corpus/core/src/client/feedback-context.ts +4 -0
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/sse-event-processor.ts +7 -1
- package/corpus/core/src/mcp/build-server.ts +57 -1
- package/corpus/core/src/mcp/embed-app.ts +86 -2
- package/corpus/core/src/mcp/oauth-route.ts +109 -13
- package/corpus/core/src/server/core-routes-plugin.ts +78 -0
- package/corpus/core/src/server/onboarding-html.ts +35 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
- package/corpus/templates/analytics/AGENTS.md +8 -1
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +97 -2
- package/corpus/templates/analytics/actions/update-dashboard.ts +36 -7
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
- package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -4
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
- package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
- package/corpus/templates/assets/actions/generate-image.ts +24 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
- package/corpus/templates/assets/app/global.css +17 -1
- package/corpus/templates/assets/app/routes/_index.tsx +94 -14
- package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
- package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
- package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
- package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
- package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
- package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
- package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
- package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
- package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
- package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
- package/corpus/templates/clips/desktop/src/styles.css +70 -2
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +73 -0
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +34 -5
- package/corpus/templates/content/AGENTS.md +20 -3
- package/corpus/templates/content/actions/_database-utils.ts +15 -0
- package/corpus/templates/content/actions/_property-utils.ts +371 -4
- package/corpus/templates/content/actions/configure-document-property.ts +35 -1
- package/corpus/templates/content/actions/create-content-database.ts +5 -1
- package/corpus/templates/content/actions/delete-document-property.ts +52 -2
- package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
- package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
- package/corpus/templates/content/actions/set-document-property.ts +39 -0
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
- package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
- package/corpus/templates/content/app/global.css +9 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
- package/corpus/templates/content/server/db/schema.ts +30 -0
- package/corpus/templates/content/server/plugins/db.ts +87 -0
- package/corpus/templates/content/shared/api.ts +7 -0
- package/corpus/templates/content/shared/properties.ts +109 -0
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
- package/corpus/templates/forms/actions/export-responses.ts +6 -0
- package/corpus/templates/forms/actions/list-responses.ts +2 -0
- package/corpus/templates/forms/actions/response-insights.ts +2 -0
- package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
- package/corpus/templates/forms/server/db/schema.ts +7 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
- package/corpus/templates/forms/server/lib/integrations.ts +76 -2
- package/corpus/templates/forms/server/plugins/db.ts +20 -0
- package/corpus/templates/forms/shared/types.ts +12 -0
- package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
- package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
- package/dist/action.js +87 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/engine/builder-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/model-config.d.ts +2 -2
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +0 -2
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +13 -0
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +44 -0
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/cli/skills.js +2 -2
- package/dist/cli/skills.js.map +1 -1
- package/dist/cli/sync-builder-starter-manifest.d.ts +32 -0
- package/dist/cli/sync-builder-starter-manifest.d.ts.map +1 -0
- package/dist/cli/sync-builder-starter-manifest.js +179 -0
- package/dist/cli/sync-builder-starter-manifest.js.map +1 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/client-surface.d.ts +17 -0
- package/dist/client/client-surface.d.ts.map +1 -0
- package/dist/client/client-surface.js +24 -0
- package/dist/client/client-surface.js.map +1 -0
- package/dist/client/composer/TiptapComposer.d.ts +26 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +27 -5
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/feedback-context.d.ts +3 -0
- package/dist/client/feedback-context.d.ts.map +1 -1
- package/dist/client/feedback-context.js +2 -0
- package/dist/client/feedback-context.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/mcp/build-server.d.ts +21 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +43 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +86 -2
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/oauth-route.d.ts.map +1 -1
- package/dist/mcp/oauth-route.js +95 -12
- package/dist/mcp/oauth-route.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +63 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +35 -0
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/docs/design/durable-agent-runs.md +217 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/templates/analytics/app/pages/About.tsx +0 -108
- 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"
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
}
|
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
24
|
-
//
|
|
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
|
-
|
|
31
|
-
if (
|
|
27
|
+
const playOneSecondCue = useCallback(() => {
|
|
28
|
+
if (hasPlayedOneSecondCueRef.current) return;
|
|
32
29
|
hasPlayedOneSecondCueRef.current = true;
|
|
33
30
|
onOneSecond?.();
|
|
34
|
-
}, [
|
|
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
|
-
|
|
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,
|
|
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
|
|
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-
|
|
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.
|
|
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
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
14
|
-
const
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
const oscillator = ctx.createOscillator();
|
|
33
|
+
oscillator.type = "sine";
|
|
34
|
+
oscillator.frequency.setValueAtTime(voice.freq, startAt);
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
|
@@ -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": [
|