@agent-native/core 0.178.0 → 0.178.1-nightly-20260909221943
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/clips/.agents/skills/dictate/SKILL.md +1 -0
- package/corpus/templates/clips/DESIGN.md +10 -7
- package/corpus/templates/clips/actions/list-organization-state.ts +56 -3
- package/corpus/templates/clips/actions/list-recordings.ts +2 -0
- package/corpus/templates/clips/actions/search-dictations.ts +81 -0
- package/corpus/templates/clips/app/components/agent-destination-logos.tsx +50 -0
- package/corpus/templates/clips/app/components/bug-report/bug-report-dialog.tsx +60 -0
- package/corpus/templates/clips/app/components/bug-report/bug-report-form.tsx +206 -0
- package/corpus/templates/clips/app/components/clips-command-menu.tsx +653 -0
- package/corpus/templates/clips/app/components/library/empty-state.tsx +52 -17
- package/corpus/templates/clips/app/components/library/folder-card.tsx +34 -0
- package/corpus/templates/clips/app/components/library/folder-tree.tsx +109 -64
- package/corpus/templates/clips/app/components/library/library-action-hrefs.ts +27 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +295 -103
- package/corpus/templates/clips/app/components/library/library-layout.tsx +381 -203
- package/corpus/templates/clips/app/components/library/library-primary-actions.tsx +48 -0
- package/corpus/templates/clips/app/components/library/page-header.tsx +48 -8
- package/corpus/templates/clips/app/components/library/recording-card.tsx +58 -55
- package/corpus/templates/clips/app/components/library/search-bar.tsx +5 -19
- package/corpus/templates/clips/app/components/library/sidebar-feedback-button.tsx +48 -0
- package/corpus/templates/clips/app/components/player/comments-panel.tsx +2 -2
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +344 -260
- package/corpus/templates/clips/app/components/player/viewer-controls.tsx +2 -2
- package/corpus/templates/clips/app/components/recorder/camera-visualizer.tsx +0 -33
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -1
- package/corpus/templates/clips/app/components/ui/tabs.tsx +1 -1
- package/corpus/templates/clips/app/hooks/use-command-search.ts +53 -0
- package/corpus/templates/clips/app/hooks/use-navigation-state.ts +32 -13
- package/corpus/templates/clips/app/hooks/use-upload-video-picker.tsx +1 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +7 -0
- package/corpus/templates/clips/app/lib/agent-share.ts +16 -0
- package/corpus/templates/clips/app/lib/command-events.ts +7 -0
- package/corpus/templates/clips/app/root.tsx +6 -82
- package/corpus/templates/clips/app/routes/_app.dictate.tsx +25 -5
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +2 -30
- package/corpus/templates/clips/app/routes/_app.library.folder.$folderId.tsx +13 -2
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +4 -17
- package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +111 -114
- package/corpus/templates/clips/app/routes/_app.spaces.$spaceId.folder.$folderId.tsx +25 -2
- package/corpus/templates/clips/app/routes/_app.spaces.$spaceId.tsx +14 -61
- package/corpus/templates/clips/app/routes/_app.spaces._index.tsx +34 -24
- package/corpus/templates/clips/app/routes/_app.trash.tsx +1 -1
- package/corpus/templates/clips/app/routes/_app.tsx +2 -4
- package/corpus/templates/clips/app/routes/bug-report.tsx +11 -189
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +148 -123
- package/corpus/templates/clips/changelog/2026-09-08-comments-and-desktop-window-controls.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-08-global-command-search.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-09-agent-sidebar-fits-below-toolbar.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-09-desktop-switches-stay-clear-in-dark-mode.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-09-recording-agent-uses-global-panel.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +2 -2
- package/corpus/templates/clips/desktop/src/components/MediaDeviceRow.tsx +5 -3
- package/corpus/templates/clips/desktop/src/components/SourceRow.tsx +1 -0
- package/corpus/templates/clips/desktop/src/components/Switch.tsx +2 -5
- package/corpus/templates/clips/desktop/src/components/ui/switch.tsx +21 -3
- package/corpus/templates/clips/desktop/src/hooks/useMediaDevices.ts +72 -52
- package/corpus/templates/clips/desktop/src/styles.css +18 -19
- package/corpus/templates/clips/desktop/src/tailwind.css +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +18 -10
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +3 -5
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +14 -3
- package/corpus/templates/design/AGENTS.md +2 -1
- package/corpus/templates/design/actions/export-png.ts +119 -0
- package/corpus/templates/design/actions/take-design-screenshot.ts +47 -13
- package/corpus/templates/design/server/plugins/agent-chat.ts +3 -1
- package/corpus/templates/factory/actions/dispatch-factory-item.ts +51 -22
- package/corpus/templates/factory/actions/list-triage-items.ts +21 -11
- package/corpus/templates/factory/actions/poll-slack-channel.ts +7 -2
- package/corpus/templates/factory/changelog/2026-09-09-slack-feedback-keeps-claimed-markers-after-poll.md +6 -0
- package/corpus/templates/factory/changelog/2026-09-09-slack-feedback-preserves-started-claimed-threads.md +6 -0
- package/corpus/templates/factory/changelog/2026-09-09-slack-feedback-skips-claimed-parents.md +6 -0
- package/corpus/templates/factory/server/lib/factory-automation-config.ts +1 -1
- package/corpus/templates/factory/server/lib/slack-feedback-prompt.ts +47 -0
- package/corpus/templates/factory/server/plugins/agent-chat.ts +5 -2
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +5 -4
- package/corpus/templates/factory/server/triage/review-skill-alignment.ts +12 -7
- package/corpus/templates/factory/server/triage/slack-review-window.ts +39 -0
- package/dist/client/CommandMenu.js +13 -1
- package/dist/client/analytics.js +10 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/vite/client.js +25 -7
- package/dist/vite/sentry-source-maps.d.ts +12 -0
- package/dist/vite/sentry-source-maps.js +105 -0
- package/package.json +2 -1
|
@@ -12,32 +12,50 @@ const switchVariants = cva(
|
|
|
12
12
|
default: "h-[1.15rem] w-8",
|
|
13
13
|
sm: "h-3.5 w-6",
|
|
14
14
|
},
|
|
15
|
+
tone: {
|
|
16
|
+
default: "",
|
|
17
|
+
success:
|
|
18
|
+
"data-[state=checked]:border-success data-[state=checked]:bg-success data-[state=unchecked]:border-border",
|
|
19
|
+
},
|
|
15
20
|
},
|
|
16
21
|
defaultVariants: {
|
|
17
22
|
size: "default",
|
|
23
|
+
tone: "default",
|
|
18
24
|
},
|
|
19
25
|
},
|
|
20
26
|
);
|
|
21
27
|
|
|
22
28
|
const switchThumbVariants = cva(
|
|
23
29
|
"pointer-events-none block rounded-full bg-background shadow-sm ring-1 ring-foreground/20 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground",
|
|
30
|
+
{
|
|
31
|
+
variants: {
|
|
32
|
+
tone: {
|
|
33
|
+
default: "",
|
|
34
|
+
success:
|
|
35
|
+
"data-[state=checked]:bg-success-foreground dark:data-[state=checked]:bg-success-foreground",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
defaultVariants: {
|
|
39
|
+
tone: "default",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
24
42
|
);
|
|
25
43
|
|
|
26
44
|
const Switch = React.forwardRef<
|
|
27
45
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
|
28
46
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> &
|
|
29
47
|
VariantProps<typeof switchVariants>
|
|
30
|
-
>(({ className, size = "default", ...props }, ref) => (
|
|
48
|
+
>(({ className, size = "default", tone = "default", ...props }, ref) => (
|
|
31
49
|
<SwitchPrimitives.Root
|
|
32
50
|
data-slot="switch"
|
|
33
51
|
data-size={size}
|
|
34
|
-
className={cn(switchVariants({ size, className }))}
|
|
52
|
+
className={cn(switchVariants({ size, tone, className }))}
|
|
35
53
|
{...props}
|
|
36
54
|
ref={ref}
|
|
37
55
|
>
|
|
38
56
|
<SwitchPrimitives.Thumb
|
|
39
57
|
data-slot="switch-thumb"
|
|
40
|
-
className={cn(switchThumbVariants())}
|
|
58
|
+
className={cn(switchThumbVariants({ tone }))}
|
|
41
59
|
/>
|
|
42
60
|
</SwitchPrimitives.Root>
|
|
43
61
|
));
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useCallback,
|
|
3
|
-
useEffect,
|
|
4
|
-
useMemo,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
type MutableRefObject,
|
|
8
|
-
} from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
2
|
|
|
10
3
|
import {
|
|
11
4
|
chooseFallbackAudioInput,
|
|
@@ -13,6 +6,7 @@ import {
|
|
|
13
6
|
isSelectableAudioInputDevice,
|
|
14
7
|
normalizedMediaDeviceId,
|
|
15
8
|
} from "../../../shared/media-device-selection";
|
|
9
|
+
import { getAudioStreamWithFallback } from "../lib/media-capture-constraints";
|
|
16
10
|
import {
|
|
17
11
|
isHardCapturePermissionError,
|
|
18
12
|
MACOS_CAPTURE_PERMISSION_MESSAGE,
|
|
@@ -24,15 +18,6 @@ const MIC_KEY = "clips:last-mic-id";
|
|
|
24
18
|
const CAM_LABEL_KEY = "clips:last-camera-label";
|
|
25
19
|
const MIC_LABEL_KEY = "clips:last-mic-label";
|
|
26
20
|
|
|
27
|
-
function rawMicConstraints(deviceId?: string): MediaTrackConstraints {
|
|
28
|
-
return {
|
|
29
|
-
...(deviceId ? { deviceId: { exact: deviceId } } : {}),
|
|
30
|
-
echoCancellation: false,
|
|
31
|
-
noiseSuppression: false,
|
|
32
|
-
autoGainControl: false,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
21
|
function concreteMediaDeviceId(value: string | null | undefined): string {
|
|
37
22
|
const id = normalizedMediaDeviceId(value);
|
|
38
23
|
return id && !isPseudoMediaDeviceId(id) ? id : "";
|
|
@@ -43,14 +28,30 @@ function isSelectableMediaDevice(device: MediaDeviceInfo): boolean {
|
|
|
43
28
|
}
|
|
44
29
|
|
|
45
30
|
interface Props {
|
|
46
|
-
|
|
47
|
-
// it does, this page must never probe getUserMedia itself.
|
|
48
|
-
bubbleActiveRef: MutableRefObject<boolean>;
|
|
31
|
+
microphoneEnabled: boolean;
|
|
49
32
|
popoverVisible: boolean;
|
|
50
33
|
setCameraError: (message: string | null) => void;
|
|
51
34
|
setRecError: (message: string | null) => void;
|
|
52
35
|
}
|
|
53
36
|
|
|
37
|
+
export async function refreshMicrophoneAccess(
|
|
38
|
+
selectedMicId: string,
|
|
39
|
+
selectedMicLabel: string,
|
|
40
|
+
): Promise<{ deviceId: string; label: string }> {
|
|
41
|
+
const stream = await getAudioStreamWithFallback(
|
|
42
|
+
selectedMicId,
|
|
43
|
+
selectedMicLabel,
|
|
44
|
+
false,
|
|
45
|
+
);
|
|
46
|
+
const track = stream.getAudioTracks()[0] ?? null;
|
|
47
|
+
const result = {
|
|
48
|
+
deviceId: concreteMediaDeviceId(track?.getSettings().deviceId),
|
|
49
|
+
label: track?.label ?? "",
|
|
50
|
+
};
|
|
51
|
+
stream.getTracks().forEach((candidate) => candidate.stop());
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
54
55
|
export interface MediaDevicesState {
|
|
55
56
|
cameraId: string;
|
|
56
57
|
setCameraId: (id: string) => void;
|
|
@@ -69,7 +70,7 @@ export interface MediaDevicesState {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
export function useMediaDevices({
|
|
72
|
-
|
|
73
|
+
microphoneEnabled,
|
|
73
74
|
popoverVisible,
|
|
74
75
|
setCameraError,
|
|
75
76
|
setRecError,
|
|
@@ -107,8 +108,8 @@ export function useMediaDevices({
|
|
|
107
108
|
|
|
108
109
|
// ---- device enumeration -------------------------------------------------
|
|
109
110
|
// WebKit only returns full device labels after getUserMedia() has granted
|
|
110
|
-
// access once. Enumerating itself is safe; the unlock helper below
|
|
111
|
-
//
|
|
111
|
+
// access once. Enumerating itself is safe; the unlock helper below only
|
|
112
|
+
// acquires audio while the visible microphone control is enabled.
|
|
112
113
|
const loadDevices = useCallback(async () => {
|
|
113
114
|
try {
|
|
114
115
|
if (!navigator.mediaDevices?.enumerateDevices) return;
|
|
@@ -155,6 +156,18 @@ export function useMediaDevices({
|
|
|
155
156
|
};
|
|
156
157
|
}, [loadDevices]);
|
|
157
158
|
|
|
159
|
+
const refreshSelectedMicrophone = useCallback(async () => {
|
|
160
|
+
const activeMic = await refreshMicrophoneAccess(selectedMicId, micLabel);
|
|
161
|
+
if (
|
|
162
|
+
selectedMicId &&
|
|
163
|
+
activeMic.deviceId &&
|
|
164
|
+
activeMic.deviceId !== selectedMicId
|
|
165
|
+
) {
|
|
166
|
+
setMicId("");
|
|
167
|
+
setMicLabel("");
|
|
168
|
+
}
|
|
169
|
+
}, [micLabel, selectedMicId]);
|
|
170
|
+
|
|
158
171
|
const unlockDeviceLabels = useCallback(async () => {
|
|
159
172
|
// Audio-only probe to unlock mic labels. We INTENTIONALLY skip video —
|
|
160
173
|
// the on-screen camera bubble window owns the camera, and probing
|
|
@@ -163,26 +176,18 @@ export function useMediaDevices({
|
|
|
163
176
|
// WebViews in the same process). Camera-label text is low-value
|
|
164
177
|
// anyway; most machines have one.
|
|
165
178
|
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
179
|
+
// The meter already needs an input while this control is visible. Reuse the
|
|
180
|
+
// recorder's fallback chain so a disconnected saved device cannot leave
|
|
181
|
+
// both the meter and the device list stranded until the user changes a
|
|
182
|
+
// selection. This may use the system default only after an exact saved
|
|
183
|
+
// device fails, or when the user has explicitly left the picker on Default.
|
|
171
184
|
try {
|
|
172
|
-
|
|
173
|
-
await loadDevices();
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const s = await navigator.mediaDevices.getUserMedia({
|
|
177
|
-
audio: rawMicConstraints(selectedMicId),
|
|
178
|
-
video: false,
|
|
179
|
-
});
|
|
180
|
-
s.getTracks().forEach((t) => t.stop());
|
|
185
|
+
await refreshSelectedMicrophone();
|
|
181
186
|
} catch {
|
|
182
187
|
// permission denied — labels stay empty until the user grants
|
|
183
188
|
}
|
|
184
189
|
await loadDevices();
|
|
185
|
-
}, [loadDevices,
|
|
190
|
+
}, [loadDevices, refreshSelectedMicrophone]);
|
|
186
191
|
|
|
187
192
|
const requestDeviceAccess = useCallback(
|
|
188
193
|
async (kind: "camera" | "mic") => {
|
|
@@ -190,15 +195,15 @@ export function useMediaDevices({
|
|
|
190
195
|
if (!navigator.mediaDevices?.getUserMedia) {
|
|
191
196
|
throw new Error("Device selection is not available in this WebView.");
|
|
192
197
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
if (kind === "camera") {
|
|
199
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
200
|
+
video: true,
|
|
201
|
+
audio: false,
|
|
202
|
+
});
|
|
203
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
204
|
+
} else {
|
|
205
|
+
await refreshSelectedMicrophone();
|
|
206
|
+
}
|
|
202
207
|
await loadDevices();
|
|
203
208
|
} catch (err) {
|
|
204
209
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -221,20 +226,35 @@ export function useMediaDevices({
|
|
|
221
226
|
await loadDevices();
|
|
222
227
|
}
|
|
223
228
|
},
|
|
224
|
-
[
|
|
229
|
+
[loadDevices, refreshSelectedMicrophone, setCameraError, setRecError],
|
|
225
230
|
);
|
|
226
231
|
|
|
227
|
-
// Defer device-label unlocking until
|
|
232
|
+
// Defer device-label unlocking until each visible popover session. Even the
|
|
228
233
|
// selected-device probe can trigger a macOS permission dialog, so keep it
|
|
229
|
-
// attached to visible UI instead of firing on hidden
|
|
234
|
+
// attached to visible UI instead of firing on hidden WebView mount.
|
|
230
235
|
const deviceLabelsUnlocked = useRef(false);
|
|
231
236
|
useEffect(() => {
|
|
232
237
|
void loadDevices();
|
|
233
|
-
if (popoverVisible
|
|
238
|
+
if (!popoverVisible || !microphoneEnabled) {
|
|
239
|
+
deviceLabelsUnlocked.current = false;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (!deviceLabelsUnlocked.current) {
|
|
234
243
|
deviceLabelsUnlocked.current = true;
|
|
235
244
|
void unlockDeviceLabels();
|
|
236
245
|
}
|
|
237
|
-
}, [loadDevices,
|
|
246
|
+
}, [loadDevices, microphoneEnabled, popoverVisible, unlockDeviceLabels]);
|
|
247
|
+
|
|
248
|
+
// WebKit does not consistently emit `devicechange` inside a reused Tauri
|
|
249
|
+
// popover WebView. Keep the visible picker current without polling while the
|
|
250
|
+
// recorder UI is hidden.
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (!popoverVisible) return;
|
|
253
|
+
const interval = window.setInterval(() => {
|
|
254
|
+
void loadDevices();
|
|
255
|
+
}, 2_000);
|
|
256
|
+
return () => window.clearInterval(interval);
|
|
257
|
+
}, [loadDevices, popoverVisible]);
|
|
238
258
|
|
|
239
259
|
useEffect(() => {
|
|
240
260
|
if (!micId || !isPseudoMediaDeviceId(micId) || mics.length === 0) return;
|
|
@@ -65,10 +65,8 @@
|
|
|
65
65
|
box-sizing: border-box;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/* Tauri window is
|
|
69
|
-
|
|
70
|
-
exactly; macOS derives a native shadow from the panel's alpha, giving us
|
|
71
|
-
the round-cornered chromeless popover with no transparent apron. */
|
|
68
|
+
/* The main Tauri window is transparent and chromeless, so the HTML paints the
|
|
69
|
+
visible rounded popover while macOS supplies its shadow. */
|
|
72
70
|
html,
|
|
73
71
|
body {
|
|
74
72
|
margin: 0;
|
|
@@ -519,8 +517,11 @@ body[data-clips-route]:not([data-clips-route="popover"]) #root {
|
|
|
519
517
|
background: hsl(var(--ui-muted) / 0.55);
|
|
520
518
|
}
|
|
521
519
|
|
|
522
|
-
.row-
|
|
523
|
-
|
|
520
|
+
.row-on {
|
|
521
|
+
color: var(--fg);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.row-off {
|
|
524
525
|
color: var(--fg-subtle);
|
|
525
526
|
}
|
|
526
527
|
|
|
@@ -530,13 +531,15 @@ body[data-clips-route]:not([data-clips-route="popover"]) #root {
|
|
|
530
531
|
display: inline-flex;
|
|
531
532
|
align-items: center;
|
|
532
533
|
justify-content: center;
|
|
533
|
-
color:
|
|
534
|
+
color: inherit;
|
|
534
535
|
flex-shrink: 0;
|
|
535
536
|
}
|
|
536
537
|
|
|
537
|
-
.media-device-row .row
|
|
538
|
+
.media-device-row .row,
|
|
539
|
+
.system-audio-row {
|
|
538
540
|
display: grid;
|
|
539
541
|
grid-template-columns: 20px minmax(0, 1fr) 36px;
|
|
542
|
+
column-gap: 10px;
|
|
540
543
|
}
|
|
541
544
|
|
|
542
545
|
.row-main {
|
|
@@ -551,12 +554,13 @@ body[data-clips-route]:not([data-clips-route="popover"]) #root {
|
|
|
551
554
|
}
|
|
552
555
|
|
|
553
556
|
.system-audio-row {
|
|
554
|
-
display: grid;
|
|
555
|
-
grid-template-columns: 20px minmax(0, 1fr) 36px;
|
|
556
557
|
align-items: center;
|
|
557
|
-
min-height:
|
|
558
|
-
padding:
|
|
559
|
-
|
|
558
|
+
min-height: 30px;
|
|
559
|
+
padding: 5px 10px;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.system-audio-icon {
|
|
563
|
+
transform: translateX(2px);
|
|
560
564
|
}
|
|
561
565
|
|
|
562
566
|
.system-audio-label {
|
|
@@ -567,21 +571,16 @@ body[data-clips-route]:not([data-clips-route="popover"]) #root {
|
|
|
567
571
|
font-size: 13px;
|
|
568
572
|
}
|
|
569
573
|
|
|
570
|
-
.row-on .row-icon {
|
|
571
|
-
color: var(--fg);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
574
|
/* Radix device picker trigger — the menu owns keyboard and focus behavior. */
|
|
575
575
|
.row-button {
|
|
576
576
|
width: 100%;
|
|
577
577
|
min-width: 0;
|
|
578
|
-
min-width: 0;
|
|
579
578
|
display: flex;
|
|
580
579
|
align-items: center;
|
|
581
580
|
gap: 6px;
|
|
582
581
|
border: none;
|
|
583
582
|
background: transparent;
|
|
584
|
-
color:
|
|
583
|
+
color: inherit;
|
|
585
584
|
font-size: 13px;
|
|
586
585
|
font-weight: 400;
|
|
587
586
|
padding: 0;
|
|
@@ -245,6 +245,7 @@
|
|
|
245
245
|
--ui-ring: 0 0% 40%;
|
|
246
246
|
/* macOS convention for an engaged switch, and the one accent the tray uses. */
|
|
247
247
|
--ui-success: 142 71% 45%;
|
|
248
|
+
--ui-success-foreground: 0 0% 100%;
|
|
248
249
|
/* Reserved for explicit "there is something new here" alerts and notices. */
|
|
249
250
|
--ui-info: 217 91% 55%;
|
|
250
251
|
}
|
|
@@ -271,6 +272,7 @@
|
|
|
271
272
|
--ui-input: 0 0% 32%;
|
|
272
273
|
--ui-ring: 0 0% 60%;
|
|
273
274
|
--ui-success: 142 60% 52%;
|
|
275
|
+
--ui-success-foreground: 0 0% 100%;
|
|
274
276
|
--ui-info: 217 91% 65%;
|
|
275
277
|
}
|
|
276
278
|
}
|
|
@@ -333,6 +335,7 @@
|
|
|
333
335
|
--color-card-foreground: hsl(var(--ui-card-foreground));
|
|
334
336
|
|
|
335
337
|
--color-success: hsl(var(--ui-success));
|
|
338
|
+
--color-success-foreground: hsl(var(--ui-success-foreground));
|
|
336
339
|
--color-info: hsl(var(--ui-info));
|
|
337
340
|
|
|
338
341
|
--radius-lg: var(--radius);
|
|
@@ -100,16 +100,24 @@ pub fn run() {
|
|
|
100
100
|
_ => {}
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
-
// The popover and camera bubble are separate native windows.
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
tauri::WindowEvent::CloseRequested { .. }
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
// The popover and camera bubble are separate native windows. A
|
|
104
|
+
// native close hides the panel back to the tray instead of
|
|
105
|
+
// destroying the webview, so tray clicks and the app's second
|
|
106
|
+
// launch can show the same window again.
|
|
107
|
+
if window.label() == "popover" {
|
|
108
|
+
match event {
|
|
109
|
+
tauri::WindowEvent::CloseRequested { api, .. } => {
|
|
110
|
+
api.prevent_close();
|
|
111
|
+
let app = window.app_handle();
|
|
112
|
+
let _ = window.hide();
|
|
113
|
+
clips::close_bubble_if_idle(&app);
|
|
114
|
+
let _ = app.emit("clips:popover-visible", false);
|
|
115
|
+
}
|
|
116
|
+
tauri::WindowEvent::Destroyed => {
|
|
117
|
+
clips::close_bubble_if_idle(window.app_handle());
|
|
118
|
+
}
|
|
119
|
+
_ => {}
|
|
120
|
+
}
|
|
113
121
|
}
|
|
114
122
|
})
|
|
115
123
|
.invoke_handler(tauri::generate_handler![
|
|
@@ -111,11 +111,9 @@ pub fn set_capture_included(window: &WebviewWindow) {
|
|
|
111
111
|
|
|
112
112
|
pub fn build_popover_window(app: &mut tauri::App) -> Result<WebviewWindow, tauri::Error> {
|
|
113
113
|
let app_handle = app.handle().clone();
|
|
114
|
-
// The window is sized to the visible panel
|
|
115
|
-
// the native
|
|
116
|
-
//
|
|
117
|
-
// never used here: its invisible margin eats clicks and reads as dead
|
|
118
|
-
// space around the UI.
|
|
114
|
+
// The window is sized to the visible panel exactly. The HTML paints the
|
|
115
|
+
// rounded shape and macOS derives the native shadow from that alpha mask,
|
|
116
|
+
// so adding native decorations would create a second frame above it.
|
|
119
117
|
WebviewWindowBuilder::new(app, "popover", WebviewUrl::App("index.html".into()))
|
|
120
118
|
.title("Clips")
|
|
121
119
|
.inner_size(
|
|
@@ -20,9 +20,20 @@ How to export designs and generate handoff documentation for developers converti
|
|
|
20
20
|
**This is not importable into Figma as editable vectors** — Figma cannot
|
|
21
21
|
parse `foreignObject` content, so it stays an opaque embedded HTML blob.
|
|
22
22
|
Use `export-design-as-figma-svg` (below) when the destination is Figma.
|
|
23
|
-
- **PNG**:
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
- **PNG**: `export-png` renders one stored HTML screen in headless Chromium and
|
|
24
|
+
uploads the PNG through the configured file provider. Pass `fileId`, or
|
|
25
|
+
`designId` plus `filename`, to select the screen. It returns a durable `url`
|
|
26
|
+
and suggested `filename`; `width` defaults to 1440px and `height` only sets
|
|
27
|
+
the responsive viewport because the export includes the screen's full page.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm action export-png --designId <designId> --fileId <fileId>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
If Chromium or file storage is unavailable, the action returns an explicit
|
|
34
|
+
failure instead of pretending that a downloadable image exists. The editor's
|
|
35
|
+
Download PNG remains the faithful client-side path for localhost/fusion
|
|
36
|
+
screens that are not stored HTML.
|
|
26
37
|
- **Deploy preview**: `deploy-design-preview` triggers a preview deploy for a
|
|
27
38
|
fusion-backed design branch. It requires the design's source to advertise
|
|
28
39
|
the `deployPreview` capability (fusion tier) and Builder.io to be connected;
|
|
@@ -40,7 +40,8 @@ Read the relevant skill before deeper work in that area.
|
|
|
40
40
|
| `present-design-variants` | Generate 2-5 variants for the user to pick and refine |
|
|
41
41
|
| `view-screen` | Re-read the current design or selected file when context is stale |
|
|
42
42
|
| `navigate` | Move the UI to a design, file, or panel |
|
|
43
|
-
| `export-
|
|
43
|
+
| `export-png` | Export screen as PNG |
|
|
44
|
+
| `export-html` / `export-zip` / `export-coding-handoff` / `export-design-as-figma-svg` | Export a finished design |
|
|
44
45
|
|
|
45
46
|
## Core Rules
|
|
46
47
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { defineAction, fail } from "@agent-native/core/action";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import takeDesignScreenshot from "./take-design-screenshot.js";
|
|
5
|
+
|
|
6
|
+
function suggestedPngFilename(filename: string): string {
|
|
7
|
+
const base = filename
|
|
8
|
+
.replace(/\\/g, "/")
|
|
9
|
+
.split("/")
|
|
10
|
+
.pop()
|
|
11
|
+
?.replace(/\.[^.]+$/, "")
|
|
12
|
+
.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
13
|
+
return `${base || "screen"}.png`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default defineAction({
|
|
17
|
+
description:
|
|
18
|
+
"Export one Design screen as a PNG image. Pass fileId, or pass designId " +
|
|
19
|
+
"and filename, to choose the screen. Returns a durable image URL; the " +
|
|
20
|
+
"screen is rendered at 1440px wide by default and full-page height.",
|
|
21
|
+
schema: z
|
|
22
|
+
.object({
|
|
23
|
+
designId: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Design project id. Required unless fileId is provided."),
|
|
27
|
+
fileId: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe(
|
|
31
|
+
"Specific design_files.id to export. Takes priority over designId and filename.",
|
|
32
|
+
),
|
|
33
|
+
filename: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.default("index.html")
|
|
37
|
+
.describe(
|
|
38
|
+
"Screen filename when fileId is not provided. Defaults to index.html.",
|
|
39
|
+
),
|
|
40
|
+
width: z.coerce
|
|
41
|
+
.number()
|
|
42
|
+
.int()
|
|
43
|
+
.min(200)
|
|
44
|
+
.max(3840)
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Render viewport width in pixels. Defaults to 1440."),
|
|
47
|
+
height: z.coerce
|
|
48
|
+
.number()
|
|
49
|
+
.int()
|
|
50
|
+
.min(200)
|
|
51
|
+
.max(4096)
|
|
52
|
+
.optional()
|
|
53
|
+
.describe(
|
|
54
|
+
"Optional viewport height in pixels. Full-page export is not cropped to this height.",
|
|
55
|
+
),
|
|
56
|
+
})
|
|
57
|
+
.refine(({ designId, fileId }) => Boolean(designId || fileId), {
|
|
58
|
+
message: "Provide designId or fileId to select a screen.",
|
|
59
|
+
path: ["designId"],
|
|
60
|
+
}),
|
|
61
|
+
readOnly: false,
|
|
62
|
+
http: { method: "POST" },
|
|
63
|
+
run: async ({ designId, fileId, filename, width, height }, ctx) => {
|
|
64
|
+
const result = await takeDesignScreenshot.run(
|
|
65
|
+
{
|
|
66
|
+
...(designId ? { designId } : {}),
|
|
67
|
+
...(fileId ? { fileId } : {}),
|
|
68
|
+
filename,
|
|
69
|
+
widths: [width ?? 1440],
|
|
70
|
+
...(height === undefined ? {} : { heights: [height] }),
|
|
71
|
+
},
|
|
72
|
+
ctx,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (result.ok !== true) return result;
|
|
76
|
+
|
|
77
|
+
const screenshot = result.screenshots?.[0];
|
|
78
|
+
if (!screenshot) {
|
|
79
|
+
fail("The PNG renderer did not return an image.", {
|
|
80
|
+
errorCode: "png_export_empty",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (!screenshot.url) {
|
|
84
|
+
if (screenshot.uploadError?.code === "file_upload_failed") {
|
|
85
|
+
fail(
|
|
86
|
+
"The PNG was rendered but its upload failed. Check the configured file storage provider and retry.",
|
|
87
|
+
{
|
|
88
|
+
errorCode: "file_upload_failed",
|
|
89
|
+
details: { designId: result.designId, fileId: result.fileId },
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
fail(
|
|
94
|
+
"The PNG was rendered but could not be returned because file storage is not configured. " +
|
|
95
|
+
"Connect or reconnect Builder.io in Settings → File uploads, or register a custom file provider.",
|
|
96
|
+
{
|
|
97
|
+
errorCode: "file_storage_not_configured",
|
|
98
|
+
details: { designId: result.designId, fileId: result.fileId },
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const screenFilename = result.filename ?? filename ?? "screen.html";
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
designId: result.designId,
|
|
108
|
+
fileId: result.fileId,
|
|
109
|
+
screenFilename,
|
|
110
|
+
filename: suggestedPngFilename(screenFilename),
|
|
111
|
+
mimeType: "image/png",
|
|
112
|
+
url: screenshot.url,
|
|
113
|
+
bytes: screenshot.bytes,
|
|
114
|
+
viewport: screenshot.viewport,
|
|
115
|
+
diagnostics: screenshot.diagnostics,
|
|
116
|
+
capturedAt: result.capturedAt,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
});
|