@agent-native/core 0.161.14 → 0.161.17
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/chat/vitest.config.ts +12 -7
- package/corpus/templates/clips/app/hooks/use-recording-leave-guard.ts +70 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
- package/corpus/templates/clips/app/routes/record.tsx +42 -0
- package/corpus/templates/clips/changelog/2026-08-18-clips-now-recovers-interrupted-uploads-and-reliably-restarts.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-18-meeting-notifications-no-longer-show-decorative-left-side-lines.md +6 -0
- package/corpus/templates/clips/desktop/src/lib/updater.ts +21 -1
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +0 -4
- package/corpus/templates/clips/desktop/src/styles.css +0 -18
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -1
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +6 -3
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +98 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +54 -20
- package/corpus/templates/design/app/global.css +6 -0
- package/corpus/templates/design/app/root.tsx +1 -1
- package/corpus/templates/design/changelog/2026-08-18-design-chat-context-stays-out-of-the-composer.md +6 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +2 -0
- package/corpus/templates/slides/app/root.tsx +21 -0
- package/corpus/templates/slides/server/handlers/decks.ts +7 -3
- package/corpus/templates/slides/server/handlers/request-auth-context.ts +58 -1
- package/corpus/templates/slides/server/handlers/share.ts +7 -2
- package/corpus/templates/slides/server/handlers/uploads.ts +7 -2
- package/dist/action.d.ts +1 -0
- package/dist/action.js +1 -1
- package/dist/agent/engine/translate-anthropic.js +19 -1
- package/dist/agent/engine/types.d.ts +7 -0
- package/dist/agent/production-agent.js +60 -5
- package/dist/agent/thread-data-builder.d.ts +16 -4
- package/dist/agent/thread-data-builder.js +159 -5
- package/dist/client/agent-chat-adapter.js +13 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-teams.js +3 -1
- package/dist/server/cors-origins.js +24 -1
- package/dist/templates/chat/vitest.config.ts +12 -7
- package/dist/transcription/builder-transcription.js +12 -1
- package/package.json +1 -1
- package/src/templates/chat/vitest.config.ts +12 -7
package/corpus/README.md
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { mergeConfig } from "vitest/config";
|
|
1
|
+
import path from "node:path";
|
|
3
2
|
|
|
4
|
-
import
|
|
3
|
+
import { defineConfig } from "vitest/config";
|
|
5
4
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
// Keep tests independent from vite.config.ts: the production config starts
|
|
6
|
+
// Nitro/Vite watchers and evaluates browser-targeted CommonJS SSR modules.
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
"@": path.resolve(__dirname, "./app"),
|
|
11
|
+
"@shared": path.resolve(__dirname, "./shared"),
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useBlocker } from "react-router";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Blocks in-app route navigation while `hasRecordingAtRisk()` is true, using
|
|
6
|
+
* React Router's `useBlocker` — the same check the recorder's `beforeunload`
|
|
7
|
+
* handler already uses for tab close/reload. Without this, an in-app link
|
|
8
|
+
* (e.g. Library) unmounts the recorder route with no warning at all, since
|
|
9
|
+
* the browser never fires `beforeunload` for client-side navigation.
|
|
10
|
+
*
|
|
11
|
+
* Exposes a tiny state machine for a confirm dialog rather than a boolean,
|
|
12
|
+
* because confirming has to navigate away only after the dialog has actually
|
|
13
|
+
* finished closing — see the `onCloseAutoFocus` deferral below.
|
|
14
|
+
*/
|
|
15
|
+
export function useRecordingLeaveGuard(hasRecordingAtRisk: () => boolean) {
|
|
16
|
+
const blocker = useBlocker(
|
|
17
|
+
useCallback(
|
|
18
|
+
({ currentLocation, nextLocation }) =>
|
|
19
|
+
currentLocation.pathname !== nextLocation.pathname &&
|
|
20
|
+
hasRecordingAtRisk(),
|
|
21
|
+
[hasRecordingAtRisk],
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const [leavePromptOpen, setLeavePromptOpen] = useState(false);
|
|
26
|
+
// See delete-recording-menu.tsx's `deletedWhileOpenRef`: navigating away
|
|
27
|
+
// while Radix is still tearing down the AlertDialog portal can leave its
|
|
28
|
+
// body pointer-events lock behind. Close the dialog first and defer
|
|
29
|
+
// `blocker.proceed()` to `onCloseAutoFocus`, once Radix has actually
|
|
30
|
+
// finished closing it, instead of calling it straight from the click
|
|
31
|
+
// handler.
|
|
32
|
+
const proceedBlockerRef = useRef<() => void>(() => {});
|
|
33
|
+
proceedBlockerRef.current =
|
|
34
|
+
blocker.state === "blocked" ? blocker.proceed : () => {};
|
|
35
|
+
const leaveConfirmedRef = useRef(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (blocker.state === "blocked") setLeavePromptOpen(true);
|
|
39
|
+
}, [blocker.state]);
|
|
40
|
+
|
|
41
|
+
const confirmLeave = useCallback(() => {
|
|
42
|
+
leaveConfirmedRef.current = true;
|
|
43
|
+
setLeavePromptOpen(false);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const onDialogOpenChange = useCallback(
|
|
47
|
+
(open: boolean) => {
|
|
48
|
+
if (open) return;
|
|
49
|
+
setLeavePromptOpen(false);
|
|
50
|
+
if (!leaveConfirmedRef.current && blocker.state === "blocked") {
|
|
51
|
+
blocker.reset();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
[blocker],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const onCloseAutoFocus = useCallback((event: { preventDefault(): void }) => {
|
|
58
|
+
if (!leaveConfirmedRef.current) return;
|
|
59
|
+
leaveConfirmedRef.current = false;
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
setTimeout(() => proceedBlockerRef.current(), 0);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
leavePromptOpen,
|
|
66
|
+
onDialogOpenChange,
|
|
67
|
+
onCloseAutoFocus,
|
|
68
|
+
confirmLeave,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -1413,6 +1413,10 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
1413
1413
|
"Connect storage on the next screen: Builder.io (free tier storage + AI) or S3-compatible storage. Clips will finish saving it.",
|
|
1414
1414
|
connectStorageToRetryLoom:
|
|
1415
1415
|
"Connect storage on the next screen: Builder.io (free tier storage + AI) or S3-compatible storage. Clips will retry the import.",
|
|
1416
|
+
leaveConfirmTitle: "Leave and discard this recording?",
|
|
1417
|
+
leaveConfirmDescription:
|
|
1418
|
+
"Your in-progress recording hasn't finished saving. Leaving this page now will discard it.",
|
|
1419
|
+
leaveAndDiscard: "Leave and discard",
|
|
1416
1420
|
},
|
|
1417
1421
|
importRoute: {
|
|
1418
1422
|
pageTitle: "Import Loom — Clips",
|
|
@@ -35,6 +35,7 @@ import { Link, useLocation, useNavigate } from "react-router";
|
|
|
35
35
|
|
|
36
36
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
37
37
|
import { useDesktopPromo } from "@/hooks/use-desktop-promo";
|
|
38
|
+
import { useRecordingLeaveGuard } from "@/hooks/use-recording-leave-guard";
|
|
38
39
|
import {
|
|
39
40
|
fetchVideoStorageStatus,
|
|
40
41
|
useVideoStorageStatus,
|
|
@@ -2518,6 +2519,22 @@ export default function RecordRoute() {
|
|
|
2518
2519
|
};
|
|
2519
2520
|
}, [extensionCapture, stopLiveTranscription]);
|
|
2520
2521
|
|
|
2522
|
+
// In-app navigation (e.g. a Library link) unmounts this route the same way
|
|
2523
|
+
// a tab close does, but the browser never fires `beforeunload` for it — so
|
|
2524
|
+
// without this, the cleanup effect above ran `releaseCapture()`
|
|
2525
|
+
// unconditionally and silently killed an at-risk recording. Route every
|
|
2526
|
+
// in-app navigation attempt through the same `hasRecordingAtRisk()` check
|
|
2527
|
+
// `warnBeforeDiscard` uses, so both exits are gated by one check instead of
|
|
2528
|
+
// two divergent ones.
|
|
2529
|
+
const {
|
|
2530
|
+
leavePromptOpen,
|
|
2531
|
+
onDialogOpenChange,
|
|
2532
|
+
onCloseAutoFocus,
|
|
2533
|
+
confirmLeave,
|
|
2534
|
+
} = useRecordingLeaveGuard(
|
|
2535
|
+
useCallback(() => !!engineRef.current?.hasRecordingAtRisk(), []),
|
|
2536
|
+
);
|
|
2537
|
+
|
|
2521
2538
|
// -------------------------------------------------------------------------
|
|
2522
2539
|
// Render.
|
|
2523
2540
|
// -------------------------------------------------------------------------
|
|
@@ -2740,6 +2757,31 @@ export default function RecordRoute() {
|
|
|
2740
2757
|
</AlertDialogContent>
|
|
2741
2758
|
</AlertDialog>
|
|
2742
2759
|
|
|
2760
|
+
<AlertDialog open={leavePromptOpen} onOpenChange={onDialogOpenChange}>
|
|
2761
|
+
<AlertDialogContent onCloseAutoFocus={onCloseAutoFocus}>
|
|
2762
|
+
<AlertDialogHeader>
|
|
2763
|
+
<AlertDialogTitle>
|
|
2764
|
+
{t("recordRoute.leaveConfirmTitle")}
|
|
2765
|
+
</AlertDialogTitle>
|
|
2766
|
+
<AlertDialogDescription>
|
|
2767
|
+
{t("recordRoute.leaveConfirmDescription")}
|
|
2768
|
+
</AlertDialogDescription>
|
|
2769
|
+
</AlertDialogHeader>
|
|
2770
|
+
<AlertDialogFooter>
|
|
2771
|
+
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
|
2772
|
+
<AlertDialogAction
|
|
2773
|
+
onClick={(event) => {
|
|
2774
|
+
event.preventDefault();
|
|
2775
|
+
confirmLeave();
|
|
2776
|
+
}}
|
|
2777
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
2778
|
+
>
|
|
2779
|
+
{t("recordRoute.leaveAndDiscard")}
|
|
2780
|
+
</AlertDialogAction>
|
|
2781
|
+
</AlertDialogFooter>
|
|
2782
|
+
</AlertDialogContent>
|
|
2783
|
+
</AlertDialog>
|
|
2784
|
+
|
|
2743
2785
|
{/* Uploading overlay (also covers the compressing pass which can run
|
|
2744
2786
|
for several minutes on long recordings — without a distinct copy
|
|
2745
2787
|
users wonder if the app froze). */}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { invoke } from "@tauri-apps/api/core";
|
|
2
|
+
import { exit, relaunch } from "@tauri-apps/plugin-process";
|
|
2
3
|
import { check, Update } from "@tauri-apps/plugin-updater";
|
|
3
4
|
import { useEffect, useState } from "react";
|
|
4
5
|
|
|
6
|
+
import { isMacPlatform } from "./platform";
|
|
7
|
+
|
|
5
8
|
declare const __CLIPS_DESKTOP_LOCAL_BUILD__: boolean;
|
|
6
9
|
|
|
7
10
|
export type UpdateStatus =
|
|
@@ -166,9 +169,26 @@ export async function installAndRestart(): Promise<void> {
|
|
|
166
169
|
// bundle no longer matches the running process as short as possible — the
|
|
167
170
|
// process is torn down by `relaunch()` right after, so capture never runs
|
|
168
171
|
// against a swapped-out bundle.
|
|
172
|
+
const restartBundlePath = await invoke<string>("restart_bundle_path").catch(
|
|
173
|
+
(err) => {
|
|
174
|
+
console.error("[clips-updater] restart target lookup failed:", err);
|
|
175
|
+
return null;
|
|
176
|
+
},
|
|
177
|
+
);
|
|
169
178
|
if (pendingUpdate) {
|
|
170
179
|
await pendingUpdate.install();
|
|
171
180
|
}
|
|
181
|
+
if (isMacPlatform() && restartBundlePath) {
|
|
182
|
+
try {
|
|
183
|
+
await invoke("schedule_restart_after_exit", {
|
|
184
|
+
bundlePath: restartBundlePath,
|
|
185
|
+
});
|
|
186
|
+
await exit(0);
|
|
187
|
+
return;
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error("[clips-updater] macOS restart handoff failed:", err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
172
192
|
await relaunch();
|
|
173
193
|
}
|
|
174
194
|
|
|
@@ -386,7 +386,6 @@ export function MeetingNotification() {
|
|
|
386
386
|
return <div className="meeting-notification-root" />;
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
const isCalendar = data.type === "calendar";
|
|
390
389
|
const hasJoin = Boolean(data.joinUrl);
|
|
391
390
|
const provider = detectMeetingJoinProvider(data.joinUrl, data.platform);
|
|
392
391
|
const providerName = joinProviderLabel(provider);
|
|
@@ -404,9 +403,6 @@ export function MeetingNotification() {
|
|
|
404
403
|
onMouseEnter={() => setDomHovered(true)}
|
|
405
404
|
onMouseLeave={() => setDomHovered(false)}
|
|
406
405
|
>
|
|
407
|
-
<div
|
|
408
|
-
className={`meeting-notification-bar ${isCalendar ? "meeting-notification-bar-calendar" : "meeting-notification-bar-adhoc"}`}
|
|
409
|
-
/>
|
|
410
406
|
<div className="meeting-notification-content">
|
|
411
407
|
<div className="meeting-notification-title">{data.title}</div>
|
|
412
408
|
<div className="meeting-notification-subtitle">{data.subtitle}</div>
|
|
@@ -5414,24 +5414,6 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
5414
5414
|
max-width: 440px;
|
|
5415
5415
|
}
|
|
5416
5416
|
|
|
5417
|
-
.meeting-notification-bar {
|
|
5418
|
-
width: 3px;
|
|
5419
|
-
height: 36px;
|
|
5420
|
-
border-radius: 999px;
|
|
5421
|
-
flex-shrink: 0;
|
|
5422
|
-
}
|
|
5423
|
-
|
|
5424
|
-
.meeting-notification-bar-calendar {
|
|
5425
|
-
background: #7dd3fc;
|
|
5426
|
-
}
|
|
5427
|
-
|
|
5428
|
-
.meeting-notification-bar-adhoc {
|
|
5429
|
-
background: transparent;
|
|
5430
|
-
border-left: 3px dashed #a3a3a3;
|
|
5431
|
-
width: 3px;
|
|
5432
|
-
border-radius: 0;
|
|
5433
|
-
}
|
|
5434
|
-
|
|
5435
5417
|
.meeting-notification-content {
|
|
5436
5418
|
flex: 1;
|
|
5437
5419
|
min-width: 0;
|
|
@@ -52,7 +52,7 @@ use state::{
|
|
|
52
52
|
};
|
|
53
53
|
use util::{
|
|
54
54
|
configure_overlay_behavior, is_recording_active, present_interactive_window,
|
|
55
|
-
set_capture_excluded,
|
|
55
|
+
restart_bundle_path, schedule_restart_after_exit, set_capture_excluded,
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
// Embedded fallback icon — a tiny 16x16 solid purple PNG so the binary always
|
|
@@ -248,6 +248,8 @@ pub fn run() {
|
|
|
248
248
|
// persistent log file (production debugging)
|
|
249
249
|
logfile::frontend_log,
|
|
250
250
|
logfile::open_logs,
|
|
251
|
+
restart_bundle_path,
|
|
252
|
+
schedule_restart_after_exit,
|
|
251
253
|
])
|
|
252
254
|
.plugin(tauri_plugin_shell::init())
|
|
253
255
|
.plugin(tauri_plugin_deep_link::init())
|
|
@@ -4055,7 +4055,11 @@ fn recording_file_has_bytes(path: &Path) -> bool {
|
|
|
4055
4055
|
}
|
|
4056
4056
|
|
|
4057
4057
|
fn remove_empty_recording_artifacts(session: &NativeFullscreenSession) {
|
|
4058
|
-
for path in session
|
|
4058
|
+
for path in session
|
|
4059
|
+
.segments
|
|
4060
|
+
.iter()
|
|
4061
|
+
.chain(std::iter::once(&session.path))
|
|
4062
|
+
{
|
|
4059
4063
|
if !path.exists() {
|
|
4060
4064
|
continue;
|
|
4061
4065
|
}
|
|
@@ -4495,8 +4499,7 @@ fn write_bytes_atomically(path: &Path, data: &[u8], label: &str) -> Result<(), S
|
|
|
4495
4499
|
.and_then(|value| value.to_str())
|
|
4496
4500
|
.unwrap_or("recording");
|
|
4497
4501
|
let temporary = path.with_file_name(format!(".{file_name}.tmp"));
|
|
4498
|
-
std::fs::write(&temporary, data)
|
|
4499
|
-
.map_err(|error| format!("{label} write failed: {error}"))?;
|
|
4502
|
+
std::fs::write(&temporary, data).map_err(|error| format!("{label} write failed: {error}"))?;
|
|
4500
4503
|
if let Err(error) = std::fs::rename(&temporary, path) {
|
|
4501
4504
|
let _ = std::fs::remove_file(&temporary);
|
|
4502
4505
|
return Err(format!("{label} finalize failed: {error}"));
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
ffi::OsString,
|
|
3
|
+
path::{Path, PathBuf},
|
|
4
|
+
process::{Command, Stdio},
|
|
5
|
+
};
|
|
6
|
+
|
|
1
7
|
use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
|
|
2
8
|
|
|
3
9
|
use crate::dlog;
|
|
@@ -505,6 +511,78 @@ pub fn frontmost_bundle_id() -> Option<String> {
|
|
|
505
511
|
None
|
|
506
512
|
}
|
|
507
513
|
|
|
514
|
+
fn bundle_path_from_executable_path(executable_path: &Path) -> Option<PathBuf> {
|
|
515
|
+
let macos_dir = executable_path.parent()?;
|
|
516
|
+
if macos_dir.file_name()?.to_str()? != "MacOS" {
|
|
517
|
+
return None;
|
|
518
|
+
}
|
|
519
|
+
let contents_dir = macos_dir.parent()?;
|
|
520
|
+
let bundle_path = contents_dir.parent()?;
|
|
521
|
+
Some(bundle_path.to_path_buf())
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#[tauri::command]
|
|
525
|
+
pub fn restart_bundle_path() -> Result<String, String> {
|
|
526
|
+
let executable_path = std::env::current_exe().map_err(|err| format!("current_exe: {err}"))?;
|
|
527
|
+
#[cfg(target_os = "macos")]
|
|
528
|
+
let bundle_path = bundle_path_from_executable_path(&executable_path).ok_or_else(|| {
|
|
529
|
+
format!(
|
|
530
|
+
"could not derive macOS bundle path from {}",
|
|
531
|
+
executable_path.display()
|
|
532
|
+
)
|
|
533
|
+
})?;
|
|
534
|
+
#[cfg(not(target_os = "macos"))]
|
|
535
|
+
let bundle_path = executable_path;
|
|
536
|
+
Ok(bundle_path.to_string_lossy().into_owned())
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
#[cfg(target_os = "macos")]
|
|
540
|
+
fn spawn_macos_restart_helper(bundle_path: &Path, args: &[OsString]) -> Result<(), String> {
|
|
541
|
+
let parent_pid = std::process::id().to_string();
|
|
542
|
+
let mut command = Command::new("/bin/sh");
|
|
543
|
+
command
|
|
544
|
+
.arg("-c")
|
|
545
|
+
.arg(
|
|
546
|
+
r#"
|
|
547
|
+
parent_pid="$1"
|
|
548
|
+
bundle_path="$2"
|
|
549
|
+
shift 2
|
|
550
|
+
while kill -0 "$parent_pid" >/dev/null 2>&1; do
|
|
551
|
+
sleep 0.1
|
|
552
|
+
done
|
|
553
|
+
if [ "$#" -gt 0 ]; then
|
|
554
|
+
exec /usr/bin/open -n "$bundle_path" --args "$@"
|
|
555
|
+
else
|
|
556
|
+
exec /usr/bin/open -n "$bundle_path"
|
|
557
|
+
fi
|
|
558
|
+
"#,
|
|
559
|
+
)
|
|
560
|
+
.arg("clips-restart-helper")
|
|
561
|
+
.arg(parent_pid)
|
|
562
|
+
.arg(bundle_path)
|
|
563
|
+
.args(args.iter().skip(1))
|
|
564
|
+
.stdin(Stdio::null())
|
|
565
|
+
.stdout(Stdio::null())
|
|
566
|
+
.stderr(Stdio::null());
|
|
567
|
+
|
|
568
|
+
command
|
|
569
|
+
.spawn()
|
|
570
|
+
.map_err(|err| format!("spawn restart helper: {err}"))?;
|
|
571
|
+
Ok(())
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#[cfg(not(target_os = "macos"))]
|
|
575
|
+
fn spawn_macos_restart_helper(_bundle_path: &Path, _args: &[OsString]) -> Result<(), String> {
|
|
576
|
+
Err("macOS restart helper is unavailable on this platform".to_string())
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
#[tauri::command]
|
|
580
|
+
pub fn schedule_restart_after_exit(bundle_path: String) -> Result<(), String> {
|
|
581
|
+
let bundle_path = PathBuf::from(bundle_path);
|
|
582
|
+
let args: Vec<OsString> = std::env::args_os().collect();
|
|
583
|
+
spawn_macos_restart_helper(&bundle_path, &args)
|
|
584
|
+
}
|
|
585
|
+
|
|
508
586
|
pub fn set_dictation_active(app: &AppHandle, active: bool) {
|
|
509
587
|
if let Some(state) = app.try_state::<DictationActive>() {
|
|
510
588
|
if let Ok(mut g) = state.0.lock() {
|
|
@@ -537,3 +615,23 @@ pub fn hide_voice_wake_popover(app: &AppHandle) {
|
|
|
537
615
|
}
|
|
538
616
|
}
|
|
539
617
|
}
|
|
618
|
+
|
|
619
|
+
#[cfg(test)]
|
|
620
|
+
mod tests {
|
|
621
|
+
use super::bundle_path_from_executable_path;
|
|
622
|
+
use std::path::Path;
|
|
623
|
+
|
|
624
|
+
#[test]
|
|
625
|
+
fn derives_macos_bundle_path_from_app_executable() {
|
|
626
|
+
let executable = Path::new("/Applications/Clips.app/Contents/MacOS/Clips");
|
|
627
|
+
let bundle =
|
|
628
|
+
bundle_path_from_executable_path(executable).expect("expected macOS bundle path");
|
|
629
|
+
assert_eq!(bundle, Path::new("/Applications/Clips.app"));
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
#[test]
|
|
633
|
+
fn rejects_non_bundle_executable_paths() {
|
|
634
|
+
let executable = Path::new("/Users/steve/dev/Clips");
|
|
635
|
+
assert!(bundle_path_from_executable_path(executable).is_none());
|
|
636
|
+
}
|
|
637
|
+
}
|
|
@@ -174,6 +174,14 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
174
174
|
uploadGenerationId?: string;
|
|
175
175
|
useGenerationFence?: boolean;
|
|
176
176
|
} | null;
|
|
177
|
+
const requestedStreamingMimeType =
|
|
178
|
+
body?.requestStreaming === true
|
|
179
|
+
? normalizeVideoMimeType(body.mimeType)
|
|
180
|
+
: null;
|
|
181
|
+
if (body?.requestStreaming === true && !requestedStreamingMimeType) {
|
|
182
|
+
setResponseStatus(event, 400);
|
|
183
|
+
return { error: "A supported video mimeType is required for retry" };
|
|
184
|
+
}
|
|
177
185
|
const recoveryEnabled = await isFeatureFlagEnabled(UPLOAD_RETRY_RESUME_FLAG, {
|
|
178
186
|
userEmail: ownerEmail,
|
|
179
187
|
userKey: ownerEmail,
|
|
@@ -353,6 +361,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
353
361
|
if (claimed) cleanupClaim = candidate;
|
|
354
362
|
}
|
|
355
363
|
|
|
364
|
+
let resumableCleanupFailed = false;
|
|
356
365
|
if (discardedResumableSession) {
|
|
357
366
|
if (cleanupClaim) {
|
|
358
367
|
const cleaned = await abortResumableUploadSession(
|
|
@@ -360,21 +369,34 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
360
369
|
{ label: `reset-${recordingId}` },
|
|
361
370
|
);
|
|
362
371
|
if (!cleaned) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
372
|
+
if (!allowsSqlRecordingChunkScratch()) {
|
|
373
|
+
setResponseStatus(event, 502);
|
|
374
|
+
return {
|
|
375
|
+
error:
|
|
376
|
+
"The previous recording upload could not be cleaned up. Retry the upload restart.",
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
// The provider-side session may remain orphaned when abort fails,
|
|
380
|
+
// but local buffered scratch is a complete recovery path for the
|
|
381
|
+
// saved file. Keep the cleanup claim for a later retry, remove the
|
|
382
|
+
// stale local session handle, and force the new chunks through the
|
|
383
|
+
// buffered handler instead of returning the same 502 repeatedly.
|
|
375
384
|
console.warn(
|
|
376
|
-
`[reset-chunks-${recordingId}] cleanup
|
|
385
|
+
`[reset-chunks-${recordingId}] provider cleanup failed; using buffered retry and retaining cleanup claim`,
|
|
377
386
|
);
|
|
387
|
+
resumableCleanupFailed = true;
|
|
388
|
+
}
|
|
389
|
+
if (!resumableCleanupFailed) {
|
|
390
|
+
const released = await compareAndSetAppState(
|
|
391
|
+
cleanupStateKey,
|
|
392
|
+
cleanupClaim as unknown as Record<string, unknown>,
|
|
393
|
+
null,
|
|
394
|
+
);
|
|
395
|
+
if (!released) {
|
|
396
|
+
console.warn(
|
|
397
|
+
`[reset-chunks-${recordingId}] cleanup claim changed while releasing it`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
378
400
|
}
|
|
379
401
|
}
|
|
380
402
|
}
|
|
@@ -385,22 +407,34 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
385
407
|
);
|
|
386
408
|
// Clear any stale resumable session so a buffered retry does not
|
|
387
409
|
// accidentally route through handleResumableChunk with stale offsets.
|
|
388
|
-
if (!discardedResumableSession || cleanupClaim) {
|
|
389
|
-
|
|
390
|
-
()
|
|
391
|
-
)
|
|
410
|
+
if (!discardedResumableSession || cleanupClaim || resumableCleanupFailed) {
|
|
411
|
+
try {
|
|
412
|
+
await deleteResumableSession(recordingId, discardedGenerationId);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
console.warn(
|
|
415
|
+
`[reset-chunks-${recordingId}] local resumable session cleanup failed`,
|
|
416
|
+
error,
|
|
417
|
+
);
|
|
418
|
+
setResponseStatus(event, 502);
|
|
419
|
+
return {
|
|
420
|
+
error:
|
|
421
|
+
"The previous recording upload could not be reset locally. Retry the upload restart.",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
392
424
|
}
|
|
393
425
|
|
|
394
426
|
let uploadMode: UploadMode = "buffered";
|
|
395
427
|
let compensateStartedSession: (() => Promise<void>) | null = null;
|
|
396
|
-
|
|
397
|
-
|
|
428
|
+
const bufferedFallbackAvailable = allowsSqlRecordingChunkScratch();
|
|
429
|
+
const shouldRetryStreaming =
|
|
430
|
+
body?.requestStreaming === true && !resumableCleanupFailed;
|
|
431
|
+
if (shouldRetryStreaming) {
|
|
432
|
+
const mimeType = requestedStreamingMimeType;
|
|
398
433
|
if (!mimeType) {
|
|
399
434
|
setResponseStatus(event, 400);
|
|
400
435
|
return { error: "A supported video mimeType is required for retry" };
|
|
401
436
|
}
|
|
402
437
|
|
|
403
|
-
const bufferedFallbackAvailable = allowsSqlRecordingChunkScratch();
|
|
404
438
|
const uploadProvider = await getActiveFileUploadProviderForRequest();
|
|
405
439
|
if (
|
|
406
440
|
shouldEnableStreamingUpload({
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
@source "./**/*.{ts,tsx}";
|
|
9
9
|
|
|
10
|
+
/* Keep agent context available to the Design agent without making the
|
|
11
|
+
* composer carry implementation details as visible chips. */
|
|
12
|
+
[data-design-app] .agent-composer-context-row {
|
|
13
|
+
display: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
:root {
|
|
11
17
|
--background: 0 0% 100%;
|
|
12
18
|
--foreground: 0 0% 10%;
|
|
@@ -70,7 +70,7 @@ const LOCALE_INIT_SCRIPT = getLocaleInitScript();
|
|
|
70
70
|
|
|
71
71
|
export function Layout({ children }: { children: React.ReactNode }) {
|
|
72
72
|
return (
|
|
73
|
-
<html lang="en" suppressHydrationWarning>
|
|
73
|
+
<html lang="en" data-design-app suppressHydrationWarning>
|
|
74
74
|
<head>
|
|
75
75
|
<meta charSet="utf-8" />
|
|
76
76
|
<meta
|
|
@@ -65,6 +65,8 @@ export default createAgentChatPlugin({
|
|
|
65
65
|
resolveOrgId: async (event) => (await getOrgContext(event)).orgId,
|
|
66
66
|
systemPrompt: `You are an AI prototyping assistant. You create and edit designs, files, design systems, variants, exports, sharing, and connected repository context through actions and shared application state.
|
|
67
67
|
|
|
68
|
+
Final responses should be concise and operational. Lead with what changed or what is needed. For ordinary design actions, use 1-3 short sentences or at most 3 flat bullets. Do not narrate your process, repeat the user's request, paste HTML or tool results, or write an essay. Mention screenshots and audits only as brief completion evidence. Expand only when the user explicitly asks for an explanation or detailed critique.
|
|
69
|
+
|
|
68
70
|
When a user message begins with [Reprompt selection], the design must remain unchanged until the user accepts a preview. Call propose-node-rewrite with the exact repromptId, target, and baseVersionHash from the message. Never call edit-design, update-design, update-file, generate-design, apply-visual-edit, or any other content-writing action for that turn. The proposal action stores preview state only; the frontend-only resolve-node-rewrite action persists a chosen variant after the user presses Accept.
|
|
69
71
|
|
|
70
72
|
When a user message begins with [Selection question], answer about the captured element and selected subtree without changing the design. You may use read-only actions when more context is needed, but do not call any content-writing action. If the user actually intended an edit, explain that they can choose Preview change from the composer mode menu and resend.
|
|
@@ -66,6 +66,26 @@ const BARE_ROUTES = new Set(["/slide"]);
|
|
|
66
66
|
/** Route prefixes that render without the app shell */
|
|
67
67
|
const BARE_PREFIXES = ["/share/", "/p/"];
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Routes that serve deck content to any visitor — owner, teammate, or an
|
|
71
|
+
* anonymous recipient of a shared link — rather than an app-management
|
|
72
|
+
* surface. `/deck/:id` renders the full editor shell (so it's not "bare"
|
|
73
|
+
* above: viewers still get chrome, just read-only), but it must never force
|
|
74
|
+
* sign-in or gate on first-run onboarding, or a shared link flashes the deck
|
|
75
|
+
* and then buries it under "create your first deck" (the onboarding gate has
|
|
76
|
+
* no route awareness of its own — see `sessionBypass` below). `/present` is
|
|
77
|
+
* the full-screen presentation view and `/slide` is the agent-embed preview;
|
|
78
|
+
* both are shown to viewers the same way.
|
|
79
|
+
*/
|
|
80
|
+
export function isShareableContentPath(pathname: string): boolean {
|
|
81
|
+
return (
|
|
82
|
+
BARE_ROUTES.has(pathname) ||
|
|
83
|
+
BARE_PREFIXES.some((p) => pathname.startsWith(p)) ||
|
|
84
|
+
pathname.startsWith("/deck/") ||
|
|
85
|
+
pathname.endsWith("/present")
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
69
89
|
export const links: LinksFunction = () => [
|
|
70
90
|
{ rel: "stylesheet", href: stylesheet },
|
|
71
91
|
];
|
|
@@ -311,6 +331,7 @@ export default function Root() {
|
|
|
311
331
|
queryClient={queryClient}
|
|
312
332
|
defaultTheme="dark"
|
|
313
333
|
i18n={{ catalog: i18nCatalog }}
|
|
334
|
+
sessionBypass={isShareableContentPath(location.pathname)}
|
|
314
335
|
>
|
|
315
336
|
<AppContent />
|
|
316
337
|
</AppProviders>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { recordChange } from "@agent-native/core/server/poll";
|
|
2
2
|
import { defineEventHandler, setResponseStatus, createEventStream } from "h3";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { resolveSlidesRequestAuth } from "./request-auth-context.js";
|
|
5
5
|
|
|
6
6
|
// --- SSE for change notifications ---
|
|
7
7
|
type SSEPush = (data: string) => void;
|
|
@@ -91,8 +91,12 @@ export function notifyClients(
|
|
|
91
91
|
// callers can't tail the stream. (The agent path runs server-side and is
|
|
92
92
|
// not affected.)
|
|
93
93
|
export const deckEvents = defineEventHandler(async (event) => {
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
94
|
+
const auth = await resolveSlidesRequestAuth(event);
|
|
95
|
+
if (!auth.ok) {
|
|
96
|
+
setResponseStatus(event, auth.statusCode);
|
|
97
|
+
return { error: auth.error };
|
|
98
|
+
}
|
|
99
|
+
if (!auth.context.email) {
|
|
96
100
|
setResponseStatus(event, 401);
|
|
97
101
|
return { error: "Unauthorized" };
|
|
98
102
|
}
|