@agent-native/core 0.153.10 → 0.154.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.md +20 -30
- package/corpus/templates/analytics/actions/search-dashboard-references.ts +58 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/analytics/app/lib/utils.ts +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +1 -1
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +2 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +366 -3
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +3 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -2
- package/corpus/templates/calendar/app/components/calendar/EventDialog.tsx +1 -1
- package/corpus/templates/calendar/app/lib/utils.ts +1 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +32 -5
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +15 -15
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
- package/corpus/templates/clips/app/lib/utils.ts +5 -6
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +43 -8
- package/corpus/templates/clips/desktop/src/app.tsx +26 -12
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +23 -9
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +261 -11
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +21 -3
- package/corpus/templates/clips/shared/mic-audio-cleanup.ts +350 -0
- package/corpus/templates/content/app/i18n-data.ts +10 -10
- package/corpus/templates/content/scripts/check-native-deps.mjs +181 -15
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +79 -7
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +14 -2
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +2 -2
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +35 -11
- package/corpus/templates/design/actions/get-design-system.ts +103 -3
- package/corpus/templates/design/actions/import-design-tokens.ts +37 -8
- package/corpus/templates/design/actions/import-github.ts +32 -138
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +49 -16
- package/corpus/templates/design/actions/present-design-variants.ts +93 -4
- package/corpus/templates/design/actions/run-design-audit.ts +229 -0
- package/corpus/templates/design/actions/sync-design-system-with-builder.ts +64 -0
- package/corpus/templates/design/app/components/design/code-workbench/commands.ts +2 -2
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +17 -4
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +5 -1
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +5 -1
- package/corpus/templates/design/app/hooks/use-question-flow.ts +81 -5
- package/corpus/templates/design/app/i18n/en-US.ts +1 -1
- package/corpus/templates/design/app/i18n-data.ts +32 -1
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +10 -10
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +83 -11
- package/corpus/templates/design/app/pages/DesignSystems.tsx +14 -0
- package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +22 -0
- package/corpus/templates/design/changelog/2026-08-13-a-screen-that-would-render-behind-an-invisible-alpine-overla.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-attach-a-ui-screenshot-and-the-design-is-reproduced-not-rein.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-linking-a-design-system-means-you-never-get-generic-directio.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-reports-when-a-screen-ignores-its-linke.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-the-design-system-preview-now-shows-how-many-named-tokens-we.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-your-design-system-prompt-and-reference-screenshots-now-reac.md +6 -0
- package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +18 -2
- package/corpus/templates/design/shared/design-review.ts +1 -0
- package/corpus/templates/design/shared/html-integrity.ts +171 -0
- package/corpus/templates/mail/app/lib/utils.ts +1 -1
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +10 -6
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +41 -3
- package/corpus/templates/slides/actions/sync-design-system-with-builder.ts +64 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +193 -20
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +58 -33
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +28 -41
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +119 -91
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +16 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +158 -31
- package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +30 -14
- package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +4 -1
- package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +14 -0
- package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +24 -0
- package/corpus/templates/slides/app/global.css +24 -0
- package/corpus/templates/slides/app/hooks/use-navigation-state.ts +5 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +19 -10
- package/corpus/templates/slides/app/lib/deck-filter.ts +47 -0
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +15 -4
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +637 -1
- package/corpus/templates/slides/app/lib/image-download.ts +84 -0
- package/corpus/templates/slides/app/lib/utils.ts +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +25 -9
- package/corpus/templates/slides/netlify.toml +1 -0
- package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +14 -2
- package/dist/agent/production-agent.js +1 -1
- package/dist/brand-kit/types.d.ts +10 -2
- package/dist/cli/code.js +1 -1
- package/dist/cli/index.js +30 -1
- package/dist/cli/native-dependencies.d.ts +24 -0
- package/dist/cli/native-dependencies.js +264 -0
- package/dist/cli/plan-local.js +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.js +2 -2
- package/dist/cli/visualize-repo.js +1 -1
- package/dist/client/FeedbackButton.js +12 -12
- package/dist/client/agent-chat-adapter.js +3 -4
- package/dist/client/assistant-ui-recovery.d.ts +1 -1
- package/dist/client/assistant-ui-recovery.js +4 -0
- package/dist/client/chat/message-components.d.ts +11 -3
- package/dist/client/chat/message-components.js +31 -5
- package/dist/client/chat/tool-call-display.d.ts +2 -1
- package/dist/client/chat/tool-call-display.js +33 -15
- package/dist/client/db-admin/SqlEditor.js +1 -1
- package/dist/client/dev-overlay/DevOverlay.js +2 -2
- package/dist/client/extensions/portable-extension.js +4 -0
- package/dist/client/guided-questions.d.ts +8 -0
- package/dist/client/guided-questions.js +12 -3
- package/dist/client/sharing/ShareButton.js +16 -24
- package/dist/client/sse-event-processor.js +35 -15
- package/dist/client/tool-display.d.ts +9 -0
- package/dist/client/tool-display.js +39 -0
- package/dist/client/vite-dev-recovery-script.js +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/html-shell.js +5 -0
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-chat/prompt-resources.js +17 -16
- package/dist/server/agent-chat-plugin.d.ts +9 -0
- package/dist/server/agent-chat-plugin.js +29 -7
- package/dist/server/builder-design-systems.d.ts +33 -1
- package/dist/server/builder-design-systems.js +293 -11
- package/dist/server/design-token-utils.d.ts +11 -0
- package/dist/server/design-token-utils.js +68 -16
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/package.json +3 -3
|
@@ -105,6 +105,9 @@ function hasPartialDashboardBuild(
|
|
|
105
105
|
export const BOUNDED_STRUCTURED_LOOKUP_GUIDANCE =
|
|
106
106
|
"BOUNDED STRUCTURED LOOKUP FAST PATH — Treat existing analytics work like an engineer treats existing code: grep before writing. For an ordinary count, aggregate, grouped metric, trend, or record lookup, first call `search-analytics-query-catalog` once with focused metric/entity terms. It searches accessible dashboard names, chart titles/descriptions/saved queries, shipped dashboard patterns, and data-dictionary definitions together. Prefer the strongest approved dictionary or saved-chart match, preserve its source and business logic, adapt only the requested filters and explicit time window, then run one bounded query against that source. A user-named source wins, but still use a matching saved definition when it supplies the source's proven query shape. If there is no useful match, inspect only the most likely source schema or ask one clarification; do not fan out across providers. Do not separately list every dashboard, call data-source status, browse the whole dictionary, load provider catalogs/corpus tools, or query a second source after a strong match. Once the query succeeds, answer immediately with its source, time window, filters, row count, and only necessary caveats. Do not enrich, cross-check, retry, or add breakdowns unless the user requested them, the first query failed, or its result conflicts with the known definition. The words `all`, `total`, or `exact` in a structured aggregate do not by themselves make it a corpus investigation. Never repeat an identical invalid or failed tool call; correct its arguments once or surface the error. This does not waive the real-data requirement: never answer from a guess, stale value, or unverified result. ";
|
|
107
107
|
|
|
108
|
+
export const DASHBOARD_REFERENCE_GUIDANCE =
|
|
109
|
+
"DASHBOARD REFERENCE DISCOVERY — When the user asks to replicate, clone, or adapt an existing dashboard, this branch takes precedence over the ordinary metric fast path: call `search-dashboard-references` with focused terms before creating, editing, or querying anything. It searches accessible active saved dashboard ids, names, descriptions, and serialized config with bounded SQL wildcard matches, including legacy saved dashboards. Treat each result as a reference to inspect with `get-sql-dashboard` when `kind` is `sql` or `get-explorer-dashboard` when `kind` is `explorer`, not as proof that its source is authoritative for the new request. Do not automatically route a replication request to first-party Analytics or copy its source semantics without checking the user's requested provider and scope. ";
|
|
110
|
+
|
|
108
111
|
export const BUILT_IN_FIRST_PARTY_SOURCE_GUIDANCE =
|
|
109
112
|
"BUILT-IN FIRST-PARTY SOURCE — Analytics always provides one built-in first-party source alongside connected external providers such as BigQuery, HubSpot, Gong, Slack, and the other configured integrations. This does not replace or restrict external sources. When `search-analytics-query-catalog` identifies a first-party dashboard/chart definition, preserve its event semantics and use `query-agent-native-analytics` over `analytics_events` or `session_recordings` as appropriate. When the user names an external provider, or the catalog identifies one as authoritative, query that provider instead. Do not report the first-party source as disconnected merely because an external provider is not configured. If the authoritative query returns no rows, report that grounded result with its scope and time window. ";
|
|
110
113
|
|
|
@@ -146,11 +149,12 @@ export function analyticsSourceGuidanceOpening(): string {
|
|
|
146
149
|
// Measured in production: this ran in under 1% of data threads while the
|
|
147
150
|
// equivalent instruction sat ~7000 words deep. Threads that did call it used
|
|
148
151
|
// roughly a third the tool calls. Keep it first and keep it imperative.
|
|
149
|
-
"START HERE — For
|
|
152
|
+
"START HERE — For an ordinary metric, cohort, list, count, or trend question, your FIRST tool call is `search-analytics-query-catalog`. For a request to replicate, clone, or adapt an existing dashboard, your FIRST tool call is `search-dashboard-references` instead; inspect the returned dashboard before choosing a source or writing anything. The catalog path is the analytics equivalent of grepping a codebase before writing new code: someone has very likely already built and saved the query you need, and its saved SQL tells you the exact source, table, and column names so you do not have to discover them. Adapt the closest saved query to the requested filters and time window, run it once, and stop. Only fall back to schema discovery or provider catalogs when the catalog search returns nothing usable — and never run more than one schema-discovery pass before querying. " +
|
|
150
153
|
ANALYTICS_CROSS_APP_ROUTING_GUIDANCE +
|
|
151
154
|
'ONE BOUNDED CALL — List, filter, count, and cohort questions ("which X, excluding Y") are a single query, not a loop. Express the include filter, the exclude filter, and the aggregation in one SQL statement or one `run-code` script that filters server-side. Never page through a cohort across separate tool calls and never fan out per item to apply a filter; that is what turns a ten-second answer into a twenty-minute one. ' +
|
|
152
155
|
"Apply real-data requirements only when presenting analytics results, source records, or derived metrics. Do not call data-source tools for workflow migration, recurring-job setup, UI/code fixes, settings help, conceptual planning, or other non-data tasks unless the user explicitly asks for data. " +
|
|
153
156
|
NON_ANALYTICS_REQUEST_GUIDANCE +
|
|
157
|
+
DASHBOARD_REFERENCE_GUIDANCE +
|
|
154
158
|
BOUNDED_STRUCTURED_LOOKUP_GUIDANCE +
|
|
155
159
|
BUILT_IN_FIRST_PARTY_SOURCE_GUIDANCE +
|
|
156
160
|
ANALYTICS_OBSERVABILITY_INCIDENT_GUIDANCE +
|
|
@@ -761,7 +765,7 @@ export function realDataFinalGuard(
|
|
|
761
765
|
}
|
|
762
766
|
return {
|
|
763
767
|
retryMessage:
|
|
764
|
-
'This is a dashboard construction/template-clone request.
|
|
768
|
+
'This is a dashboard construction/template-clone request. First call `search-dashboard-references` with the named template terms. Inspect the matching result with `get-sql-dashboard` when `kind` is `sql` or `get-explorer-dashboard` when `kind` is `explorer`, using full config only when needed. If its panels are `chartType: "extension"`, use `get-extension` then `create-extension` to clone/adapt it, then `update-dashboard` to save the new dashboard. Do not invent SQL panels for an extension-backed template. Ask one clarifying filter question if needed. Only run a data-source query before presenting numbers or authoring invented SQL.',
|
|
765
769
|
fallbackMessage:
|
|
766
770
|
"I need to inspect the template dashboard (and its extension, if it uses one) before creating the new one. Tell me the template dashboard name, or confirm the org/account filter, and I'll clone it without inventing metrics.",
|
|
767
771
|
// Expand the tool surface so a corrective retry can always reach the
|
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
isLikelyPhoneMicLabel,
|
|
9
9
|
type AudioInputFallback,
|
|
10
10
|
} from "@shared/media-device-selection";
|
|
11
|
+
import {
|
|
12
|
+
createMicAudioCleanup,
|
|
13
|
+
type MicAudioCleanupHandle,
|
|
14
|
+
} from "@shared/mic-audio-cleanup";
|
|
11
15
|
import {
|
|
12
16
|
SCREEN_CAPTURE_FRAME_RATE,
|
|
13
17
|
screenCaptureDisplayOptions,
|
|
@@ -505,6 +509,7 @@ export class RecorderEngine {
|
|
|
505
509
|
private cameraComposite: CameraCompositeHandle | null = null;
|
|
506
510
|
private audioMixCtx: AudioContext | null = null;
|
|
507
511
|
private audioMixSources: MediaStreamAudioSourceNode[] = [];
|
|
512
|
+
private micAudioCleanup: MicAudioCleanupHandle[] = [];
|
|
508
513
|
private recorder: MediaRecorder | null = null;
|
|
509
514
|
private mimeType: string = "video/webm";
|
|
510
515
|
|
|
@@ -1355,6 +1360,7 @@ export class RecorderEngine {
|
|
|
1355
1360
|
// Full stream teardown happens in the finally block below.
|
|
1356
1361
|
this.cameraLive = false;
|
|
1357
1362
|
this.audioMixSources = [];
|
|
1363
|
+
this.stopMicAudioCleanup();
|
|
1358
1364
|
this.audioMixCtx?.close().catch(() => {});
|
|
1359
1365
|
this.audioMixCtx = null;
|
|
1360
1366
|
for (const s of [this.cameraStream, this.rawCameraStream, this.micStream]) {
|
|
@@ -1801,11 +1807,18 @@ export class RecorderEngine {
|
|
|
1801
1807
|
private buildMixedAudioTrack(
|
|
1802
1808
|
streams: (MediaStream | null | undefined)[],
|
|
1803
1809
|
): MediaStreamTrack | null {
|
|
1804
|
-
const
|
|
1810
|
+
const audioInputs = streams
|
|
1805
1811
|
.filter((s): s is MediaStream => s != null)
|
|
1806
|
-
.flatMap((
|
|
1807
|
-
|
|
1808
|
-
|
|
1812
|
+
.flatMap((stream) =>
|
|
1813
|
+
stream.getAudioTracks().map((track) => ({
|
|
1814
|
+
track,
|
|
1815
|
+
isMicrophone: stream === this.micStream,
|
|
1816
|
+
})),
|
|
1817
|
+
);
|
|
1818
|
+
if (audioInputs.length === 0) return null;
|
|
1819
|
+
if (audioInputs.length === 1 && !audioInputs[0].isMicrophone) {
|
|
1820
|
+
return audioInputs[0].track;
|
|
1821
|
+
}
|
|
1809
1822
|
|
|
1810
1823
|
const ctx = this.audioMixCtx ?? new AudioContext();
|
|
1811
1824
|
this.audioMixCtx = ctx;
|
|
@@ -1814,7 +1827,15 @@ export class RecorderEngine {
|
|
|
1814
1827
|
}
|
|
1815
1828
|
this.audioMixSources = [];
|
|
1816
1829
|
const dest = ctx.createMediaStreamDestination();
|
|
1817
|
-
for (const
|
|
1830
|
+
for (const input of audioInputs) {
|
|
1831
|
+
let track = input.track;
|
|
1832
|
+
if (input.isMicrophone) {
|
|
1833
|
+
const cleanup = createMicAudioCleanup(new MediaStream([track]), {
|
|
1834
|
+
audioContext: ctx,
|
|
1835
|
+
});
|
|
1836
|
+
this.micAudioCleanup.push(cleanup);
|
|
1837
|
+
track = cleanup.stream.getAudioTracks()[0] ?? track;
|
|
1838
|
+
}
|
|
1818
1839
|
const source = ctx.createMediaStreamSource(new MediaStream([track]));
|
|
1819
1840
|
source.connect(dest);
|
|
1820
1841
|
this.audioMixSources.push(source);
|
|
@@ -1822,6 +1843,11 @@ export class RecorderEngine {
|
|
|
1822
1843
|
return dest.stream.getAudioTracks()[0];
|
|
1823
1844
|
}
|
|
1824
1845
|
|
|
1846
|
+
private stopMicAudioCleanup(): void {
|
|
1847
|
+
for (const cleanup of this.micAudioCleanup) cleanup.stop();
|
|
1848
|
+
this.micAudioCleanup = [];
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1825
1851
|
private buildCombinedStream(): MediaStream {
|
|
1826
1852
|
if (this.opts.mode === "screen") {
|
|
1827
1853
|
return this.buildDisplayRecordingStream();
|
|
@@ -2356,6 +2382,7 @@ export class RecorderEngine {
|
|
|
2356
2382
|
// aren't mistaken for a disconnect.
|
|
2357
2383
|
this.cameraLive = false;
|
|
2358
2384
|
this.audioMixSources = [];
|
|
2385
|
+
this.stopMicAudioCleanup();
|
|
2359
2386
|
this.audioMixCtx?.close().catch(() => {});
|
|
2360
2387
|
this.audioMixCtx = null;
|
|
2361
2388
|
this.cameraComposite?.cleanup();
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { useT } from "@agent-native/core/client/i18n";
|
|
7
7
|
import {
|
|
8
8
|
IconCheck,
|
|
9
|
-
|
|
9
|
+
IconLink,
|
|
10
10
|
IconLock,
|
|
11
11
|
IconSend2,
|
|
12
12
|
IconTrash,
|
|
@@ -273,10 +273,12 @@ export function CopyField({
|
|
|
273
273
|
label,
|
|
274
274
|
value,
|
|
275
275
|
disabled,
|
|
276
|
+
description,
|
|
276
277
|
}: {
|
|
277
278
|
label: string;
|
|
278
279
|
value: string;
|
|
279
280
|
disabled?: boolean;
|
|
281
|
+
description?: string;
|
|
280
282
|
}) {
|
|
281
283
|
const t = useT();
|
|
282
284
|
const [copied, setCopied] = useState(false);
|
|
@@ -287,26 +289,24 @@ export function CopyField({
|
|
|
287
289
|
setTimeout(() => setCopied(false), 1400);
|
|
288
290
|
};
|
|
289
291
|
return (
|
|
290
|
-
<div>
|
|
291
|
-
|
|
292
|
-
<
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
className="flex-1 h-9 font-mono text-xs"
|
|
299
|
-
/>
|
|
292
|
+
<div className="flex items-center justify-between gap-3">
|
|
293
|
+
<div className="min-w-0">
|
|
294
|
+
{label ? <div className="text-sm font-medium">{label}</div> : null}
|
|
295
|
+
{description ? (
|
|
296
|
+
<div className="text-xs text-muted-foreground">{description}</div>
|
|
297
|
+
) : null}
|
|
298
|
+
</div>
|
|
299
|
+
<div aria-live="polite" className="shrink-0">
|
|
300
300
|
<Button
|
|
301
301
|
type="button"
|
|
302
302
|
variant="outline"
|
|
303
|
-
size="
|
|
303
|
+
size="sm"
|
|
304
304
|
onClick={copy}
|
|
305
|
-
aria-label={t("shareUi.copy")}
|
|
306
305
|
disabled={disabled}
|
|
307
|
-
className="h-9
|
|
306
|
+
className="h-9"
|
|
308
307
|
>
|
|
309
|
-
{copied ? <IconCheck size={14} /> : <
|
|
308
|
+
{copied ? <IconCheck size={14} /> : <IconLink size={14} />}
|
|
309
|
+
{copied ? t("recordRoute.linkCopied") : t("shareUi.copy")}
|
|
310
310
|
</Button>
|
|
311
311
|
</div>
|
|
312
312
|
</div>
|
|
@@ -647,13 +647,13 @@ const messages = {
|
|
|
647
647
|
collapse: "Collapse",
|
|
648
648
|
changelogMarkdown: `# Changelog
|
|
649
649
|
|
|
650
|
-
All notable user-facing changes to Clips are documented here. Open it any time from the command menu (Cmd
|
|
650
|
+
All notable user-facing changes to Clips are documented here. Open it any time from the command menu (Cmd K -> "What's new") or from Settings.
|
|
651
651
|
|
|
652
652
|
## 2026-06-23
|
|
653
653
|
|
|
654
654
|
### Added
|
|
655
655
|
|
|
656
|
-
- See what's new right inside Clips. A changelog now lives in the command menu (Cmd
|
|
656
|
+
- See what's new right inside Clips. A changelog now lives in the command menu (Cmd K) and in Settings.`,
|
|
657
657
|
playback: "Playback",
|
|
658
658
|
defaultPlaybackSpeed: "Default playback speed",
|
|
659
659
|
playbackDescription: "Applied automatically when you open a recording.",
|
|
@@ -1193,7 +1193,7 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
1193
1193
|
seekTo: "Seek to {{time}}",
|
|
1194
1194
|
},
|
|
1195
1195
|
editorToolbar: {
|
|
1196
|
-
undoTooltip: "Undo (Cmd/Ctrl
|
|
1196
|
+
undoTooltip: "Undo (Cmd/Ctrl Z)",
|
|
1197
1197
|
playPauseTooltip: "Play / Pause (Space)",
|
|
1198
1198
|
sourceDuration: "({{duration}} src)",
|
|
1199
1199
|
previewSpeed: "Preview speed",
|
|
@@ -8,7 +8,7 @@ export function isMacPlatform(): boolean {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function shortcutModifierLabel(): string {
|
|
11
|
-
return isMacPlatform() ? "
|
|
11
|
+
return isMacPlatform() ? "Cmd" : "Ctrl";
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function shortcutLabel(shortcut: string): string {
|
|
@@ -18,14 +18,13 @@ export function shortcutLabel(shortcut: string): string {
|
|
|
18
18
|
.map((part) => {
|
|
19
19
|
const token = part.trim();
|
|
20
20
|
const lower = token.toLowerCase();
|
|
21
|
-
if (lower === "cmd" || lower === "meta") return isMac ? "
|
|
21
|
+
if (lower === "cmd" || lower === "meta") return isMac ? "Cmd" : "Ctrl";
|
|
22
22
|
if (lower === "ctrl" || lower === "control") return "Ctrl";
|
|
23
|
-
if (lower === "alt" || lower === "option")
|
|
24
|
-
|
|
25
|
-
if (lower === "shift") return isMac ? "\u21e7" : "Shift";
|
|
23
|
+
if (lower === "alt" || lower === "option") return "Alt";
|
|
24
|
+
if (lower === "shift") return "Shift";
|
|
26
25
|
if (lower === "enter") return "Enter";
|
|
27
26
|
if (lower === "space") return "Space";
|
|
28
27
|
return token.length === 1 ? token.toUpperCase() : token;
|
|
29
28
|
})
|
|
30
|
-
.join("
|
|
29
|
+
.join(" ");
|
|
31
30
|
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
isLikelyPhoneMicLabel,
|
|
23
23
|
type AudioInputFallback,
|
|
24
24
|
} from "@shared/media-device-selection";
|
|
25
|
+
import { createMicAudioCleanup } from "@shared/mic-audio-cleanup";
|
|
25
26
|
import { scheduleReadyChime } from "@shared/recording-audio";
|
|
26
27
|
import {
|
|
27
28
|
SCREEN_CAPTURE_FRAME_RATE,
|
|
@@ -210,6 +211,7 @@ type ActiveRecording = {
|
|
|
210
211
|
outputStream: MediaStream;
|
|
211
212
|
sourceStreams: MediaStream[];
|
|
212
213
|
audioContext: AudioContext | null;
|
|
214
|
+
cleanupAudio: () => void;
|
|
213
215
|
chunkIndex: number;
|
|
214
216
|
uploadChain: Promise<void>;
|
|
215
217
|
uploadPromises: Promise<unknown>[];
|
|
@@ -787,17 +789,38 @@ async function buildCompositor(
|
|
|
787
789
|
|
|
788
790
|
async function createMixedAudio(
|
|
789
791
|
streams: MediaStream[],
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
792
|
+
microphoneStream: MediaStream | null,
|
|
793
|
+
): Promise<{
|
|
794
|
+
audioContext: AudioContext | null;
|
|
795
|
+
tracks: MediaStreamTrack[];
|
|
796
|
+
cleanup: () => void;
|
|
797
|
+
}> {
|
|
798
|
+
const audioInputs = streams.flatMap((stream) =>
|
|
799
|
+
stream.getAudioTracks().map((track) => ({
|
|
800
|
+
track,
|
|
801
|
+
isMicrophone: stream === microphoneStream,
|
|
802
|
+
})),
|
|
803
|
+
);
|
|
804
|
+
if (!audioInputs.length) {
|
|
805
|
+
return { audioContext: null, tracks: [], cleanup() {} };
|
|
806
|
+
}
|
|
807
|
+
if (audioInputs.length === 1 && !audioInputs[0].isMicrophone) {
|
|
808
|
+
return { audioContext: null, tracks: [audioInputs[0].track], cleanup() {} };
|
|
795
809
|
}
|
|
796
810
|
|
|
797
811
|
const audioContext = new AudioContext();
|
|
798
812
|
await audioContext.resume().catch(() => undefined);
|
|
799
813
|
const destination = audioContext.createMediaStreamDestination();
|
|
800
|
-
|
|
814
|
+
const micCleanup: Array<{ stop: () => void }> = [];
|
|
815
|
+
for (const input of audioInputs) {
|
|
816
|
+
let track = input.track;
|
|
817
|
+
if (input.isMicrophone) {
|
|
818
|
+
const cleanup = createMicAudioCleanup(new MediaStream([track]), {
|
|
819
|
+
audioContext,
|
|
820
|
+
});
|
|
821
|
+
micCleanup.push(cleanup);
|
|
822
|
+
track = cleanup.stream.getAudioTracks()[0] ?? track;
|
|
823
|
+
}
|
|
801
824
|
// One source per track (not per stream) so each input is isolated in the
|
|
802
825
|
// mix graph and can be detached independently.
|
|
803
826
|
const source = audioContext.createMediaStreamSource(
|
|
@@ -817,7 +840,16 @@ async function createMixedAudio(
|
|
|
817
840
|
}
|
|
818
841
|
});
|
|
819
842
|
}
|
|
820
|
-
|
|
843
|
+
let cleanedUp = false;
|
|
844
|
+
return {
|
|
845
|
+
audioContext,
|
|
846
|
+
tracks: destination.stream.getAudioTracks(),
|
|
847
|
+
cleanup() {
|
|
848
|
+
if (cleanedUp) return;
|
|
849
|
+
cleanedUp = true;
|
|
850
|
+
for (const cleanup of micCleanup) cleanup.stop();
|
|
851
|
+
},
|
|
852
|
+
};
|
|
821
853
|
}
|
|
822
854
|
|
|
823
855
|
async function uploadChunk(
|
|
@@ -1081,6 +1113,7 @@ function disposePrepared(): void {
|
|
|
1081
1113
|
|
|
1082
1114
|
function cleanup(recording: ActiveRecording): void {
|
|
1083
1115
|
recording.stopCompositor?.();
|
|
1116
|
+
recording.cleanupAudio();
|
|
1084
1117
|
stopStreams([recording.outputStream, ...recording.sourceStreams]);
|
|
1085
1118
|
void recording.audioContext?.close().catch(() => undefined);
|
|
1086
1119
|
}
|
|
@@ -1215,7 +1248,7 @@ async function begin(message: BeginMessage): Promise<{
|
|
|
1215
1248
|
? [ready.cameraStream]
|
|
1216
1249
|
: []),
|
|
1217
1250
|
];
|
|
1218
|
-
const mixedAudio = await createMixedAudio(audioInputs);
|
|
1251
|
+
const mixedAudio = await createMixedAudio(audioInputs, ready.micStream);
|
|
1219
1252
|
|
|
1220
1253
|
const outputStream = new MediaStream([videoTrack, ...mixedAudio.tracks]);
|
|
1221
1254
|
const mimeType = pickMimeType() || "video/webm";
|
|
@@ -1257,6 +1290,7 @@ async function begin(message: BeginMessage): Promise<{
|
|
|
1257
1290
|
...(compositor ? [compositor.canvasStream] : []),
|
|
1258
1291
|
],
|
|
1259
1292
|
audioContext: mixedAudio.audioContext,
|
|
1293
|
+
cleanupAudio: mixedAudio.cleanup,
|
|
1260
1294
|
chunkIndex: 0,
|
|
1261
1295
|
uploadChain: Promise.resolve(),
|
|
1262
1296
|
uploadPromises: [],
|
|
@@ -1802,6 +1836,7 @@ async function restart(
|
|
|
1802
1836
|
// Tear down the old compositor + mixing context; keep the capture tracks alive
|
|
1803
1837
|
// so the next BEGIN can build a fresh compositor/recorder on the same streams.
|
|
1804
1838
|
recording.stopCompositor?.();
|
|
1839
|
+
recording.cleanupAudio();
|
|
1805
1840
|
void recording.audioContext?.close().catch(() => undefined);
|
|
1806
1841
|
activeRecording = null;
|
|
1807
1842
|
|
|
@@ -739,6 +739,14 @@ function meetingCanStartNotes(meeting: PopoverMeeting): boolean {
|
|
|
739
739
|
);
|
|
740
740
|
}
|
|
741
741
|
|
|
742
|
+
function humanReadableShortcutLabel(shortcut: string): string {
|
|
743
|
+
return shortcut
|
|
744
|
+
.split("+")
|
|
745
|
+
.map((token) => token.trim())
|
|
746
|
+
.filter(Boolean)
|
|
747
|
+
.join(" ");
|
|
748
|
+
}
|
|
749
|
+
|
|
742
750
|
function voiceShortcutLabel(
|
|
743
751
|
shortcut: VoiceShortcutPreference,
|
|
744
752
|
customShortcut: string,
|
|
@@ -747,13 +755,15 @@ function voiceShortcutLabel(
|
|
|
747
755
|
case "fn":
|
|
748
756
|
return "Fn";
|
|
749
757
|
case "cmd-shift-space":
|
|
750
|
-
return "Cmd
|
|
758
|
+
return "Cmd Shift Space";
|
|
751
759
|
case "ctrl-shift-space":
|
|
752
|
-
return "Ctrl
|
|
760
|
+
return "Ctrl Shift Space";
|
|
753
761
|
case "custom":
|
|
754
|
-
return customShortcut
|
|
762
|
+
return customShortcut
|
|
763
|
+
? humanReadableShortcutLabel(customShortcut)
|
|
764
|
+
: "Custom shortcut";
|
|
755
765
|
case "both":
|
|
756
|
-
return "Fn, Cmd
|
|
766
|
+
return "Fn, Cmd Shift Space, or Ctrl Shift Space";
|
|
757
767
|
}
|
|
758
768
|
}
|
|
759
769
|
|
|
@@ -5983,10 +5993,10 @@ function Setup({
|
|
|
5983
5993
|
const shortcutHint: Record<VoiceShortcutPreference, string> = {
|
|
5984
5994
|
fn: "Press the Fn / globe key to dictate. macOS requires Input Monitoring for this one shortcut.",
|
|
5985
5995
|
"cmd-shift-space":
|
|
5986
|
-
"Press Cmd
|
|
5987
|
-
"ctrl-shift-space": "Press Ctrl
|
|
5996
|
+
"Press Cmd Shift Space to dictate. This does not need Input Monitoring.",
|
|
5997
|
+
"ctrl-shift-space": "Press Ctrl Shift Space to dictate.",
|
|
5988
5998
|
custom: `Press ${voiceCustomShortcut || "your recorded shortcut"} to dictate.`,
|
|
5989
|
-
both: "Any of Fn, Cmd
|
|
5999
|
+
both: "Any of Fn, Cmd Shift Space, or Ctrl Shift Space. Includes Fn, so macOS may ask for Input Monitoring.",
|
|
5990
6000
|
};
|
|
5991
6001
|
const fnShortcutSelected = voiceShortcut === "fn" || voiceShortcut === "both";
|
|
5992
6002
|
const modeHint: Record<VoiceMode, string> = {
|
|
@@ -7373,7 +7383,7 @@ function Setup({
|
|
|
7373
7383
|
/>
|
|
7374
7384
|
<DesktopSettingsRow
|
|
7375
7385
|
label="Open Clips"
|
|
7376
|
-
description="Open the tray popover; Cmd
|
|
7386
|
+
description="Open the tray popover; Cmd Shift L remains available."
|
|
7377
7387
|
control={
|
|
7378
7388
|
<ShortcutRecorder
|
|
7379
7389
|
value={popoverCustomShortcut}
|
|
@@ -7383,7 +7393,7 @@ function Setup({
|
|
|
7383
7393
|
}
|
|
7384
7394
|
>
|
|
7385
7395
|
<>
|
|
7386
|
-
<span>Leave this empty to use only Cmd
|
|
7396
|
+
<span>Leave this empty to use only Cmd Shift L.</span>
|
|
7387
7397
|
{shortcutRegistrationError ? (
|
|
7388
7398
|
<span className="setup-warning">
|
|
7389
7399
|
{shortcutRegistrationError}
|
|
@@ -7707,8 +7717,8 @@ function Setup({
|
|
|
7707
7717
|
)
|
|
7708
7718
|
}
|
|
7709
7719
|
>
|
|
7710
|
-
<option value="cmd-shift-space">Cmd
|
|
7711
|
-
<option value="ctrl-shift-space">Ctrl
|
|
7720
|
+
<option value="cmd-shift-space">Cmd Shift Space</option>
|
|
7721
|
+
<option value="ctrl-shift-space">Ctrl Shift Space</option>
|
|
7712
7722
|
<option value="custom">Custom shortcut</option>
|
|
7713
7723
|
<option value="fn">
|
|
7714
7724
|
Fn (globe, needs Input Monitoring)
|
|
@@ -8239,7 +8249,11 @@ function ShortcutRecorder({
|
|
|
8239
8249
|
event.stopPropagation();
|
|
8240
8250
|
}}
|
|
8241
8251
|
>
|
|
8242
|
-
{recording
|
|
8252
|
+
{recording
|
|
8253
|
+
? "Press shortcut..."
|
|
8254
|
+
: value
|
|
8255
|
+
? humanReadableShortcutLabel(value)
|
|
8256
|
+
: placeholder}
|
|
8243
8257
|
</button>
|
|
8244
8258
|
{value ? (
|
|
8245
8259
|
<button
|
|
@@ -55,6 +55,10 @@ import {
|
|
|
55
55
|
waitForAcceptedRecordingAfterFinalizeError,
|
|
56
56
|
waitForReadyRecordingAfterFinalizeError,
|
|
57
57
|
} from "../../../shared/finalize-recovery";
|
|
58
|
+
import {
|
|
59
|
+
createMicAudioCleanup,
|
|
60
|
+
type MicAudioCleanupHandle,
|
|
61
|
+
} from "../../../shared/mic-audio-cleanup";
|
|
58
62
|
import type { LocalRecordingMode } from "../shared/config";
|
|
59
63
|
import { createAudioCue, type AudioCue } from "./audio-cue";
|
|
60
64
|
import { createCameraCompositeStream } from "./camera-composite";
|
|
@@ -541,35 +545,45 @@ interface RecordingAudio {
|
|
|
541
545
|
}
|
|
542
546
|
|
|
543
547
|
/**
|
|
544
|
-
* Build the audio track(s) for the recording.
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
* one source we pass it through untouched.
|
|
548
|
+
* Build the audio track(s) for the recording. Microphone tracks pass through
|
|
549
|
+
* the cleanup graph whether or not system/display audio is also present, then
|
|
550
|
+
* both sources are mixed into one track when needed.
|
|
548
551
|
*/
|
|
549
552
|
function buildRecordingAudio(
|
|
550
553
|
micTracks: MediaStreamTrack[],
|
|
551
554
|
systemTracks: MediaStreamTrack[],
|
|
552
555
|
): RecordingAudio {
|
|
553
|
-
if (!micTracks.length
|
|
554
|
-
//
|
|
556
|
+
if (!micTracks.length) {
|
|
557
|
+
// System-only audio keeps its original stereo signal; there is no
|
|
558
|
+
// microphone path to clean in this branch.
|
|
555
559
|
return {
|
|
556
|
-
tracks:
|
|
560
|
+
tracks: systemTracks,
|
|
557
561
|
cleanup() {},
|
|
558
562
|
};
|
|
559
563
|
}
|
|
560
564
|
const AudioCtx = window.AudioContext || (window as any).webkitAudioContext;
|
|
561
565
|
if (!AudioCtx) {
|
|
562
|
-
// No WebAudio —
|
|
566
|
+
// No WebAudio — keep the capture alive with the raw microphone signal.
|
|
563
567
|
return { tracks: micTracks, cleanup() {} };
|
|
564
568
|
}
|
|
565
569
|
const ctx: AudioContext = new AudioCtx();
|
|
566
570
|
const destination = ctx.createMediaStreamDestination();
|
|
567
|
-
|
|
571
|
+
const micCleanup: MicAudioCleanupHandle[] = [];
|
|
572
|
+
const cleanedMicTracks = micTracks.map((track) => {
|
|
573
|
+
const cleanup = createMicAudioCleanup(new MediaStream([track]), {
|
|
574
|
+
audioContext: ctx,
|
|
575
|
+
});
|
|
576
|
+
micCleanup.push(cleanup);
|
|
577
|
+
return cleanup.stream.getAudioTracks()[0] ?? track;
|
|
578
|
+
});
|
|
579
|
+
for (const tracks of [cleanedMicTracks, systemTracks]) {
|
|
580
|
+
if (!tracks.length) continue;
|
|
568
581
|
ctx.createMediaStreamSource(new MediaStream(tracks)).connect(destination);
|
|
569
582
|
}
|
|
570
583
|
return {
|
|
571
584
|
tracks: destination.stream.getAudioTracks(),
|
|
572
585
|
cleanup() {
|
|
586
|
+
for (const cleanup of micCleanup) cleanup.stop();
|
|
573
587
|
ctx.close().catch(() => {});
|
|
574
588
|
},
|
|
575
589
|
};
|