@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.
Files changed (113) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -2
  4. package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
  5. package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
  6. package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
  7. package/corpus/templates/calendar/server/handlers/google-auth.ts +1 -8
  8. package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  9. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +88 -35
  10. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +36 -1
  11. package/corpus/templates/clips/app/i18n/en-US.ts +4 -3
  12. package/corpus/templates/clips/desktop/src/app.tsx +30 -40
  13. package/corpus/templates/clips/desktop/src/components/MicOffConfirmation.tsx +47 -0
  14. package/corpus/templates/clips/desktop/src/styles.css +110 -0
  15. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +114 -7
  16. package/corpus/templates/clips/shared/recording-capture.ts +18 -13
  17. package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
  18. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -5
  19. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +0 -3
  20. package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +0 -4
  21. package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +4 -1
  22. package/corpus/templates/content/app/hooks/use-notion.ts +17 -15
  23. package/corpus/templates/crm/.agents/skills/real-time-sync/SKILL.md +1 -1
  24. package/corpus/templates/design/.agents/skills/real-time-sync/SKILL.md +1 -1
  25. package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
  26. package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
  27. package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
  28. package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
  29. package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
  30. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +18 -3
  31. package/corpus/templates/mail/server/handlers/google-auth.ts +1 -8
  32. package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
  33. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +4 -0
  34. package/corpus/templates/slides/.agents/skills/real-time-sync/SKILL.md +1 -1
  35. package/corpus/templates/slides/actions/_await-fit-check.ts +30 -5
  36. package/corpus/templates/slides/actions/add-slide.ts +6 -2
  37. package/corpus/templates/slides/actions/create-deck.ts +2 -1
  38. package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -0
  39. package/corpus/templates/slides/app/pages/DeckEditor.tsx +34 -1
  40. package/corpus/templates/slides/app/routes/p.$id.tsx +3 -1
  41. package/corpus/templates/slides/changelog/2026-08-18-generated-slides-preserve-speaker-notes.md +6 -0
  42. package/corpus/templates/slides/server/handlers/assets.ts +23 -12
  43. package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +15 -6
  44. package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +15 -6
  45. package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +34 -19
  46. package/corpus/templates/slides/server/handlers/share.ts +1 -1
  47. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  48. package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +1 -1
  49. package/corpus/templates/slides/server/routes/api/exports/[filename].get.ts +9 -3
  50. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -5
  51. package/corpus/templates/slides/server/routes/api/exports/html.post.ts +9 -7
  52. package/corpus/templates/slides/server/routes/api/exports/pptx.post.ts +9 -7
  53. package/corpus/templates/slides/shared/api.ts +7 -1
  54. package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
  55. package/dist/agent/engine/translate-anthropic.js +27 -2
  56. package/dist/agent/production-agent.d.ts +2 -2
  57. package/dist/agent/production-agent.js +16 -8
  58. package/dist/agent/thread-data-builder.d.ts +1 -0
  59. package/dist/agent/thread-data-builder.js +6 -4
  60. package/dist/agent/tool-approval-store.d.ts +10 -1
  61. package/dist/agent/tool-approval-store.js +19 -2
  62. package/dist/agent/types.d.ts +9 -0
  63. package/dist/client/AssistantChat.js +14 -6
  64. package/dist/client/EnvironmentBadge.d.ts +15 -0
  65. package/dist/client/EnvironmentBadge.js +79 -0
  66. package/dist/client/MultiTabAssistantChat.js +73 -17
  67. package/dist/client/agent-chat-adapter.js +11 -1
  68. package/dist/client/api-path.js +14 -1
  69. package/dist/client/app-providers.js +2 -1
  70. package/dist/client/chat/tool-call-display.d.ts +12 -2
  71. package/dist/client/chat/tool-call-display.js +17 -7
  72. package/dist/client/chat/widgets/WorkspaceFileWidget.js +1 -1
  73. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +6 -0
  74. package/dist/client/chat/widgets/builtin-tool-renderers.js +19 -0
  75. package/dist/client/onboarding/FirstRunOnboarding.js +46 -32
  76. package/dist/client/onboarding/use-onboarding.d.ts +8 -1
  77. package/dist/client/onboarding/use-onboarding.js +22 -7
  78. package/dist/client/session-replay.js +58 -15
  79. package/dist/client/settings/SettingsTabsPage.js +19 -6
  80. package/dist/client/sse-event-processor.d.ts +7 -0
  81. package/dist/client/sse-event-processor.js +4 -1
  82. package/dist/client/ui/index.d.ts +1 -0
  83. package/dist/client/ui/index.js +1 -0
  84. package/dist/client/use-action.js +26 -0
  85. package/dist/collab/awareness.d.ts +2 -2
  86. package/dist/collab/struct-routes.d.ts +1 -1
  87. package/dist/config.d.ts +15 -0
  88. package/dist/config.js +53 -0
  89. package/dist/extensions/fetch-tool.js +8 -2
  90. package/dist/mcp/embed-app.js +1 -1
  91. package/dist/notifications/routes.d.ts +1 -1
  92. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  93. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  94. package/dist/provider-api/index.d.ts +10 -0
  95. package/dist/provider-api/index.js +45 -9
  96. package/dist/server/auth.js +10 -3
  97. package/dist/server/poll.js +7 -3
  98. package/dist/server/sentry.d.ts +1 -1
  99. package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  100. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  101. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
  102. package/dist/vite/client.d.ts +5 -0
  103. package/dist/vite/client.js +64 -2
  104. package/dist/workspace-files/actions.d.ts +1 -8
  105. package/dist/workspace-files/actions.js +5 -21
  106. package/dist/workspace-files/index.d.ts +1 -1
  107. package/dist/workspace-files/index.js +1 -1
  108. package/dist/workspace-files/store.d.ts +18 -0
  109. package/dist/workspace-files/store.js +24 -3
  110. package/package.json +3 -3
  111. package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  112. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  113. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8321
