@agent-native/core 0.168.9 → 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 (48) 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/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +1 -0
  34. package/corpus/templates/content/app/global.css +5 -0
  35. package/corpus/templates/content/docs/solutions/2026-08-20-code-block-slash-production-regression-shape.md +492 -0
  36. package/corpus/templates/content/docs/solutions/2026-08-20-code-block-surface-background-shape.md +273 -0
  37. package/dist/deploy/build.js +1 -1
  38. package/dist/observability/routes.d.ts +3 -3
  39. package/dist/progress/routes.d.ts +1 -1
  40. package/dist/secrets/routes.d.ts +6 -6
  41. package/dist/server/action-routes.js +3 -2
  42. package/dist/server/realtime-token.d.ts +1 -1
  43. package/dist/server/request-origin.d.ts +6 -0
  44. package/dist/server/request-origin.js +12 -0
  45. package/dist/server/transcribe-voice.d.ts +1 -1
  46. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  47. package/dist/shared/mcp-embed-headers.js +10 -3
  48. package/package.json +1 -1
@@ -5,28 +5,18 @@ import {
5
5
  IconCircleOff,
6
6
  IconRefresh,
7
7
  } from "@tabler/icons-react";
8
- import { invoke } from "@tauri-apps/api/core";
9
8
  import { useState, useCallback, useEffect } from "react";
10
9
 
10
+ import {
11
+ permissionStatusForPane,
12
+ readPermissionStatuses,
13
+ requestOrOpenPermission,
14
+ type MacosPrivacyPane,
15
+ type PermissionStatuses,
16
+ } from "../lib/permission-status";
11
17
  import { isMacPlatform, isWindowsPlatform } from "../lib/platform";
12
18
 
13
19
  type CaptureMode = "screen" | "screen-camera" | "camera";
14
- type MacosPrivacyPane =
15
- | "camera"
16
- | "microphone"
17
- | "screen"
18
- | "speech"
19
- | "accessibility"
20
- | "input-monitoring";
21
-
22
- type PermissionStatuses = {
23
- screen: boolean;
24
- camera: boolean;
25
- microphone: boolean;
26
- speech: boolean;
27
- accessibility: boolean;
28
- inputMonitoring: boolean;
29
- };
30
20
 
