@agent-native/core 0.168.10 → 0.168.11

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 (41) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/changelog/2026-08-19-desktop-settings-are-tighter-with-a-new-advanced-tab.md +6 -0
  3. package/corpus/templates/clips/desktop/README.md +79 -1
  4. package/corpus/templates/clips/desktop/components.json +20 -0
  5. package/corpus/templates/clips/desktop/package.json +10 -1
  6. package/corpus/templates/clips/desktop/src/app.tsx +1812 -1994
  7. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +23 -65
  8. package/corpus/templates/clips/desktop/src/components/settings/settings-ui.tsx +399 -0
  9. package/corpus/templates/clips/desktop/src/components/ui/alert-dialog.tsx +166 -0
  10. package/corpus/templates/clips/desktop/src/components/ui/button.tsx +57 -0
  11. package/corpus/templates/clips/desktop/src/components/ui/empty.tsx +104 -0
  12. package/corpus/templates/clips/desktop/src/components/ui/input.tsx +22 -0
  13. package/corpus/templates/clips/desktop/src/components/ui/popover.tsx +38 -0
  14. package/corpus/templates/clips/desktop/src/components/ui/select.tsx +164 -0
  15. package/corpus/templates/clips/desktop/src/components/ui/skeleton.tsx +15 -0
  16. package/corpus/templates/clips/desktop/src/components/ui/spinner.tsx +17 -0
  17. package/corpus/templates/clips/desktop/src/components/ui/switch.tsx +27 -0
  18. package/corpus/templates/clips/desktop/src/components/ui/textarea.tsx +22 -0
  19. package/corpus/templates/clips/desktop/src/dev/browser-preview.ts +65 -0
  20. package/corpus/templates/clips/desktop/src/hooks/useSystemAccessRows.ts +128 -0
  21. package/corpus/templates/clips/desktop/src/hooks/useWhisperSettings.ts +0 -2
  22. package/corpus/templates/clips/desktop/src/lib/permission-status.ts +84 -0
  23. package/corpus/templates/clips/desktop/src/lib/rewind-status.ts +6 -5
  24. package/corpus/templates/clips/desktop/src/lib/settings-navigation.ts +18 -5
  25. package/corpus/templates/clips/desktop/src/lib/utils.ts +6 -0
  26. package/corpus/templates/clips/desktop/src/main.tsx +31 -4
  27. package/corpus/templates/clips/desktop/src/overlays/finalizing.tsx +2 -2
  28. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +2 -2
  29. package/corpus/templates/clips/desktop/src/styles.css +75 -1427
  30. package/corpus/templates/clips/desktop/src/tailwind.css +282 -0
  31. package/corpus/templates/clips/desktop/tsconfig.json +4 -1
  32. package/corpus/templates/clips/desktop/vite.config.ts +7 -1
  33. package/dist/observability/routes.d.ts +3 -3
  34. package/dist/progress/routes.d.ts +1 -1
  35. package/dist/secrets/routes.d.ts +6 -6
  36. package/dist/server/action-routes.js +3 -2
  37. package/dist/server/realtime-token.d.ts +1 -1
  38. package/dist/server/request-origin.d.ts +6 -0
  39. package/dist/server/request-origin.js +12 -0
  40. package/dist/server/transcribe-voice.d.ts +1 -1
  41. package/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Lets the tray's React surfaces render in a plain browser tab (`pnpm
