@agent-native/core 0.161.23 → 0.162.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -2
- package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/calendar/server/handlers/google-auth.ts +1 -8
- package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +88 -35
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +36 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -3
- package/corpus/templates/clips/desktop/src/app.tsx +30 -40
- package/corpus/templates/clips/desktop/src/components/MicOffConfirmation.tsx +47 -0
- package/corpus/templates/clips/desktop/src/styles.css +110 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +114 -7
- package/corpus/templates/clips/shared/recording-capture.ts +18 -13
- package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -5
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +0 -3
- package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +0 -4
- package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +4 -1
- package/corpus/templates/content/app/hooks/use-notion.ts +17 -15
- package/corpus/templates/crm/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/design/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +18 -3
- package/corpus/templates/mail/server/handlers/google-auth.ts +1 -8
- package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +4 -0
- package/corpus/templates/slides/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/slides/actions/_await-fit-check.ts +30 -5
- package/corpus/templates/slides/actions/add-slide.ts +6 -2
- package/corpus/templates/slides/actions/create-deck.ts +2 -1
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +34 -1
- package/corpus/templates/slides/app/routes/p.$id.tsx +3 -1
- package/corpus/templates/slides/changelog/2026-08-18-generated-slides-preserve-speaker-notes.md +6 -0
- package/corpus/templates/slides/server/handlers/assets.ts +23 -12
- package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +15 -6
- package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +15 -6
- package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +34 -19
- package/corpus/templates/slides/server/handlers/share.ts +1 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +1 -1
- package/corpus/templates/slides/server/routes/api/exports/[filename].get.ts +9 -3
- package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -5
- package/corpus/templates/slides/server/routes/api/exports/html.post.ts +9 -7
- package/corpus/templates/slides/server/routes/api/exports/pptx.post.ts +9 -7
- package/corpus/templates/slides/shared/api.ts +7 -1
- package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/agent/engine/translate-anthropic.js +27 -2
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.js +16 -8
- package/dist/agent/thread-data-builder.d.ts +1 -0
- package/dist/agent/thread-data-builder.js +6 -4
- package/dist/agent/tool-approval-store.d.ts +10 -1
- package/dist/agent/tool-approval-store.js +19 -2
- package/dist/agent/types.d.ts +9 -0
- package/dist/client/AssistantChat.js +14 -6
- package/dist/client/EnvironmentBadge.d.ts +15 -0
- package/dist/client/EnvironmentBadge.js +79 -0
- package/dist/client/MultiTabAssistantChat.js +73 -17
- package/dist/client/agent-chat-adapter.js +11 -1
- package/dist/client/api-path.js +14 -1
- package/dist/client/app-providers.js +2 -1
- package/dist/client/chat/tool-call-display.d.ts +12 -2
- package/dist/client/chat/tool-call-display.js +17 -7
- package/dist/client/chat/widgets/WorkspaceFileWidget.js +1 -1
- package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +6 -0
- package/dist/client/chat/widgets/builtin-tool-renderers.js +19 -0
- package/dist/client/onboarding/FirstRunOnboarding.js +46 -32
- package/dist/client/onboarding/use-onboarding.d.ts +8 -1
- package/dist/client/onboarding/use-onboarding.js +22 -7
- package/dist/client/session-replay.js +58 -15
- package/dist/client/settings/SettingsTabsPage.js +19 -6
- package/dist/client/sse-event-processor.d.ts +7 -0
- package/dist/client/sse-event-processor.js +4 -1
- package/dist/client/ui/index.d.ts +1 -0
- package/dist/client/ui/index.js +1 -0
- package/dist/client/use-action.js +26 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.js +53 -0
- package/dist/extensions/fetch-tool.js +8 -2
- package/dist/mcp/embed-app.js +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/index.d.ts +10 -0
- package/dist/provider-api/index.js +45 -9
- package/dist/server/auth.js +10 -3
- package/dist/server/poll.js +7 -3
- package/dist/server/sentry.d.ts +1 -1
- package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/vite/client.d.ts +5 -0
- package/dist/vite/client.js +64 -2
- package/dist/workspace-files/actions.d.ts +1 -8
- package/dist/workspace-files/actions.js +5 -21
- package/dist/workspace-files/index.d.ts +1 -1
- package/dist/workspace-files/index.js +1 -1
- package/dist/workspace-files/store.d.ts +18 -0
- package/dist/workspace-files/store.js +24 -3
- package/package.json +3 -3
- package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
package/corpus/README.md
CHANGED
|
@@ -50,7 +50,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
|
|
|
50
50
|
|
|
51
51
|
For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
|
|
52
52
|
|
|
53
|
-
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It
|
|
53
|
+
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It polls every 2 seconds only while an agent run is in flight; otherwise it idles at 60 seconds whether or not SSE is connected (`SSE_FALLBACK_INTERVAL_MS` and `IDLE_POLL_INTERVAL_MS`, both 60 s). A hidden tab polls no faster than every 10 seconds (`HIDDEN_POLL_INTERVAL_MS`). Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
|
|
54
54
|
|
|
55
55
|
5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
|
|
56
56
|
|
|
@@ -1623,8 +1623,15 @@ function MetricRenderer({
|
|
|
1623
1623
|
|
|
1624
1624
|
function numericTableValue(value: unknown): number | null {
|
|
1625
1625
|
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
1626
|
-
if (typeof value !== "string"
|
|
1627
|
-
const
|
|
1626
|
+
if (typeof value !== "string") return null;
|
|
1627
|
+
const trimmed = value.trim();
|
|
1628
|
+
if (trimmed === "") return null;
|
|
1629
|
+
// Strip display formatting (currency symbols, thousands separators,
|
|
1630
|
+
// percent signs, whitespace) so a formatted numeric column ("$1,234",
|
|
1631
|
+
// "12.5%") still parses as a number instead of falling back to text.
|
|
1632
|
+
const cleaned = trimmed.replace(/[$€£¥%,\s]/g, "");
|
|
1633
|
+
if (cleaned === "" || cleaned === "-" || cleaned === "+") return null;
|
|
1634
|
+
const numeric = Number(cleaned);
|
|
1628
1635
|
return Number.isFinite(numeric) ? numeric : null;
|
|
1629
1636
|
}
|
|
1630
1637
|
|
|
@@ -1651,6 +1658,15 @@ export function sortTableRows(
|
|
|
1651
1658
|
|
|
1652
1659
|
const an = numericTableValue(av);
|
|
1653
1660
|
const bn = numericTableValue(bv);
|
|
1661
|
+
|
|
1662
|
+
// A value that fails to parse as a number sorts after one that
|
|
1663
|
+
// does, regardless of column direction — same rule as null above.
|
|
1664
|
+
// Keeps a mostly-numeric column ("10", "9", "N/A") grouped by
|
|
1665
|
+
// magnitude instead of interleaving text lexically with numbers,
|
|
1666
|
+
// and never coerces the unparseable value to 0.
|
|
1667
|
+
if (an !== null && bn === null) return -1;
|
|
1668
|
+
if (an === null && bn !== null) return 1;
|
|
1669
|
+
|
|
1654
1670
|
const comparison =
|
|
1655
1671
|
an !== null && bn !== null
|
|
1656
1672
|
? an - bn
|
|
@@ -50,7 +50,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
|
|
|
50
50
|
|
|
51
51
|
For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
|
|
52
52
|
|
|
53
|
-
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It
|
|
53
|
+
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It polls every 2 seconds only while an agent run is in flight; otherwise it idles at 60 seconds whether or not SSE is connected (`SSE_FALLBACK_INTERVAL_MS` and `IDLE_POLL_INTERVAL_MS`, both 60 s). A hidden tab polls no faster than every 10 seconds (`HIDDEN_POLL_INTERVAL_MS`). Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
|
|
54
54
|
|
|
55
55
|
5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
|
|
56
56
|
|
|
@@ -50,7 +50,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
|
|
|
50
50
|
|
|
51
51
|
For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
|
|
52
52
|
|
|
53
|
-
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It
|
|
53
|
+
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It polls every 2 seconds only while an agent run is in flight; otherwise it idles at 60 seconds whether or not SSE is connected (`SSE_FALLBACK_INTERVAL_MS` and `IDLE_POLL_INTERVAL_MS`, both 60 s). A hidden tab polls no faster than every 10 seconds (`HIDDEN_POLL_INTERVAL_MS`). Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
|
|
54
54
|
|
|
55
55
|
5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
|
|
56
56
|
|
|
@@ -50,7 +50,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
|
|
|
50
50
|
|
|
51
51
|
For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
|
|
52
52
|
|
|
53
|
-
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It
|
|
53
|
+
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It polls every 2 seconds only while an agent run is in flight; otherwise it idles at 60 seconds whether or not SSE is connected (`SSE_FALLBACK_INTERVAL_MS` and `IDLE_POLL_INTERVAL_MS`, both 60 s). A hidden tab polls no faster than every 10 seconds (`HIDDEN_POLL_INTERVAL_MS`). Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
|
|
54
54
|
|
|
55
55
|
5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
|
|
56
56
|
|
|
@@ -163,23 +163,16 @@ function googleOAuthErrorPayload(
|
|
|
163
163
|
message: string;
|
|
164
164
|
code?: string;
|
|
165
165
|
accountId?: string;
|
|
166
|
-
existingOwner?: string;
|
|
167
|
-
attemptedOwner?: string;
|
|
168
166
|
} {
|
|
169
167
|
if (
|
|
170
168
|
error instanceof OAuthAccountOwnedByOtherUserError ||
|
|
171
169
|
error?.name === "OAuthAccountOwnedByOtherUserError"
|
|
172
170
|
) {
|
|
173
171
|
const account = error.accountId || "This Google account";
|
|
174
|
-
const existingOwner = error.existingOwner || undefined;
|
|
175
|
-
const attemptedOwner = error.attemptedOwner || undefined;
|
|
176
|
-
const message = `${account} is connected to another login. Sign out, then sign in with ${account}.`;
|
|
177
172
|
return {
|
|
178
|
-
message,
|
|
173
|
+
message: `${account} is already connected to another login. Sign out, then sign in with the login that originally connected it.`,
|
|
179
174
|
code: "account_owner_mismatch",
|
|
180
175
|
accountId: error.accountId,
|
|
181
|
-
existingOwner,
|
|
182
|
-
attemptedOwner,
|
|
183
176
|
};
|
|
184
177
|
}
|
|
185
178
|
|
|
@@ -50,7 +50,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
|
|
|
50
50
|
|
|
51
51
|
For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
|
|
52
52
|
|
|
53
|
-
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It
|
|
53
|
+
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It polls every 2 seconds only while an agent run is in flight; otherwise it idles at 60 seconds whether or not SSE is connected (`SSE_FALLBACK_INTERVAL_MS` and `IDLE_POLL_INTERVAL_MS`, both 60 s). A hidden tab polls no faster than every 10 seconds (`HIDDEN_POLL_INTERVAL_MS`). Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
|
|
54
54
|
|
|
55
55
|
5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
|
|
56
56
|
|
|
@@ -2,6 +2,7 @@ import { agentNativePath } from "@agent-native/core/client/api-path";
|
|
|
2
2
|
import { useT } from "@agent-native/core/client/i18n";
|
|
3
3
|
import { isSelectableAudioInputDevice } from "@shared/media-device-selection";
|
|
4
4
|
import {
|
|
5
|
+
IconArrowLeft,
|
|
5
6
|
IconBrowser,
|
|
6
7
|
IconCamera,
|
|
7
8
|
IconChevronDown,
|
|
@@ -14,16 +15,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
14
15
|
|
|
15
16
|
import { CaptureInstallInlineLink } from "@/components/capture-install-options";
|
|
16
17
|
import { ImportMenu } from "@/components/import-menu";
|
|
17
|
-
import {
|
|
18
|
-
AlertDialog,
|
|
19
|
-
AlertDialogAction,
|
|
20
|
-
AlertDialogCancel,
|
|
21
|
-
AlertDialogContent,
|
|
22
|
-
AlertDialogDescription,
|
|
23
|
-
AlertDialogFooter,
|
|
24
|
-
AlertDialogHeader,
|
|
25
|
-
AlertDialogTitle,
|
|
26
|
-
} from "@/components/ui/alert-dialog";
|
|
27
18
|
import { Button } from "@/components/ui/button";
|
|
28
19
|
import {
|
|
29
20
|
Collapsible,
|
|
@@ -124,6 +115,69 @@ type SurfaceOption = {
|
|
|
124
115
|
|
|
125
116
|
const REQUEST_MIC_ACCESS_VALUE = "__clips_request_microphone_access__";
|
|
126
117
|
|
|
118
|
+
function MicOffConfirmation({
|
|
119
|
+
onBack,
|
|
120
|
+
onUnmute,
|
|
121
|
+
onContinue,
|
|
122
|
+
}: {
|
|
123
|
+
onBack: () => void;
|
|
124
|
+
onUnmute: () => void;
|
|
125
|
+
onContinue: () => void;
|
|
126
|
+
}) {
|
|
127
|
+
const t = useT();
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className="fixed inset-0 z-50 flex min-h-screen flex-col overflow-y-auto bg-background text-foreground">
|
|
131
|
+
<header className="flex shrink-0 items-center px-6 py-5 sm:px-10">
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
onClick={onBack}
|
|
135
|
+
className="inline-flex items-center gap-2 rounded-lg px-1 py-1 text-lg font-semibold transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
136
|
+
>
|
|
137
|
+
<IconArrowLeft className="h-5 w-5 rtl:-scale-x-100" />
|
|
138
|
+
{t("recordingPage.back")}
|
|
139
|
+
</button>
|
|
140
|
+
</header>
|
|
141
|
+
|
|
142
|
+
<main className="flex flex-1 flex-col items-center justify-center px-6 py-12 text-center sm:py-16">
|
|
143
|
+
<div
|
|
144
|
+
className="relative flex h-28 w-28 items-center justify-center rounded-full bg-destructive/10 text-destructive"
|
|
145
|
+
aria-hidden="true"
|
|
146
|
+
>
|
|
147
|
+
<IconMicrophone className="h-12 w-12" strokeWidth={1.75} />
|
|
148
|
+
<span className="absolute h-1 w-20 rotate-45 rounded-full bg-destructive" />
|
|
149
|
+
</div>
|
|
150
|
+
<h1 className="mt-10 text-3xl font-semibold tracking-tight sm:text-4xl">
|
|
151
|
+
{t("preRecord.micOffConfirmTitle")}
|
|
152
|
+
</h1>
|
|
153
|
+
<p className="mt-4 max-w-md text-base leading-7 text-muted-foreground sm:text-lg">
|
|
154
|
+
{t("preRecord.micOffConfirmDescription")}
|
|
155
|
+
</p>
|
|
156
|
+
</main>
|
|
157
|
+
|
|
158
|
+
<footer className="flex shrink-0 justify-center border-t border-border bg-muted/20 px-6 py-6 sm:px-10">
|
|
159
|
+
<div className="flex w-full max-w-md gap-4">
|
|
160
|
+
<Button
|
|
161
|
+
type="button"
|
|
162
|
+
variant="outline"
|
|
163
|
+
onClick={onUnmute}
|
|
164
|
+
className="h-12 flex-1 rounded-xl text-base font-semibold"
|
|
165
|
+
>
|
|
166
|
+
{t("preRecord.unmuteMicrophone")}
|
|
167
|
+
</Button>
|
|
168
|
+
<Button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={onContinue}
|
|
171
|
+
className="h-12 flex-1 rounded-xl text-base font-semibold"
|
|
172
|
+
>
|
|
173
|
+
{t("preRecord.startWithoutMic")}
|
|
174
|
+
</Button>
|
|
175
|
+
</div>
|
|
176
|
+
</footer>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
127
181
|
function isPseudoMediaDeviceId(value: string | null | undefined): boolean {
|
|
128
182
|
const id = value?.trim().toLowerCase();
|
|
129
183
|
return !id || id === "default" || id === "communications";
|
|
@@ -465,6 +519,20 @@ export function PreRecordPanel({
|
|
|
465
519
|
onStart(buildStartOpts());
|
|
466
520
|
}, [micId, buildStartOpts, onStart]);
|
|
467
521
|
|
|
522
|
+
const handleMicWarningBack = useCallback(() => {
|
|
523
|
+
setMicWarningOpen(false);
|
|
524
|
+
}, []);
|
|
525
|
+
|
|
526
|
+
const handleMicWarningUnmute = useCallback(() => {
|
|
527
|
+
chooseMic("default");
|
|
528
|
+
setMicWarningOpen(false);
|
|
529
|
+
}, [chooseMic]);
|
|
530
|
+
|
|
531
|
+
const handleMicWarningContinue = useCallback(() => {
|
|
532
|
+
setMicWarningOpen(false);
|
|
533
|
+
onStart(buildStartOpts());
|
|
534
|
+
}, [buildStartOpts, onStart]);
|
|
535
|
+
|
|
468
536
|
const selectedCameraLabel = useMemo(() => {
|
|
469
537
|
if (!needsCamera) return null;
|
|
470
538
|
if (cameraId === "default") return t("preRecord.defaultCamera");
|
|
@@ -619,6 +687,16 @@ export function PreRecordPanel({
|
|
|
619
687
|
return null;
|
|
620
688
|
}, [audioEnabled, cameraTest.status, micTest.status, needsCamera, t]);
|
|
621
689
|
|
|
690
|
+
if (micWarningOpen) {
|
|
691
|
+
return (
|
|
692
|
+
<MicOffConfirmation
|
|
693
|
+
onBack={handleMicWarningBack}
|
|
694
|
+
onUnmute={handleMicWarningUnmute}
|
|
695
|
+
onContinue={handleMicWarningContinue}
|
|
696
|
+
/>
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
622
700
|
return (
|
|
623
701
|
<div className="mx-auto w-full max-w-lg overflow-hidden rounded-2xl border border-border bg-card shadow-lg">
|
|
624
702
|
<div className="p-6">
|
|
@@ -965,31 +1043,6 @@ export function PreRecordPanel({
|
|
|
965
1043
|
</>
|
|
966
1044
|
)}
|
|
967
1045
|
</div>
|
|
968
|
-
|
|
969
|
-
<AlertDialog open={micWarningOpen} onOpenChange={setMicWarningOpen}>
|
|
970
|
-
<AlertDialogContent>
|
|
971
|
-
<AlertDialogHeader>
|
|
972
|
-
<AlertDialogTitle>
|
|
973
|
-
{t("preRecord.micOffConfirmTitle")}
|
|
974
|
-
</AlertDialogTitle>
|
|
975
|
-
<AlertDialogDescription>
|
|
976
|
-
{t("preRecord.micOffConfirmDescription")}
|
|
977
|
-
</AlertDialogDescription>
|
|
978
|
-
</AlertDialogHeader>
|
|
979
|
-
<AlertDialogFooter>
|
|
980
|
-
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
|
981
|
-
<AlertDialogAction
|
|
982
|
-
onClick={(event) => {
|
|
983
|
-
event.preventDefault();
|
|
984
|
-
setMicWarningOpen(false);
|
|
985
|
-
onStart(buildStartOpts());
|
|
986
|
-
}}
|
|
987
|
-
>
|
|
988
|
-
{t("preRecord.startWithoutMic")}
|
|
989
|
-
</AlertDialogAction>
|
|
990
|
-
</AlertDialogFooter>
|
|
991
|
-
</AlertDialogContent>
|
|
992
|
-
</AlertDialog>
|
|
993
1046
|
</div>
|
|
994
1047
|
);
|
|
995
1048
|
}
|
|
@@ -505,6 +505,8 @@ export class RecorderEngine {
|
|
|
505
505
|
private cameraComposite: CameraCompositeHandle | null = null;
|
|
506
506
|
private audioMixCtx: AudioContext | null = null;
|
|
507
507
|
private audioMixSources: MediaStreamAudioSourceNode[] = [];
|
|
508
|
+
private wakeLock: WakeLockSentinel | null = null;
|
|
509
|
+
private wakeLockGeneration = 0;
|
|
508
510
|
private recorder: MediaRecorder | null = null;
|
|
509
511
|
private mimeType: string = "video/webm";
|
|
510
512
|
|
|
@@ -890,7 +892,7 @@ export class RecorderEngine {
|
|
|
890
892
|
this.opts.displaySurface ?? "window",
|
|
891
893
|
);
|
|
892
894
|
const displayOptions: ExtendedDisplayMediaOptions =
|
|
893
|
-
screenCaptureDisplayOptions(displaySurface);
|
|
895
|
+
screenCaptureDisplayOptions(displaySurface, wantsMic);
|
|
894
896
|
|
|
895
897
|
if (wantsMic || wantsDisplay) {
|
|
896
898
|
this.audioMixCtx?.close().catch(() => {});
|
|
@@ -904,6 +906,14 @@ export class RecorderEngine {
|
|
|
904
906
|
} catch (err) {
|
|
905
907
|
throw this.friendlyError(err, "screen");
|
|
906
908
|
}
|
|
909
|
+
// The OS is as free to sleep/lock the display during a pause as
|
|
910
|
+
// during active recording, which ends getDisplayMedia's video track
|
|
911
|
+
// outright (unlike the mic/camera, display capture requires an
|
|
912
|
+
// active compositor). Without this, a paused recording left idle
|
|
913
|
+
// finalizes early with no warning — see onDisplayTrackEnded below.
|
|
914
|
+
// Best-effort: unsupported browsers or a denied request must never
|
|
915
|
+
// block capture.
|
|
916
|
+
void this.acquireWakeLock();
|
|
907
917
|
}
|
|
908
918
|
|
|
909
919
|
if (wantsCamera) {
|
|
@@ -2360,9 +2370,34 @@ export class RecorderEngine {
|
|
|
2360
2370
|
);
|
|
2361
2371
|
}
|
|
2362
2372
|
|
|
2373
|
+
/** Best-effort — unsupported browsers or a denied request must never block capture. */
|
|
2374
|
+
private async acquireWakeLock(): Promise<void> {
|
|
2375
|
+
const generation = ++this.wakeLockGeneration;
|
|
2376
|
+
try {
|
|
2377
|
+
// coercion-ok: optional chaining yields undefined only when the Wake
|
|
2378
|
+
// Lock API itself is absent (older/other browsers) — a real rejection
|
|
2379
|
+
// (denied request) is caught below, not coerced here.
|
|
2380
|
+
const wakeLock = (await navigator.wakeLock?.request?.("screen")) ?? null;
|
|
2381
|
+
if (generation !== this.wakeLockGeneration || !this.displayStream) {
|
|
2382
|
+
wakeLock?.release().catch(() => {});
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
this.wakeLock = wakeLock;
|
|
2386
|
+
} catch {
|
|
2387
|
+
if (generation === this.wakeLockGeneration) this.wakeLock = null;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
private releaseWakeLock(): void {
|
|
2392
|
+
this.wakeLockGeneration += 1;
|
|
2393
|
+
this.wakeLock?.release().catch(() => {});
|
|
2394
|
+
this.wakeLock = null;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2363
2397
|
private cleanupTracks(): void {
|
|
2364
2398
|
// Clear before stopping tracks so the `ended` events our own stop() fires
|
|
2365
2399
|
// aren't mistaken for a disconnect.
|
|
2400
|
+
this.releaseWakeLock();
|
|
2366
2401
|
this.cameraLive = false;
|
|
2367
2402
|
this.audioMixSources = [];
|
|
2368
2403
|
this.audioMixCtx?.close().catch(() => {});
|
|
@@ -1335,10 +1335,11 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
1335
1335
|
cameraOff: "Camera off",
|
|
1336
1336
|
includeCameraAria: "Include camera in this recording",
|
|
1337
1337
|
startRecording: "Start recording",
|
|
1338
|
-
micOffConfirmTitle: "
|
|
1338
|
+
micOffConfirmTitle: "Your mic is muted",
|
|
1339
1339
|
micOffConfirmDescription:
|
|
1340
|
-
"
|
|
1341
|
-
startWithoutMic: "
|
|
1340
|
+
"To have sound in your video, you’ll need to unmute your microphone.",
|
|
1341
|
+
startWithoutMic: "Continue",
|
|
1342
|
+
unmuteMicrophone: "Unmute",
|
|
1342
1343
|
uploadVideo: "Upload video",
|
|
1343
1344
|
importLoom: "Import Loom",
|
|
1344
1345
|
importing: "Importing...",
|
|
@@ -37,16 +37,6 @@ import {
|
|
|
37
37
|
useState,
|
|
38
38
|
} from "react";
|
|
39
39
|
|
|
40
|
-
import {
|
|
41
|
-
AlertDialog,
|
|
42
|
-
AlertDialogAction,
|
|
43
|
-
AlertDialogCancel,
|
|
44
|
-
AlertDialogContent,
|
|
45
|
-
AlertDialogDescription,
|
|
46
|
-
AlertDialogFooter,
|
|
47
|
-
AlertDialogHeader,
|
|
48
|
-
AlertDialogTitle,
|
|
49
|
-
} from "./components/AlertDialog";
|
|
50
40
|
import { FeedbackButton } from "./components/FeedbackButton";
|
|
51
41
|
import {
|
|
52
42
|
CamIcon,
|
|
@@ -58,6 +48,7 @@ import {
|
|
|
58
48
|
SettingsIcon,
|
|
59
49
|
} from "./components/Icons";
|
|
60
50
|
import { MediaDeviceRow } from "./components/MediaDeviceRow";
|
|
51
|
+
import { MicOffConfirmation } from "./components/MicOffConfirmation";
|
|
61
52
|
import { ReadinessPanel } from "./components/ReadinessPanel";
|
|
62
53
|
import { SourceRow, type CaptureSource } from "./components/SourceRow";
|
|
63
54
|
import { Switch } from "./components/Switch";
|
|
@@ -3333,6 +3324,22 @@ export function App() {
|
|
|
3333
3324
|
void handleStartRecording(options);
|
|
3334
3325
|
}
|
|
3335
3326
|
|
|
3327
|
+
function closeMicOffConfirmation() {
|
|
3328
|
+
pendingStartOptionsRef.current = undefined;
|
|
3329
|
+
setMicOffConfirmOpen(false);
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
function unmuteFromConfirmation() {
|
|
3333
|
+
setMicOn(true);
|
|
3334
|
+
closeMicOffConfirmation();
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
function continueWithoutMic() {
|
|
3338
|
+
const options = pendingStartOptionsRef.current;
|
|
3339
|
+
closeMicOffConfirmation();
|
|
3340
|
+
void handleStartRecording(options);
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3336
3343
|
recordShortcutHandlerRef.current = () => {
|
|
3337
3344
|
if (recorder) {
|
|
3338
3345
|
emit("clips:recorder-stop").catch(() => {});
|
|
@@ -3960,7 +3967,19 @@ export function App() {
|
|
|
3960
3967
|
|
|
3961
3968
|
return (
|
|
3962
3969
|
<div className="app app-recorder" ref={appRef}>
|
|
3963
|
-
|
|
3970
|
+
{micOffConfirmOpen ? (
|
|
3971
|
+
<MicOffConfirmation
|
|
3972
|
+
onBack={closeMicOffConfirmation}
|
|
3973
|
+
onUnmute={unmuteFromConfirmation}
|
|
3974
|
+
onContinue={continueWithoutMic}
|
|
3975
|
+
/>
|
|
3976
|
+
) : null}
|
|
3977
|
+
|
|
3978
|
+
<div
|
|
3979
|
+
className="recorder-home-content"
|
|
3980
|
+
hidden={micOffConfirmOpen}
|
|
3981
|
+
aria-hidden={micOffConfirmOpen}
|
|
3982
|
+
>
|
|
3964
3983
|
<Header
|
|
3965
3984
|
mode={mode}
|
|
3966
3985
|
onModeChange={setMode}
|
|
@@ -4077,35 +4096,6 @@ export function App() {
|
|
|
4077
4096
|
</button>
|
|
4078
4097
|
) : null}
|
|
4079
4098
|
|
|
4080
|
-
<AlertDialog
|
|
4081
|
-
open={micOffConfirmOpen}
|
|
4082
|
-
onOpenChange={(open) => {
|
|
4083
|
-
setMicOffConfirmOpen(open);
|
|
4084
|
-
if (!open) pendingStartOptionsRef.current = undefined;
|
|
4085
|
-
}}
|
|
4086
|
-
>
|
|
4087
|
-
<AlertDialogContent>
|
|
4088
|
-
<AlertDialogHeader>
|
|
4089
|
-
<AlertDialogTitle>Record without a microphone?</AlertDialogTitle>
|
|
4090
|
-
<AlertDialogDescription>
|
|
4091
|
-
Your mic is off, so this recording won't capture any audio.
|
|
4092
|
-
Turn it on before starting if you want narration.
|
|
4093
|
-
</AlertDialogDescription>
|
|
4094
|
-
</AlertDialogHeader>
|
|
4095
|
-
<AlertDialogFooter>
|
|
4096
|
-
<AlertDialogAction
|
|
4097
|
-
onClick={() => {
|
|
4098
|
-
const options = pendingStartOptionsRef.current;
|
|
4099
|
-
pendingStartOptionsRef.current = undefined;
|
|
4100
|
-
void handleStartRecording(options);
|
|
4101
|
-
}}
|
|
4102
|
-
>
|
|
4103
|
-
Start anyway
|
|
4104
|
-
</AlertDialogAction>
|
|
4105
|
-
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
4106
|
-
</AlertDialogFooter>
|
|
4107
|
-
</AlertDialogContent>
|
|
4108
|
-
</AlertDialog>
|
|
4109
4099
|
{recError ? (
|
|
4110
4100
|
recError === MACOS_UPDATE_RESTART_MESSAGE ? (
|
|
4111
4101
|
<UpdateRestartBanner message={recError} />
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { IconArrowLeft, IconMicrophone2 } from "@tabler/icons-react";
|
|
2
|
+
|
|
3
|
+
export function MicOffConfirmation({
|
|
4
|
+
onBack,
|
|
5
|
+
onUnmute,
|
|
6
|
+
onContinue,
|
|
7
|
+
}: {
|
|
8
|
+
onBack: () => void;
|
|
9
|
+
onUnmute: () => void;
|
|
10
|
+
onContinue: () => void;
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<section className="mic-off-confirmation" aria-labelledby="mic-off-title">
|
|
14
|
+
<header className="mic-off-confirmation-header">
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
className="mic-off-confirmation-back"
|
|
18
|
+
onClick={onBack}
|
|
19
|
+
>
|
|
20
|
+
<IconArrowLeft size={18} aria-hidden="true" />
|
|
21
|
+
Back
|
|
22
|
+
</button>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<div className="mic-off-confirmation-body">
|
|
26
|
+
<div className="mic-off-confirmation-visual" aria-hidden="true">
|
|
27
|
+
<IconMicrophone2 size={42} strokeWidth={1.7} />
|
|
28
|
+
<span className="mic-off-confirmation-slash" />
|
|
29
|
+
</div>
|
|
30
|
+
<h2 id="mic-off-title">Your mic is muted</h2>
|
|
31
|
+
<p>
|
|
32
|
+
To have sound in your video, you'll need to unmute your
|
|
33
|
+
microphone.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<footer className="mic-off-confirmation-actions">
|
|
38
|
+
<button type="button" className="secondary" onClick={onUnmute}>
|
|
39
|
+
Unmute
|
|
40
|
+
</button>
|
|
41
|
+
<button type="button" className="primary" onClick={onContinue}>
|
|
42
|
+
Continue
|
|
43
|
+
</button>
|
|
44
|
+
</footer>
|
|
45
|
+
</section>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -3150,6 +3150,116 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
3150
3150
|
}
|
|
3151
3151
|
}
|
|
3152
3152
|
|
|
3153
|
+
/* ------------------------------------------------------------------------- */
|
|
3154
|
+
/* Microphone-off confirmation */
|
|
3155
|
+
/* ------------------------------------------------------------------------- */
|
|
3156
|
+
|
|
3157
|
+
.mic-off-confirmation {
|
|
3158
|
+
min-height: 430px;
|
|
3159
|
+
display: flex;
|
|
3160
|
+
flex-direction: column;
|
|
3161
|
+
background: var(--bg);
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
.mic-off-confirmation-header {
|
|
3165
|
+
display: flex;
|
|
3166
|
+
align-items: center;
|
|
3167
|
+
padding: 16px 18px 8px;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
.mic-off-confirmation-back {
|
|
3171
|
+
display: inline-flex;
|
|
3172
|
+
align-items: center;
|
|
3173
|
+
gap: 6px;
|
|
3174
|
+
min-height: 30px;
|
|
3175
|
+
padding: 4px 6px 4px 2px;
|
|
3176
|
+
border: 0;
|
|
3177
|
+
border-radius: var(--radius-sm);
|
|
3178
|
+
background: transparent;
|
|
3179
|
+
color: var(--fg);
|
|
3180
|
+
cursor: pointer;
|
|
3181
|
+
font-size: 15px;
|
|
3182
|
+
font-weight: 650;
|
|
3183
|
+
transition:
|
|
3184
|
+
background 120ms,
|
|
3185
|
+
color 120ms;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
.mic-off-confirmation-back:hover {
|
|
3189
|
+
background: var(--surface-hover);
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
.mic-off-confirmation-back:focus-visible {
|
|
3193
|
+
outline: none;
|
|
3194
|
+
box-shadow: 0 0 0 3px var(--brand-ring);
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
.mic-off-confirmation-body {
|
|
3198
|
+
display: flex;
|
|
3199
|
+
flex: 1 1 auto;
|
|
3200
|
+
flex-direction: column;
|
|
3201
|
+
align-items: center;
|
|
3202
|
+
justify-content: center;
|
|
3203
|
+
padding: 24px 28px 30px;
|
|
3204
|
+
text-align: center;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
.mic-off-confirmation-visual {
|
|
3208
|
+
position: relative;
|
|
3209
|
+
display: flex;
|
|
3210
|
+
align-items: center;
|
|
3211
|
+
justify-content: center;
|
|
3212
|
+
width: 88px;
|
|
3213
|
+
height: 88px;
|
|
3214
|
+
border-radius: 50%;
|
|
3215
|
+
background: var(--surface-strong);
|
|
3216
|
+
color: var(--status-warn);
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
.mic-off-confirmation-slash {
|
|
3220
|
+
position: absolute;
|
|
3221
|
+
width: 58px;
|
|
3222
|
+
height: 3px;
|
|
3223
|
+
border-radius: var(--radius-pill);
|
|
3224
|
+
background: var(--status-warn);
|
|
3225
|
+
transform: rotate(45deg);
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
.mic-off-confirmation-body h2 {
|
|
3229
|
+
margin: 24px 0 0;
|
|
3230
|
+
font-size: 22px;
|
|
3231
|
+
font-weight: 700;
|
|
3232
|
+
letter-spacing: -0.02em;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
.mic-off-confirmation-body p {
|
|
3236
|
+
max-width: 300px;
|
|
3237
|
+
margin: 8px 0 0;
|
|
3238
|
+
color: var(--fg-muted);
|
|
3239
|
+
font-size: 14px;
|
|
3240
|
+
line-height: 1.55;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
.mic-off-confirmation-actions {
|
|
3244
|
+
display: grid;
|
|
3245
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
3246
|
+
gap: 10px;
|
|
3247
|
+
padding: 16px 18px 18px;
|
|
3248
|
+
border-top: 1px solid var(--border);
|
|
3249
|
+
background: var(--surface);
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
.mic-off-confirmation-actions .primary,
|
|
3253
|
+
.mic-off-confirmation-actions .secondary {
|
|
3254
|
+
width: auto;
|
|
3255
|
+
min-width: 0;
|
|
3256
|
+
height: 42px;
|
|
3257
|
+
padding: 0 12px;
|
|
3258
|
+
border-radius: var(--radius);
|
|
3259
|
+
font-size: 13px;
|
|
3260
|
+
white-space: nowrap;
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3153
3263
|
/* ------------------------------------------------------------------------- */
|
|
3154
3264
|
/* Alert dialog */
|
|
3155
3265
|
/* ------------------------------------------------------------------------- */
|