31
21
  type ReadinessItem = {
32
22
  label: string;
@@ -53,39 +43,39 @@ function readinessItems({
53
43
  const items: ReadinessItem[] = [
54
44
  {
55
45
  label: "Screen Recording",
56
- detail: "Needed for screen or window capture.",
46
+ detail: "Allows Clips to record your screen",
57
47
  pane: "screen",
58
48
  active: mode !== "camera",
59
49
  },
60
50
  {
61
51
  label: "Microphone",
62
- detail: "Needed when the mic is on.",
52
+ detail: "Allows Clips to access your microphone",
63
53
  pane: "microphone",
64
54
  active: micOn,
65
55
  },
66
56
  {
67
57
  label: "Speech Recognition",
68
- detail: "Used for native transcripts.",
58
+ detail: "Allows Clips to use speech recognition",
69
59
  pane: "speech",
70
60
  active: micOn,
71
61
  macosOnly: true,
72
62
  },
73
63
  {
74
64
  label: "Camera",
75
- detail: "Needed when camera is on.",
65
+ detail: "Allows Clips to access your camera",
76
66
  pane: "camera",
77
67
  active: mode !== "screen" && cameraOn,
78
68
  },
79
69
  {
80
70
  label: "Accessibility",
81
- detail: "Needed to paste dictated text into other apps.",
71
+ detail: "Allows Clips to control this device to paste dictated text",
82
72
  pane: "accessibility",
83
73
  active: includeVoicePaste,
84
74
  macosOnly: true,
85
75
  },
86
76
  {
87
77
  label: "Input Monitoring",
88
- detail: "Only needed for the Fn dictation shortcut.",
78
+ detail: "Allows Clips to detect the Fn key",
89
79
  pane: "input-monitoring",
90
80
  active: includeFnMonitoring,
91
81
  macosOnly: true,
@@ -95,22 +85,6 @@ function readinessItems({
95
85
  return items.filter((item) => item.active && (!item.macosOnly || mac));
96
86
  }
97
87
 
98
- function statusForPane(
99
- pane: MacosPrivacyPane,
100
- statuses: PermissionStatuses | null,
101
- ): boolean | null {
102
- if (!statuses) return null;
103
- const map: Record<MacosPrivacyPane, boolean> = {
104
- screen: statuses.screen,
105
- camera: statuses.camera,
106
- microphone: statuses.microphone,
107
- speech: statuses.speech,
108
- accessibility: statuses.accessibility,
109
- "input-monitoring": statuses.inputMonitoring,
110
- };
111
- return map[pane];
112
- }
113
-
114
88
  export function ReadinessPanel({
115
89
  mode,
116
90
  cameraOn,
@@ -146,35 +120,19 @@ export function ReadinessPanel({
146
120
  const checkStatuses = useCallback(async () => {
147
121
  setChecking(true);
148
122
  try {
149
- const result = await invoke<PermissionStatuses>(
150
- "check_permission_statuses",
151
- );
152
- setStatuses(result);
153
- } catch {
154
- // Non-macOS or command not available — leave statuses null
123
+ setStatuses(await readPermissionStatuses());
155
124
  } finally {
156
125
  setChecking(false);
157
126
  }
158
127
  }, []);
159
128
 
160
- const requestOrOpenPermission = useCallback(
161
- async (pane: MacosPrivacyPane) => {
162
- if (mac && pane === "screen") {
163
- try {
164
- const granted = await invoke<boolean>(
165
- "system_audio_request_permission",
166
- );
167
- await checkStatuses();
168
- if (granted) return;
169
- } catch {
170
- // Fall through to the dedicated privacy pane. The request API may
171
- // be unavailable on an older macOS build or the user may already
172
- // have denied the prompt once.
173
- }
174
- }
175
- onOpenPermission(pane);
176
- },
177
- [checkStatuses, mac, onOpenPermission],
129
+ const grantPermission = useCallback(
130
+ (pane: MacosPrivacyPane) =>
131
+ requestOrOpenPermission(pane, {
132
+ onOpenSettings: onOpenPermission,
133
+ onRecheck: () => void checkStatuses(),
134
+ }),
135
+ [checkStatuses, onOpenPermission],
178
136
  );
179
137
 
180
138
  useEffect(() => {
@@ -203,7 +161,7 @@ export function ReadinessPanel({
203
161
  <div className="readiness-list">
204
162
  {items.length ? (
205
163
  items.map((item) => {
206
- const granted = statusForPane(item.pane, statuses);
164
+ const granted = permissionStatusForPane(item.pane, statuses);
207
165
  return (
208
166
  <div className="readiness-item" key={item.pane}>
209
167
  <div className="readiness-item-copy">
@@ -239,7 +197,7 @@ export function ReadinessPanel({
239
197
  <button
240
198
  type="button"
241
199
  className="readiness-open-button"
242
- onClick={() => void requestOrOpenPermission(item.pane)}
200
+ onClick={() => void grantPermission(item.pane)}
243
201
  >
244
202
  Open
245
203
  </button>
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Presentation for the tray Settings surface, built on Tailwind + the shadcn
3
+ * primitives in `@/components/ui`. Everything here is layout and typography —
4
+ * no Tauri calls, no feature state — so a row's behavior stays in app.tsx and
5
+ * its shape stays here.
6
+ */
7
+ import { IconCheck, IconChevronDown, IconX } from "@tabler/icons-react";
8
+ import { useState, type ReactElement, type ReactNode } from "react";
9
+
10
+ import { Button } from "@/components/ui/button";
11
+ import {
12
+ Popover,
13
+ PopoverClose,
14
+ PopoverContent,
15
+ PopoverTrigger,
16
+ } from "@/components/ui/popover";
17
+ import {
18
+ Select,
19
+ SelectContent,
20
+ SelectItem,
21
+ SelectTrigger,
22
+ SelectValue,
23
+ } from "@/components/ui/select";
24
+ import { cn } from "@/lib/utils";
25
+
26
+ /**
27
+ * The single shape every control in a settings row wears: same height, same
28
+ * radius, same fill, same type size, same hover. Dropdowns, keycaps, value
29
+ * triggers, and action buttons are interchangeable in a row, so a control that
30
+ * invents its own size is the thing that reads as sloppy.
31
+ *
32
+ * At rest it is the page's own background inside a hairline the same weight as
33
+ * the card's — `border-border`, not the heavier `border-input`. The grey fill is
34
+ * the hover state and nothing else, so a row of controls reads as quiet until
35
+ * the pointer is actually over one.
36
+ */
37
+ // `rounded-md` (radius − 2px), not the card's own `rounded-lg`: a control
38
+ // nested inside a rounded card wants a concentrically smaller radius, or its
39
+ // corners read puffier than the surface containing it.
40
+ const ROW_CONTROL =
41
+ "h-8 rounded-md border border-border bg-background px-3 text-sm font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50";
42
+
43
+ /** A card of rows. `label` is the small uppercase divider; omit it when the
44
+ * tab is a single card and its heading already names the group. */
45
+ export function SettingsGroup({
46
+ label,
47
+ children,
48
+ }: {
49
+ label?: string;
50
+ children: ReactNode;
51
+ }) {
52
+ return (
53
+ <section className="min-w-0">
54
+ {label ? (
55
+ <div className="px-0.5 pb-1.5 text-2xs font-bold uppercase tracking-[0.08em] text-muted-foreground">
56
+ {label}
57
+ </div>
58
+ ) : null}
59
+ <div className="overflow-hidden rounded-lg border border-border bg-card">
60
+ {children}
61
+ </div>
62
+ </section>
63
+ );
64
+ }
65
+
66
+ /**
67
+ * One setting per row: label and a grounding line on the left, its current
68
+ * state on the right.
69
+ *
70
+ * Every row carries a `description` by product decision — a desktop setting
71
+ * that silently changes OS-level behavior needs grounding the label alone
72
+ * cannot give. It must say something the label does not: where the setting
73
+ * applies, what the user will see, what it costs. Restating the label ("Keep
74
+ * dictation visible — keeps dictation visible") is the failure mode, and the
75
+ * reason to delete a line rather than pad it.
76
+ */
77
+ export function SettingsRow({
78
+ label,
79
+ description,
80
+ control,
81
+ children,
82
+ stacked = false,
83
+ }: {
84
+ label: string;
85
+ description?: string;
86
+ control?: ReactNode;
87
+ children?: ReactNode;
88
+ stacked?: boolean;
89
+ }) {
90
+ return (
91
+ <div
92
+ className={cn(
93
+ // The divider is a pseudo-element inset to the row's own padding, not a
94
+ // `border-b`. An edge-to-edge rule cuts the card into stacked boxes;
95
+ // stopping it short of the corners keeps one card with rows inside it.
96
+ // Dimmer than the card's own outline too — it separates siblings rather
97
+ // than bounding the card, so it should not compete with the edge.
98
+ "relative grid min-h-[46px] min-w-0 items-center gap-4 px-3.5 py-2.5",
99
+ "after:absolute after:inset-x-3.5 after:bottom-0 after:h-px after:bg-border/50 after:content-[''] last:after:hidden",
100
+ stacked
101
+ ? "grid-cols-[minmax(0,1fr)] items-start gap-2"
102
+ : "grid-cols-[minmax(0,1fr)_auto]",
103
+ )}
104
+ >
105
+ <div className="grid min-w-0 gap-0.5">
106
+ <span className="truncate text-base font-medium text-foreground">
107
+ {label}
108
+ </span>
109
+ {description ? (
110
+ <span className="text-xs text-muted-foreground">{description}</span>
111
+ ) : null}
112
+ </div>
113
+ {control ? (
114
+ <div
115
+ className={cn(
116
+ "flex min-w-0 items-center gap-2",
117
+ stacked ? "w-full justify-start" : "justify-end",
118
+ )}
119
+ >
120
+ {control}
121
+ </div>
122
+ ) : null}
123
+ {children ? (
124
+ <div className="col-span-full grid min-w-0 gap-2 text-xs text-muted-foreground">
125
+ {children}
126
+ </div>
127
+ ) : null}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ /** A dropdown wearing the shared row-control shape. */
133
+ export function SettingsSelect({
134
+ value,
135
+ onValueChange,
136
+ options,
137
+ ariaLabel,
138
+ /** Shown when `value` matches no option — a catalog still loading, or empty.
139
+ * Without it the trigger renders as a bare chevron. */
140
+ placeholder,
141
+ disabled = false,
142
+ }: {
143
+ value: string;
144
+ onValueChange: (value: string) => void;
145
+ options: Array<{ value: string; label: string; disabled?: boolean }>;
146
+ ariaLabel: string;
147
+ placeholder?: string;
148
+ disabled?: boolean;
149
+ }) {
150
+ // Radix renders the placeholder only for an empty value, so a stored value
151
+ // that matches no option (a catalog still loading) would otherwise leave the
152
+ // trigger showing nothing but its chevron.
153
+ const selected = options.some((option) => option.value === value);
154
+
155
+ return (
156
+ <Select
157
+ value={selected ? value : ""}
158
+ onValueChange={onValueChange}
159
+ disabled={disabled}
160
+ >
161
+ <SelectTrigger
162
+ aria-label={ariaLabel}
163
+ /* The registry trigger ships focus:ring-2 + ring-offset-2; zero BOTH or
164
+ the offset halo survives ring-0 and doubles up with ROW_CONTROL's
165
+ focus-visible ring. */
166
+ className={cn(
167
+ ROW_CONTROL,
168
+ "w-auto gap-1.5 shadow-none focus:ring-0 focus:ring-offset-0",
169
+ )}
170
+ >
171
+ <SelectValue placeholder={placeholder} />
172
+ </SelectTrigger>
173
+ <SelectContent align="end">
174
+ {options.map((option) => (
175
+ <SelectItem
176
+ key={option.value}
177
+ value={option.value}
178
+ disabled={option.disabled}
179
+ className="text-sm"
180
+ >
181
+ {option.label}
182
+ </SelectItem>
183
+ ))}
184
+ </SelectContent>
185
+ </Select>
186
+ );
187
+ }
188
+
189
+ /** Shows the current value and opens its detail — a dropdown in every respect
190
+ * a user can see, so it wears the same shape. */
191
+ export function SettingsValueTrigger({
192
+ value,
193
+ mono = false,
194
+ className,
195
+ ...props
196
+ }: {
197
+ value: string;
198
+ mono?: boolean;
199
+ } & React.ComponentPropsWithRef<"button">) {
200
+ return (
201
+ <button
202
+ type="button"
203
+ className={cn(
204
+ ROW_CONTROL,
205
+ "inline-flex max-w-60 items-center gap-1.5",
206
+ className,
207
+ )}
208
+ {...props}
209
+ >
210
+ <span className={cn("truncate", mono && "font-mono text-xs")}>
211
+ {value}
212
+ </span>
213
+ <IconChevronDown
214
+ className="size-3.5 shrink-0 opacity-50"
215
+ stroke={1.9}
216
+ aria-hidden="true"
217
+ />
218
+ </button>
219
+ );
220
+ }
221
+
222
+ /**
223
+ * An action in a settings row: a real button, never a bare text link, and by
224
+ * default the same soft filled shape as the dropdowns it sits beside.
225
+ * `primary` is for the single action a row wants you to take; `quiet` is for a
226
+ * secondary one that should not compete with the control next to it.
227
+ */
228
+ export function SettingsActionButton({
229
+ className,
230
+ emphasis = "soft",
231
+ ...props
232
+ }: { emphasis?: "soft" | "primary" | "quiet" | "destructive" } & Omit<
233
+ React.ComponentProps<typeof Button>,
234
+ "variant" | "size"
235
+ >) {
236
+ return (
237
+ <Button
238
+ variant={emphasis === "primary" ? "default" : "ghost"}
239
+ className={cn(
240
+ "gap-1.5",
241
+ emphasis === "soft" && ROW_CONTROL,
242
+ emphasis === "primary" && "h-8 rounded-md px-3 text-sm font-medium",
243
+ emphasis === "quiet" &&
244
+ "h-8 rounded-md px-2 text-sm font-medium text-muted-foreground hover:bg-accent",
245
+ // Quiet-red, not a filled slab: red text that gains a red-tinted wash
246
+ // on hover. For actions that permanently delete data — nothing else.
247
+ emphasis === "destructive" &&
248
+ "h-8 rounded-md px-2 text-sm font-medium text-destructive hover:bg-destructive/10 hover:text-destructive",
249
+ className,
250
+ )}
251
+ {...props}
252
+ />
253
+ );
254
+ }
255
+
256
+ /**
257
+ * A titled popover for a setting's detail. The tray window is small and
258
+ * chromeless, so the title and close button orient the user inside a layer
259
+ * that has no window frame of its own.
260
+ */
261
+ export function SettingsPopover({
262
+ title,
263
+ trigger,
264
+ children,
265
+ open,
266
+ onOpenChange,
267
+ side = "bottom",
268
+ className,
269
+ }: {
270
+ title: string;
271
+ trigger: ReactElement;
272
+ children: ReactNode;
273
+ open: boolean;
274
+ onOpenChange: (open: boolean) => void;
275
+ side?: "bottom" | "left" | "right" | "top";
276
+ className?: string;
277
+ }) {
278
+ return (
279
+ <Popover open={open} onOpenChange={onOpenChange}>
280
+ <PopoverTrigger asChild>{trigger}</PopoverTrigger>
281
+ <PopoverContent
282
+ side={side}
283
+ align="end"
284
+ sideOffset={8}
285
+ collisionPadding={12}
286
+ data-popover-overlay="true"
287
+ className={cn("w-80 p-3", className)}
288
+ >
289
+ <div className="mb-2 flex items-center justify-between gap-2">
290
+ <div className="text-sm font-semibold" role="heading" aria-level={2}>
291
+ {title}
292
+ </div>
293
+ <PopoverClose
294
+ className="inline-flex size-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
295
+ aria-label={`Close ${title}`}
296
+ >
297
+ <IconX className="size-3.5" stroke={1.9} />
298
+ </PopoverClose>
299
+ </div>
300
+ {children}
301
+ </PopoverContent>
302
+ </Popover>
303
+ );
304
+ }
305
+
306
+ /**
307
+ * A one-of-many setting whose choices need more room than a select — because
308
+ * picking one reveals a follow-up control. `keepOpenValues` names those, so the
309
+ * popover does not close before the user can finish.
310
+ */
311
+ export function SettingsChoicePopover({
312
+ title,
313
+ value,
314
+ options,
315
+ onChange,
316
+ trigger,
317
+ keepOpenValues,
318
+ children,
319
+ }: {
320
+ title: string;
321
+ value: string;
322
+ options: Array<{ value: string; label: string }>;
323
+ onChange: (value: string) => void;
324
+ trigger: ReactElement;
325
+ keepOpenValues?: string[];
326
+ children?: ReactNode;
327
+ }) {
328
+ const [open, setOpen] = useState(false);
329
+
330
+ return (
331
+ <SettingsPopover
332
+ title={title}
333
+ open={open}
334
+ onOpenChange={setOpen}
335
+ trigger={trigger}
336
+ >
337
+ <div className="grid gap-2">
338
+ {/* Plain toggle buttons, not role="listbox"/"option": those roles
339
+ promise arrow-key navigation and typeahead this popover does not
340
+ implement, so a screen reader would announce a contract the
341
+ keyboard cannot honor. Tab-per-button with aria-pressed matches
342
+ how it actually behaves. */}
343
+ <div className="grid gap-0.5" role="group" aria-label={title}>
344
+ {options.map((option) => {
345
+ const selected = option.value === value;
346
+ return (
347
+ <button
348
+ key={option.value}
349
+ type="button"
350
+ aria-pressed={selected}
351
+ className={cn(
352
+ "flex w-full items-center justify-between gap-2.5 rounded-sm px-2 py-1.5 text-left text-sm font-medium transition-colors",
353
+ selected
354
+ ? "bg-accent text-foreground"
355
+ : "text-muted-foreground hover:bg-accent hover:text-foreground",
356
+ )}
357
+ onClick={() => {
358
+ onChange(option.value);
359
+ if (!keepOpenValues?.includes(option.value)) setOpen(false);
360
+ }}
361
+ >
362
+ <span className="truncate">{option.label}</span>
363
+ {selected ? (
364
+ <IconCheck className="size-3.5 shrink-0" stroke={2.1} />
365
+ ) : null}
366
+ </button>
367
+ );
368
+ })}
369
+ </div>
370
+ {children}
371
+ </div>
372
+ </SettingsPopover>
373
+ );
374
+ }
375
+
376
+ /** The keycap rendering of a shortcut, e.g. `⌘⇧Space`. */
377
+ export function SettingsKeycap({
378
+ children,
379
+ active = false,
380
+ className,
381
+ ...props
382
+ }: {
383
+ active?: boolean;
384
+ } & React.ComponentPropsWithRef<"button">) {
385
+ return (
386
+ <button
387
+ type="button"
388
+ className={cn(
389
+ ROW_CONTROL,
390
+ "inline-flex items-center whitespace-nowrap",
391
+ active && "border-ring bg-accent",
392
+ className,
393
+ )}
394
+ {...props}
395
+ >
396
+ {children}
397
+ </button>
398
+ );
399
+ }