3
+ * vite:dev`, http://localhost:1420) so layout and copy can be reviewed without
4
+ * a Rust build.
5
+ *
6
+ * Outside Tauri there is no IPC: `getCurrentWindow()` reads
7
+ * `window.__TAURI_INTERNALS__.metadata` and throws during the first effect,
8
+ * which blanks the whole app. This installs the smallest stub that keeps React
9
+ * mounted.
10
+ *
11
+ * Every `invoke` REJECTS rather than returning invented data. A settings row
12
+ * that cannot read its value should show its default and say so, not a
13
+ * fabricated state that looks real in a screenshot — so what you see here is
14
+ * the surface's genuine "backend unavailable" rendering. Anything needing true
15
+ * device state (permission grants, Whisper catalog, Rewind) has to be checked
16
+ * in the real app.
17
+ *
18
+ * Dev-only: `main.tsx` calls this behind `import.meta.env.DEV`, so it is absent
19
+ * from any production bundle.
20
+ */
21
+
22
+ type TauriInternals = {
23
+ metadata: {
24
+ currentWindow: { label: string };
25
+ currentWebview: { label: string; windowLabel: string };
26
+ };
27
+ invoke: (cmd: string, args?: unknown) => Promise<never>;
28
+ transformCallback: (cb: (payload: unknown) => void) => number;
29
+ convertFileSrc: (path: string) => string;
30
+ };
31
+
32
+ declare global {
33
+ interface Window {
34
+ __TAURI_INTERNALS__?: TauriInternals;
35
+ }
36
+ }
37
+
38
+ export function installBrowserPreview(): void {
39
+ if (window.__TAURI_INTERNALS__) return;
40
+
41
+ let callbackId = 0;
42
+ const callbacks = new Map<number, (payload: unknown) => void>();
43
+
44
+ window.__TAURI_INTERNALS__ = {
45
+ metadata: {
46
+ currentWindow: { label: "main" },
47
+ currentWebview: { label: "main", windowLabel: "main" },
48
+ },
49
+ invoke: (cmd) =>
50
+ Promise.reject(
51
+ new Error(`Tauri command "${cmd}" is unavailable in browser preview`),
52
+ ),
53
+ transformCallback: (cb) => {
54
+ const id = ++callbackId;
55
+ callbacks.set(id, cb);
56
+ return id;
57
+ },
58
+ convertFileSrc: (path) => path,
59
+ };
60
+
61
+ console.info(
62
+ "[clips-tray] browser preview: Tauri commands will reject. Add a route " +
63
+ "hash to jump straight to a surface, e.g. #settings or #settings/advanced.",
64
+ );
65
+ }
@@ -0,0 +1,128 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ import {
4
+ permissionStatusForPane,
5
+ readPermissionStatuses,
6
+ requestOrOpenPermission,
7
+ type MacosPrivacyPane,
8
+ type PermissionStatuses,
9
+ } from "../lib/permission-status";
10
+ import { isMacPlatform } from "../lib/platform";
11
+
12
+ export type SystemAccessRow = {
13
+ key: string;
14
+ label: string;
15
+ description: string;
16
+ /** `null` means the grant could not be read — never render it as denied. */
17
+ granted: boolean | null;
18
+ onGrant: () => void;
19
+ };
20
+
21
+ type SystemAccessDefinition = {
22
+ key: string;
23
+ label: string;
24
+ description: string;
25
+ panes: MacosPrivacyPane[];
26
+ };
27
+
28
+ /**
29
+ * Collapses the per-pane permission list into the few lines a user acts on:
30
+ * capture as one grant, then only the extras their configuration needs.
31
+ */
32
+ export function useSystemAccessRows({
33
+ includeVoicePaste,
34
+ includeFnMonitoring,
35
+ onOpenSettings,
36
+ }: {
37
+ includeVoicePaste: boolean;
38
+ includeFnMonitoring: boolean;
39
+ onOpenSettings: (pane: MacosPrivacyPane) => void;
40
+ }): SystemAccessRow[] {
41
+ const mac = isMacPlatform();
42
+ const [statuses, setStatuses] = useState<PermissionStatuses | null>(null);
43
+
44
+ const recheck = useCallback(() => {
45
+ void readPermissionStatuses().then(setStatuses);
46
+ }, []);
47
+
48
+ // Grants are changed in System Settings, outside this window. Re-reading on
49
+ // focus is what stops a row from still demanding "Grant" after the user did.
50
+ useEffect(() => {
51
+ recheck();
52
+ window.addEventListener("focus", recheck);
53
+ return () => window.removeEventListener("focus", recheck);
54
+ }, [recheck]);
55
+
56
+ // One row per OS grant, worded exactly like the recorder's readiness panel —
57
+ // the same grant must read as the same thing on both surfaces, and each
58
+ // Grant button must open exactly the pane its row names. The earlier
59
+ // combined capture row walked three panes one click at a time, which read
60
+ // as one grant that mysteriously kept not taking.
61
+ const definitions: SystemAccessDefinition[] = [
62
+ {
63
+ key: "screen",
64
+ label: "Screen Recording",
65
+ description: "Allows Clips to record your screen",
66
+ panes: ["screen"],
67
+ },
68
+ {
69
+ key: "microphone",
70
+ label: "Microphone",
71
+ description: "Allows Clips to access your microphone",
72
+ panes: ["microphone"],
73
+ },
74
+ {
75
+ key: "camera",
76
+ label: "Camera",
77
+ description: "Allows Clips to access your camera",
78
+ panes: ["camera"],
79
+ },
80
+ ];
81
+ if (mac) {
82
+ definitions.push({
83
+ key: "speech",
84
+ label: "Speech Recognition",
85
+ description: "Allows Clips to use speech recognition",
86
+ panes: ["speech"],
87
+ });
88
+ }
89
+ if (mac && includeVoicePaste) {
90
+ definitions.push({
91
+ key: "accessibility",
92
+ label: "Accessibility",
93
+ description: "Allows Clips to control this device to paste dictated text",
94
+ panes: ["accessibility"],
95
+ });
96
+ }
97
+ if (mac && includeFnMonitoring) {
98
+ definitions.push({
99
+ key: "input-monitoring",
100
+ label: "Input Monitoring",
101
+ description: "Allows Clips to detect the Fn key",
102
+ panes: ["input-monitoring"],
103
+ });
104
+ }
105
+
106
+ return definitions.map(({ panes, ...definition }) => {
107
+ const results = panes.map((pane) =>
108
+ permissionStatusForPane(pane, statuses),
109
+ );
110
+ const granted = results.some((result) => result === null)
111
+ ? null
112
+ : results.every(Boolean);
113
+ return {
114
+ ...definition,
115
+ granted,
116
+ onGrant: () => {
117
+ const target =
118
+ panes.find(
119
+ (pane) => permissionStatusForPane(pane, statuses) === false,
120
+ ) ?? panes[0];
121
+ void requestOrOpenPermission(target, {
122
+ onOpenSettings,
123
+ onRecheck: recheck,
124
+ });
125
+ },
126
+ };
127
+ });
128
+ }
@@ -48,7 +48,6 @@ export function useWhisperSettings(
48
48
 
49
49
  const enabled = featureConfig?.whisperModelEnabled !== false;
50
50
  const modelId = featureConfig?.whisperModelId ?? "base";
51
- const selectedModel = catalog.find((m) => m.id === modelId) ?? null;
52
51
  const deletableModels = catalog.filter(
53
52
  (m) => m.id !== modelId && downloadedModelIds.includes(m.id),
54
53
  );
@@ -162,7 +161,6 @@ export function useWhisperSettings(
162
161
  status,
163
162
  enabled,
164
163
  modelId,
165
- selectedModel,
166
164
  deletableModels,
167
165
  triggerDownload,
168
166
  setEnabled,
@@ -0,0 +1,84 @@
1
+ // Reading and repairing OS privacy grants. Shared by the recorder's readiness
2
+ // list and Settings › General › System access so both surfaces report the same
3
+ // three states.
4
+ import { invoke } from "@tauri-apps/api/core";
5
+
6
+ import { isMacPlatform } from "./platform";
7
+
8
+ export type MacosPrivacyPane =
9
+ | "camera"
10
+ | "microphone"
11
+ | "screen"
12
+ | "speech"
13
+ | "accessibility"
14
+ | "input-monitoring";
15
+
16
+ export type PermissionStatuses = {
17
+ screen: boolean;
18
+ camera: boolean;
19
+ microphone: boolean;
20
+ speech: boolean;
21
+ accessibility: boolean;
22
+ inputMonitoring: boolean;
23
+ };
24
+
25
+ /**
26
+ * `null` means the grants could not be read — a non-macOS host, or the command
27
+ * failing. It is not the same as "denied", and callers must not render it as
28
+ * one: an unreadable grant offers to open System Settings, a denied grant asks
29
+ * the user to fix it.
30
+ */
31
+ export async function readPermissionStatuses(): Promise<PermissionStatuses | null> {
32
+ if (!isMacPlatform()) return null;
33
+ try {
34
+ return await invoke<PermissionStatuses>("check_permission_statuses");
35
+ } catch {
36
+ // coercion-ok: null IS the typed "could not read" value — callers render
37
+ // it as unknown, never as denied (locked by permission-status.test.ts).
38
+ return null;
39
+ }
40
+ }
41
+
42
+ export function permissionStatusForPane(
43
+ pane: MacosPrivacyPane,
44
+ statuses: PermissionStatuses | null,
45
+ ): boolean | null {
46
+ if (!statuses) return null;
47
+ const map: Record<MacosPrivacyPane, boolean> = {
48
+ screen: statuses.screen,
49
+ camera: statuses.camera,
50
+ microphone: statuses.microphone,
51
+ speech: statuses.speech,
52
+ accessibility: statuses.accessibility,
53
+ "input-monitoring": statuses.inputMonitoring,
54
+ };
55
+ return map[pane];
56
+ }
57
+
58
+ /**
59
+ * Screen Recording is the one grant macOS will still prompt for in-app, so try
60
+ * that before sending the user to System Settings. Every other pane only opens.
61
+ */
62
+ export async function requestOrOpenPermission(
63
+ pane: MacosPrivacyPane,
64
+ {
65
+ onOpenSettings,
66
+ onRecheck,
67
+ }: {
68
+ onOpenSettings: (pane: MacosPrivacyPane) => void;
69
+ onRecheck?: () => void;
70
+ },
71
+ ): Promise<void> {
72
+ if (isMacPlatform() && pane === "screen") {
73
+ try {
74
+ const granted = await invoke<boolean>("system_audio_request_permission");
75
+ onRecheck?.();
76
+ if (granted) return;
77
+ } catch {
78
+ // coercion-ok: the request API may be unavailable on an older macOS
79
+ // build, or the user may have denied the prompt once already — falling
80
+ // through to open the dedicated privacy pane IS the recovery path.
81
+ }
82
+ }
83
+ onOpenSettings(pane);
84
+ }
@@ -52,7 +52,8 @@ export function getRewindStatusPresentation({
52
52
  return {
53
53
  kind: "excluded",
54
54
  title: "Rewind is protecting a private moment",
55
- detail: status.coverage || "An excluded app is being skipped.",
55
+ detail:
56
+ status.coverage || "An excluded app is in front, so capture is paused.",
56
57
  isLive: false,
57
58
  hasError: false,
58
59
  };
@@ -62,7 +63,7 @@ export function getRewindStatusPresentation({
62
63
  return {
63
64
  kind: "unavailable",
64
65
  title: "Rewind is unavailable",
65
- detail: "Rewind capture is not available on this device.",
66
+ detail: "Capture isn't available on this device.",
66
67
  isLive: false,
67
68
  hasError: false,
68
69
  };
@@ -75,7 +76,7 @@ export function getRewindStatusPresentation({
75
76
  detail:
76
77
  status.lastError ||
77
78
  status.coverage ||
78
- "Rewind is retaining local coverage.",
79
+ "New moments are being saved to this device.",
79
80
  isLive: true,
80
81
  hasError: Boolean(status.lastError),
81
82
  };
@@ -93,10 +94,10 @@ export function getRewindStatusPresentation({
93
94
 
94
95
  return {
95
96
  kind: "idle",
96
- title: "Rewind is enabled but not currently capturing",
97
+ title: "Rewind is on but not capturing",
97
98
  detail: clipRecordingActive
98
99
  ? "Rewind will resume when this Clip ends."
99
- : "No new local coverage is being retained right now.",
100
+ : "Nothing new is being saved right now.",
100
101
  isLive: false,
101
102
  hasError: false,
102
103
  };
@@ -1,11 +1,24 @@
1
- export type DesktopSettingsTab =
2
- | "general"
3
- | "recording"
4
- | "meetings"
5
- | "dictation";
1
+ // Order is the settings nav order; app.tsx's settingsTabs labels/icons follow it.
2
+ export const DESKTOP_SETTINGS_TABS = [
3
+ "general",
4
+ "recording",
5
+ "rewind",
6
+ "meetings",
7
+ "dictation",
8
+ "advanced",
9
+ ] as const;
10
+
11
+ export type DesktopSettingsTab = (typeof DESKTOP_SETTINGS_TABS)[number];
6
12
 
7
13
  export function initialDesktopSettingsTab(
8
14
  tab?: DesktopSettingsTab,
9
15
  ): DesktopSettingsTab {
10
16
  return tab ?? "general";
11
17
  }
18
+
19
+ /** `#settings/<tab>` detail → a known tab, or undefined for anything else. */
20
+ export function asDesktopSettingsTab(
21
+ value: string | undefined,
22
+ ): DesktopSettingsTab | undefined {
23
+ return DESKTOP_SETTINGS_TABS.find((tab) => tab === value);
24
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -2,8 +2,10 @@ import { invoke } from "@tauri-apps/api/core";
2
2
  import React from "react";
3
3
  import ReactDOM from "react-dom/client";
4
4
 
5
- import { App } from "./app";
5
+ import { App, installAuthFetchInterceptor } from "./app";
6
+ import { installBrowserPreview } from "./dev/browser-preview";
6
7
  import { initDesktopSentry } from "./lib/sentry";
8
+ import { asDesktopSettingsTab } from "./lib/settings-navigation";
7
9
  import { Bubble } from "./overlays/bubble";
8
10
  import { Countdown } from "./overlays/countdown";
9
11
  import { Finalizing } from "./overlays/finalizing";
@@ -17,7 +19,8 @@ import { RegionGuideEditor, RegionGuides } from "./overlays/region-guides";
17
19
  import { RegionRecordBorder } from "./overlays/region-record-border";
18
20
  import { Toolbar } from "./overlays/toolbar";
19
21
 
20
- import "./styles.css";
22
+ // Imports styles.css itself, into a lower cascade layer — see tailwind.css.
23
+ import "./tailwind.css";
21
24
 
22
25
  /**
23
26
  * One bundle, one HTML, many views. We pick which component to mount based
@@ -26,7 +29,14 @@ import "./styles.css";
26
29
  */
27
30
  function currentRoute(): string {
28
31
  const hash = window.location.hash.replace(/^#/, "").toLowerCase();
29
- return hash || "popover";
32
+ // `#settings/advanced` names a surface plus where to open it; the route is
33
+ // the part before the slash.
34
+ return hash.split("/")[0] || "popover";
35
+ }
36
+
37
+ /** `#settings/advanced` → "advanced". */
38
+ function currentRouteDetail(): string | undefined {
39
+ return window.location.hash.replace(/^#/, "").toLowerCase().split("/")[1];
30
40
  }
31
41
 
32
42
  function installRouteAttributes(route: string): void {
@@ -64,6 +74,16 @@ function pickRoute(route: string): React.ReactElement {
64
74
  return <RegionRecordBorder />;
65
75
  case "monitor-picker":
66
76
  return <MonitorPicker />;
77
+ // Opens the popover straight onto Settings. Rust never spawns this route —
78
+ // it exists so `#settings` / `#settings/advanced` reaches the surface
79
+ // directly in a browser tab or a dev window.
80
+ case "settings":
81
+ return (
82
+ <App
83
+ initialView="settings"
84
+ initialSettingsTab={asDesktopSettingsTab(currentRouteDetail())}
85
+ />
86
+ );
67
87
  default:
68
88
  return <App />;
69
89
  }
@@ -214,7 +234,14 @@ function installConsoleCapture(route: string): void {
214
234
  const rootEl = document.getElementById("root");
215
235
  if (rootEl) {
216
236
  const route = currentRoute();
217
- installRouteAttributes(route);
237
+ // Before anything reads Tauri internals: outside the app shell this keeps the
238
+ // surfaces mountable in a browser tab. No-ops inside Tauri, absent in prod.
239
+ if (import.meta.env.DEV) installBrowserPreview();
240
+ // Before React renders: covers fetches made during the first commit.
241
+ installAuthFetchInterceptor();
242
+ // `settings` is the popover window showing one of its views, so it takes the
243
+ // popover's shell styling rather than a full-viewport overlay's.
244
+ installRouteAttributes(route === "settings" ? "popover" : route);
218
245
  initDesktopSentry(route);
219
246
  installConsoleCapture(route);
220
247
  installBeforeUnloadCleanup();
@@ -219,8 +219,8 @@ export function Finalizing() {
219
219
  ? "Uploaded"
220
220
  : progress.stage === "failed"
221
221
  ? progress.savedLocally
222
- ? "Upload paused clip saved locally"
223
- : "Upload paused open Clips to recover"
222
+ ? "Upload paused. Clip saved locally."
223
+ : "Upload paused. Open Clips to recover."
224
224
  : progress.stage === "uploading" ||
225
225
  progress.stage === "processing" ||
226
226
  progress.stage === "opening"
@@ -366,8 +366,8 @@ export function Toolbar() {
366
366
  className={`toolbar-v-disk-indicator toolbar-v-disk-indicator-${diskSpaceLevel}`}
367
367
  title={
368
368
  diskSpaceLevel === "critical"
369
- ? "Disk almost full stop recording now to avoid losing your clip"
370
- : "Low disk space save your recording soon"
369
+ ? "Disk almost full. Stop recording now to avoid losing your clip."
370
+ : "Low disk space. Save your recording soon."
371
371
  }
372
372
  data-no-drag
373
373
  >