34
+ - template files: 8323
@@ -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 runs every 2 seconds until SSE is connected, then relaxes to 15 seconds (`SSE_FALLBACK_INTERVAL_MS`). If SSE is disabled or unavailable (e.g., edge/serverless deployments), polling continues at the 2 s cadence. 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.
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" || value.trim() === "") return null;
1627
- const numeric = Number(value);
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 runs every 2 seconds until SSE is connected, then relaxes to 15 seconds (`SSE_FALLBACK_INTERVAL_MS`). If SSE is disabled or unavailable (e.g., edge/serverless deployments), polling continues at the 2 s cadence. 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.
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 runs every 2 seconds until SSE is connected, then relaxes to 15 seconds (`SSE_FALLBACK_INTERVAL_MS`). If SSE is disabled or unavailable (e.g., edge/serverless deployments), polling continues at the 2 s cadence. 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.
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 runs every 2 seconds until SSE is connected, then relaxes to 15 seconds (`SSE_FALLBACK_INTERVAL_MS`). If SSE is disabled or unavailable (e.g., edge/serverless deployments), polling continues at the 2 s cadence. 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.
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 runs every 2 seconds until SSE is connected, then relaxes to 15 seconds (`SSE_FALLBACK_INTERVAL_MS`). If SSE is disabled or unavailable (e.g., edge/serverless deployments), polling continues at the 2 s cadence. 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.
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: "Record without a microphone?",
1338
+ micOffConfirmTitle: "Your mic is muted",
1339
1339
  micOffConfirmDescription:
1340
- "Your mic is off, so this recording won't capture any audio. Turn it on before starting if you want narration.",
1341
- startWithoutMic: "Start anyway",
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
- <div className="recorder-home-content">
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&apos;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&apos;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
  /* ------------------------------------------------------------------------- */