@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.
Files changed (41) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/chat/vitest.config.ts +12 -7
  3. package/corpus/templates/clips/app/hooks/use-recording-leave-guard.ts +70 -0
  4. package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
  5. package/corpus/templates/clips/app/routes/record.tsx +42 -0
  6. package/corpus/templates/clips/changelog/2026-08-18-clips-now-recovers-interrupted-uploads-and-reliably-restarts.md +6 -0
  7. package/corpus/templates/clips/changelog/2026-08-18-meeting-notifications-no-longer-show-decorative-left-side-lines.md +6 -0
  8. package/corpus/templates/clips/desktop/src/lib/updater.ts +21 -1
  9. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +0 -4
  10. package/corpus/templates/clips/desktop/src/styles.css +0 -18
  11. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -1
  12. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +6 -3
  13. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +98 -0
  14. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +54 -20
  15. package/corpus/templates/design/app/global.css +6 -0
  16. package/corpus/templates/design/app/root.tsx +1 -1
  17. package/corpus/templates/design/changelog/2026-08-18-design-chat-context-stays-out-of-the-composer.md +6 -0
  18. package/corpus/templates/design/server/plugins/agent-chat.ts +2 -0
  19. package/corpus/templates/slides/app/root.tsx +21 -0
  20. package/corpus/templates/slides/server/handlers/decks.ts +7 -3
  21. package/corpus/templates/slides/server/handlers/request-auth-context.ts +58 -1
  22. package/corpus/templates/slides/server/handlers/share.ts +7 -2
  23. package/corpus/templates/slides/server/handlers/uploads.ts +7 -2
  24. package/dist/action.d.ts +1 -0
  25. package/dist/action.js +1 -1
  26. package/dist/agent/engine/translate-anthropic.js +19 -1
  27. package/dist/agent/engine/types.d.ts +7 -0
  28. package/dist/agent/production-agent.js +60 -5
  29. package/dist/agent/thread-data-builder.d.ts +16 -4
  30. package/dist/agent/thread-data-builder.js +159 -5
  31. package/dist/client/agent-chat-adapter.js +13 -1
  32. package/dist/collab/awareness.d.ts +2 -2
  33. package/dist/collab/routes.d.ts +1 -1
  34. package/dist/observability/routes.d.ts +3 -3
  35. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  36. package/dist/server/agent-teams.js +3 -1
  37. package/dist/server/cors-origins.js +24 -1
  38. package/dist/templates/chat/vitest.config.ts +12 -7
  39. package/dist/transcription/builder-transcription.js +12 -1
  40. package/package.json +1 -1
  41. package/src/templates/chat/vitest.config.ts +12 -7
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8309
34
+ - template files: 8313
@@ -1,9 +1,14 @@
1
- import baseConfig from "@agent-native/core/vitest-config";
2
- import { mergeConfig } from "vitest/config";
1
+ import path from "node:path";
3
2
 
4
- import viteConfig from "./vite.config";
3
+ import { defineConfig } from "vitest/config";
5
4
 
6
- // Kept separate from vite.config.ts so the production config never imports
7
- // vitest: `agent-native build` loads vite.config.ts in installs where vitest
8
- // is absent.
9
- export default mergeConfig(viteConfig, baseConfig);
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). */}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-18
4
+ ---
5
+
6
+ Clips now recovers interrupted uploads and reliably restarts after installing desktop updates.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-18
4
+ ---
5
+
6
+ Meeting notifications no longer show decorative left-side lines.
@@ -1,7 +1,10 @@
1
- import { relaunch } from "@tauri-apps/plugin-process";
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.segments.iter().chain(std::iter::once(&session.path)) {
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
- setResponseStatus(event, 502);
364
- return {
365
- error:
366
- "The previous recording upload could not be cleaned up. Retry the upload restart.",
367
- };
368
- }
369
- const released = await compareAndSetAppState(
370
- cleanupStateKey,
371
- cleanupClaim as unknown as Record<string, unknown>,
372
- null,
373
- );
374
- if (!released) {
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 claim changed while releasing it`,
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
- await deleteResumableSession(recordingId, discardedGenerationId).catch(
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
- if (body?.requestStreaming === true) {
397
- const mimeType = normalizeVideoMimeType(body.mimeType);
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
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-18
4
+ ---
5
+
6
+ Design chat keeps context out of the composer and uses shorter completion summaries.
@@ -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 { resolveSlidesRequestAuthContext } from "./request-auth-context.js";
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 session = await resolveSlidesRequestAuthContext(event);
95
- if (!session.email) {
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
  }