@agent-native/core 0.149.5 → 0.151.0

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 (197) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  3. package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +5 -0
  4. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +83 -45
  5. package/corpus/templates/analytics/changelog/2026-08-11-analytics-dashboards-keep-the-selected-tab-when-reopened.md +6 -0
  6. package/corpus/templates/assets/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  7. package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +5 -0
  8. package/corpus/templates/brain/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  9. package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +5 -0
  10. package/corpus/templates/calendar/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  11. package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +5 -0
  12. package/corpus/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  13. package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
  14. package/corpus/templates/clips/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  15. package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +5 -0
  16. package/corpus/templates/clips/changelog/2026-08-11-clips-recovers-from-transient-updates-and-avoids-macos-priva.md +6 -0
  17. package/corpus/templates/clips/desktop/src/app.tsx +32 -176
  18. package/corpus/templates/clips/desktop/src/lib/updater.ts +52 -37
  19. package/corpus/templates/clips/desktop/src/styles.css +1 -166
  20. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -0
  21. package/corpus/templates/clips/desktop/src-tauri/Info.plist +4 -0
  22. package/corpus/templates/clips/desktop/src-tauri/src/eventkit.rs +28 -1
  23. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
  24. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +48 -1
  25. package/corpus/templates/clips/desktop/src-tauri/src/permission_status.rs +3 -0
  26. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +5 -0
  27. package/corpus/templates/clips/server/plugins/auth.ts +3 -0
  28. package/corpus/templates/content/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  29. package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +5 -0
  30. package/corpus/templates/content/actions/_document-discovery-query.ts +127 -0
  31. package/corpus/templates/content/actions/list-documents.ts +189 -198
  32. package/corpus/templates/content/actions/search-documents.ts +95 -24
  33. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +2 -1
  34. package/corpus/templates/content/app/hooks/use-documents.ts +84 -6
  35. package/corpus/templates/content/shared/api.ts +10 -0
  36. package/corpus/templates/design/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  37. package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +5 -0
  38. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +5 -1
  39. package/corpus/templates/design/changelog/2026-08-11-design-agent-prompts-copy-successfully-in-the-desktop-app.md +6 -0
  40. package/corpus/templates/dispatch/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  41. package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +5 -0
  42. package/corpus/templates/dispatch/app/global.css +2 -2
  43. package/corpus/templates/dispatch/changelog/2026-08-10-desktop-workspace-sign-in.md +6 -0
  44. package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-no-longer-shows-the-generic-chat-starter-among-defa.md +6 -0
  45. package/corpus/templates/dispatch/changelog/2026-08-11-integrations-and-scheduled-work-open-as-first-class-electron-pages.md +6 -0
  46. package/corpus/templates/dispatch/changelog/2026-08-11-sidebar-text-is-slightly-easier-to-read-in-light-and-dark-mo.md +6 -0
  47. package/corpus/templates/dispatch/server/lib/identity-sso.ts +57 -31
  48. package/corpus/templates/dispatch/server/plugins/feature-flags.ts +5 -0
  49. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +82 -33
  50. package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +7 -15
  51. package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +9 -1
  52. package/corpus/templates/dispatch/shared/feature-flags.ts +15 -0
  53. package/corpus/templates/factory/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  54. package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +5 -0
  55. package/corpus/templates/forms/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  56. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +8 -7
  57. package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +5 -0
  58. package/corpus/templates/forms/app/hooks/use-forms.ts +8 -1
  59. package/corpus/templates/forms/changelog/2026-08-11-fixed-public-forms-so-conditional-questions-stay-hidden-unti.md +6 -0
  60. package/corpus/templates/forms/server/handlers/submissions.ts +2 -1
  61. package/corpus/templates/forms/server/lib/public-form-ssr.ts +37 -2
  62. package/corpus/templates/forms/shared/page-url.ts +54 -0
  63. package/corpus/templates/forms/shared/types.ts +4 -4
  64. package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +5 -0
  65. package/corpus/templates/mail/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  66. package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +5 -0
  67. package/corpus/templates/plan/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  68. package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +5 -0
  69. package/corpus/templates/slides/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  70. package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +5 -0
  71. package/corpus/templates/slides/actions/add-slide.ts +0 -27
  72. package/corpus/templates/slides/actions/import-file.ts +28 -9
  73. package/corpus/templates/slides/actions/view-screen.ts +4 -4
  74. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +8 -1
  75. package/corpus/templates/slides/app/components/editor/AiEditingMarker.tsx +25 -0
  76. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +43 -48
  77. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +9 -6
  78. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -9
  79. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +2 -0
  80. package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +0 -26
  81. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +904 -217
  82. package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +1 -0
  83. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +11 -0
  84. package/corpus/templates/slides/app/components/editor/inline-edit-session.ts +16 -0
  85. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +22 -6
  86. package/corpus/templates/slides/app/components/editor/style-clipboard.ts +61 -0
  87. package/corpus/templates/slides/app/components/layout/Layout.tsx +26 -2
  88. package/corpus/templates/slides/app/components/ui/context-menu.tsx +1 -0
  89. package/corpus/templates/slides/app/context/DeckContext.tsx +75 -25
  90. package/corpus/templates/slides/app/global.css +1 -1
  91. package/corpus/templates/slides/app/i18n/en-US.ts +6 -1
  92. package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -1
  93. package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +12 -12
  94. package/corpus/templates/slides/app/lib/slide-agent-context.ts +52 -0
  95. package/corpus/templates/slides/app/lib/slide-image-replacement.ts +61 -0
  96. package/corpus/templates/slides/app/pages/DeckEditor.tsx +55 -116
  97. package/corpus/templates/slides/app/pages/Index.tsx +28 -3
  98. package/corpus/templates/slides/changelog/2026-08-11-agent-chat-now-follows-the-current-slide-and-selection.md +6 -0
  99. package/corpus/templates/slides/changelog/2026-08-11-deck-titles-now-use-slide-content-instead-of-placeholder-filenames.md +6 -0
  100. package/corpus/templates/slides/changelog/2026-08-11-history-now-opens-from-the-deck-overflow-menu.md +6 -0
  101. package/corpus/templates/slides/changelog/2026-08-11-images-can-be-dropped-directly-onto-slides-and-resized-or-re.md +6 -0
  102. package/corpus/templates/slides/changelog/2026-08-11-new-decks-now-transition-directly-to-a-full-page-editor-load.md +6 -0
  103. package/corpus/templates/slides/changelog/2026-08-11-selected-slide-layers-can-be-nudged-precisely-with-arrow-key.md +6 -0
  104. package/corpus/templates/slides/changelog/2026-08-11-slides-can-copy-and-paste-element-styles-with-cmd-option-c-v.md +6 -0
  105. package/corpus/templates/slides/changelog/2026-08-11-slides-shows-one-compact-ai-marker-and-keeps-your-current-sl.md +6 -0
  106. package/corpus/templates/slides/changelog/2026-08-11-text-edits-preserve-slide-layout-when-no-changes-are-made.md +6 -0
  107. package/corpus/templates/slides/changelog/2026-08-11-the-style-toolbar-now-uses-the-full-available-width.md +6 -0
  108. package/corpus/templates/slides/changelog/2026-08-11-undo-and-redo-now-move-one-visible-slide-state-at-a-time.md +6 -0
  109. package/corpus/templates/slides/server/lib/chat-attachments.ts +6 -2
  110. package/corpus/templates/slides/shared/deck-title.ts +56 -0
  111. package/corpus/templates/tasks/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  112. package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +5 -0
  113. package/dist/agent/run-store.js +6 -1
  114. package/dist/agent/thread-data-builder.js +19 -44
  115. package/dist/agent/types.d.ts +13 -0
  116. package/dist/catalog.json +1 -0
  117. package/dist/cli/create.js +1 -1
  118. package/dist/client/AssistantChat.js +58 -10
  119. package/dist/client/FileStorageSetupCard.d.ts +6 -0
  120. package/dist/client/FileStorageSetupCard.js +18 -0
  121. package/dist/client/NewWorkspaceAppFlow.js +10 -0
  122. package/dist/client/agent-chat-adapter.js +87 -11
  123. package/dist/client/agent-chat.d.ts +1 -1
  124. package/dist/client/agent-chat.js +8 -3
  125. package/dist/client/chat/attachment-adapters.js +10 -0
  126. package/dist/client/chat/message-components.d.ts +4 -1
  127. package/dist/client/chat/message-components.js +37 -13
  128. package/dist/client/chat/tool-call-display.js +14 -0
  129. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  130. package/dist/client/chat-first/apps-rail.js +6 -6
  131. package/dist/client/chat-first/copy.js +1 -0
  132. package/dist/client/chat-first/types.d.ts +1 -0
  133. package/dist/client/extensions/EmbeddedExtension.js +1 -1
  134. package/dist/client/extensions/ExtensionViewer.js +1 -1
  135. package/dist/client/extensions/InlineExtensionFrame.js +1 -1
  136. package/dist/client/onboarding/FirstRunOnboarding.js +17 -2
  137. package/dist/client/settings/FileStorageSettingsForm.d.ts +2 -0
  138. package/dist/client/settings/FileStorageSettingsForm.js +113 -0
  139. package/dist/client/settings/SettingsPanel.js +2 -1
  140. package/dist/client/sse-event-processor.js +21 -0
  141. package/dist/extensions/actions.js +2 -2
  142. package/dist/extensions/html-shell.js +3 -1
  143. package/dist/file-upload/pre-upload-attachments.d.ts +15 -10
  144. package/dist/file-upload/pre-upload-attachments.js +68 -24
  145. package/dist/file-upload/registry.d.ts +4 -5
  146. package/dist/file-upload/registry.js +7 -14
  147. package/dist/file-upload/s3.d.ts +9 -0
  148. package/dist/file-upload/s3.js +168 -0
  149. package/dist/file-upload/types.d.ts +3 -3
  150. package/dist/file-upload/types.js +2 -2
  151. package/dist/onboarding/app-profile.js +23 -1
  152. package/dist/onboarding/default-steps.js +69 -0
  153. package/dist/onboarding/types.d.ts +2 -0
  154. package/dist/progress/routes.d.ts +1 -1
  155. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  156. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  157. package/dist/resources/handlers.d.ts +1 -1
  158. package/dist/secrets/routes.d.ts +6 -6
  159. package/dist/server/agent-chat/browser-team-tools.js +20 -0
  160. package/dist/server/agent-chat-plugin.js +12 -3
  161. package/dist/server/auth.js +45 -21
  162. package/dist/server/better-auth-instance.d.ts +2 -0
  163. package/dist/server/better-auth-instance.js +2 -2
  164. package/dist/server/core-routes-plugin.d.ts +1 -0
  165. package/dist/server/core-routes-plugin.js +26 -16
  166. package/dist/server/identity-sso-store.d.ts +3 -0
  167. package/dist/server/identity-sso-store.js +45 -0
  168. package/dist/server/identity-sso.d.ts +20 -10
  169. package/dist/server/identity-sso.js +79 -31
  170. package/dist/server/index.d.ts +1 -1
  171. package/dist/server/index.js +1 -1
  172. package/dist/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  173. package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
  174. package/dist/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  175. package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +5 -0
  176. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
  177. package/dist/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  178. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
  179. package/dist/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
  180. package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -0
  181. package/dist/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
  182. package/docs/content/authentication.mdx +27 -0
  183. package/docs/content/cross-app-sso.mdx +38 -8
  184. package/docs/content/extensions.mdx +15 -15
  185. package/docs/content/file-uploads.mdx +10 -12
  186. package/docs/content/generative-ui.mdx +6 -1
  187. package/package.json +2 -2
  188. package/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  189. package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
  190. package/src/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  191. package/src/templates/default/.agents/skills/frontend-design/SKILL.md +5 -0
  192. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
  193. package/src/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  194. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
  195. package/src/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
  196. package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -0
  197. package/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
@@ -3,6 +3,7 @@ export {
3
3
  createSlidesCanvasInteractionCore,
4
4
  isWithinSlidesCanvasEdgeMoveBand,
5
5
  resolveSlidesCanvasDragTarget,
6
+ resolveSlidesCanvasNudge,
6
7
  resolveSlidesCanvasPointerIntent,
7
8
  slidesCanvasInteractionCore,
8
9
  SLIDES_CANVAS_EDGE_MOVE_BAND,
@@ -76,6 +76,17 @@ export function createSlidesCanvasInteractionCore(
76
76
  return createCanvasInteractionCore(slidesCanvasInteractionConfig, adapter);
77
77
  }
78
78
 
79
+ /**
80
+ * Slides only nudges selected layers for plain arrows and Shift+arrows. Other
81
+ * modifier chords stay native so browser and editing shortcuts keep working.
82
+ */
83
+ export function resolveSlidesCanvasNudge(
84
+ input: Parameters<typeof slidesCanvasInteractionCore.nudge>[0],
85
+ ) {
86
+ if (input.altKey || input.ctrlKey || input.metaKey) return null;
87
+ return slidesCanvasInteractionCore.nudge(input);
88
+ }
89
+
79
90
  /** Creates one shared controller per live Slides pointer gesture. */
80
91
  export function createSlidesCanvasGestureController(
81
92
  adapter: SlidesCanvasGestureAdapter,
@@ -0,0 +1,16 @@
1
+ export interface InlineEditContentSnapshot {
2
+ slideId: string;
3
+ content: string;
4
+ }
5
+
6
+ export function shouldPersistInlineEditContent(
7
+ initial: InlineEditContentSnapshot | null,
8
+ current: InlineEditContentSnapshot | null,
9
+ ): boolean {
10
+ if (!current) return false;
11
+ return (
12
+ !initial ||
13
+ initial.slideId !== current.slideId ||
14
+ initial.content !== current.content
15
+ );
16
+ }
@@ -521,15 +521,31 @@ export function removeSlideObjectAndLayoutSpacer(element: HTMLElement): void {
521
521
  element.remove();
522
522
  }
523
523
 
524
+ /**
525
+ * Whether Delete may remove this selected element from the slide content.
526
+ *
527
+ * Selection is intentionally broader than freeform object manipulation: an
528
+ * AI-generated flow-layout div is still user content and must be removable.
529
+ * The renderer's structural shells and the hidden spacer used to preserve a
530
+ * moved object's original layout slot are not user content.
531
+ */
532
+ export function isDeletableSlideElement(element: HTMLElement): boolean {
533
+ return (
534
+ !element.classList.contains("fmd-layout-spacer") &&
535
+ !element.classList.contains("fmd-slide") &&
536
+ !element.classList.contains("fmd-autofit-scale") &&
537
+ !element.hasAttribute("data-fmd-autofit-content") &&
538
+ !element.hasAttribute("data-slide-canvas")
539
+ );
540
+ }
541
+
524
542
  /**
525
543
  * Whether Delete should remove `element` even though it is not a freeform
526
- * canvas object.
544
+ * canvas object or generic flow element.
527
545
  *
528
- * Flow-layout nodes are deliberately excluded from object operations, because
529
- * deleting an arbitrary one collapses the layout around it. An image is the
530
- * exception: it is a leaf, users select it directly, and removing it leaves
531
- * the surrounding grid or card intact. Without this, selecting a picture in a
532
- * card grid and pressing Delete silently did nothing.
546
+ * Images are handled specially because they are leaves: an image nested in a
547
+ * card should be removed without swallowing the surrounding card. Generic
548
+ * flow elements are covered by `isDeletableSlideElement`.
533
549
  */
534
550
  export function isDeletableFlowImage(element: HTMLElement): boolean {
535
551
  return (
@@ -1,5 +1,7 @@
1
1
  /** Shared style clipboard for copy/paste style (Cmd+Option+C / Cmd+Option+V) */
2
2
 
3
+ import type { SlideStylePatch, SlideStyleSnapshot } from "./slide-style";
4
+
3
5
  export interface CopiedStyle {
4
6
  color?: string;
5
7
  bold?: boolean;
@@ -13,6 +15,65 @@ export function setCopiedStyle(s: CopiedStyle | null): void {
13
15
  copiedStyle = s;
14
16
  }
15
17
 
18
+ /** Appearance-only style payload used by the canvas editor. */
19
+ export type CopiedElementStyle = Pick<
20
+ SlideStylePatch,
21
+ | "color"
22
+ | "backgroundColor"
23
+ | "fontSize"
24
+ | "fontWeight"
25
+ | "fontStyle"
26
+ | "textDecoration"
27
+ | "lineHeight"
28
+ | "textAlign"
29
+ | "opacity"
30
+ | "borderRadius"
31
+ | "borderWidth"
32
+ | "borderColor"
33
+ | "paddingLeft"
34
+ | "paddingRight"
35
+ | "paddingTop"
36
+ | "paddingBottom"
37
+ >;
38
+
39
+ let copiedElementStyle: CopiedElementStyle | null = null;
40
+
41
+ function px(value: number): string {
42
+ return `${Number.isInteger(value) ? value : Number(value.toFixed(2))}px`;
43
+ }
44
+
45
+ /** Converts the inspector's computed snapshot into an appearance-only patch. */
46
+ export function copiedElementStyleFromSnapshot(
47
+ snapshot: SlideStyleSnapshot,
48
+ ): CopiedElementStyle {
49
+ return {
50
+ color: snapshot.color,
51
+ backgroundColor: snapshot.backgroundColor,
52
+ fontSize: px(snapshot.fontSize),
53
+ fontWeight: snapshot.fontWeight,
54
+ fontStyle: snapshot.fontStyle,
55
+ textDecoration: snapshot.textDecoration,
56
+ lineHeight: String(snapshot.lineHeight),
57
+ textAlign: snapshot.textAlign,
58
+ opacity: String(Math.max(0, Math.min(100, snapshot.opacity)) / 100),
59
+ borderRadius: px(snapshot.borderRadius),
60
+ borderWidth: px(snapshot.borderWidth),
61
+ borderColor: snapshot.borderColor,
62
+ paddingLeft: px(snapshot.paddingX),
63
+ paddingRight: px(snapshot.paddingX),
64
+ paddingTop: px(snapshot.paddingY),
65
+ paddingBottom: px(snapshot.paddingY),
66
+ };
67
+ }
68
+
69
+ export function getCopiedElementStyle(): CopiedElementStyle | null {
70
+ return copiedElementStyle ? { ...copiedElementStyle } : null;
71
+ }
72
+
73
+ export function setCopiedElementStyle(style: CopiedElementStyle | null): void {
74
+ copiedElementStyle = style ? { ...style } : null;
75
+ }
76
+
16
77
  // Brand palette — persisted in localStorage
17
78
  const STORAGE_KEY = "slide-brand-palette";
18
79
 
@@ -9,6 +9,12 @@ import { useLocation } from "react-router";
9
9
 
10
10
  import { useDecks } from "@/context/DeckContext";
11
11
  import { useSidebarCollapsed } from "@/hooks/use-sidebar-collapsed";
12
+ import {
13
+ hasCurrentSlideSelection,
14
+ readPublishedSlidesSelection,
15
+ SLIDES_SELECTION_CHANGED_EVENT,
16
+ type SlidesAgentSelection,
17
+ } from "@/lib/slide-agent-context";
12
18
  import { TAB_ID } from "@/lib/tab-id";
13
19
  import { cn } from "@/lib/utils";
14
20
 
@@ -45,6 +51,8 @@ export function Layout({ children }: LayoutProps) {
45
51
  const location = useLocation();
46
52
  const t = useT();
47
53
  const [sidebarOpen, setSidebarOpen] = useState(false);
54
+ const [slidesSelection, setSlidesSelection] =
55
+ useState<SlidesAgentSelection | null>(() => readPublishedSlidesSelection());
48
56
  const [editorSidebarOverride, setEditorSidebarOverride] =
49
57
  useState<EditorSidebarOverride | null>(null);
50
58
  const { collapsed: sidebarCollapsed, setCollapsed: setSidebarCollapsed } =
@@ -53,6 +61,20 @@ export function Layout({ children }: LayoutProps) {
53
61
  const isEmptyDecksState =
54
62
  location.pathname === "/" && !decksLoading && decks.length === 0;
55
63
 
64
+ useEffect(() => {
65
+ const onSelectionChanged = (event: Event) => {
66
+ setSlidesSelection(
67
+ (event as CustomEvent<SlidesAgentSelection | null>).detail ?? null,
68
+ );
69
+ };
70
+ window.addEventListener(SLIDES_SELECTION_CHANGED_EVENT, onSelectionChanged);
71
+ return () =>
72
+ window.removeEventListener(
73
+ SLIDES_SELECTION_CHANGED_EVENT,
74
+ onSelectionChanged,
75
+ );
76
+ }, []);
77
+
56
78
  // Scope new chats to the deck the user is currently editing. The route
57
79
  // is `/deck/:id`; everywhere else (list, presentation) leaves
58
80
  // scope null so chats stay in the general pool. Keep the visible label
@@ -61,12 +83,14 @@ export function Layout({ children }: LayoutProps) {
61
83
  const match = location.pathname.match(/^\/deck\/([^/]+)/);
62
84
  const deckId = match?.[1];
63
85
  if (!deckId) return null;
86
+ const hasSelection = hasCurrentSlideSelection(slidesSelection, deckId);
64
87
  return {
65
88
  type: "deck" as const,
66
89
  id: deckId,
67
- label: t("agent.thisDeck"),
90
+ label: t(hasSelection ? "agent.currentSelection" : "agent.thisSlide"),
91
+ contextKey: "slides-current-context",
68
92
  };
69
- }, [location.pathname, t]);
93
+ }, [location.pathname, slidesSelection, t]);
70
94
 
71
95
  useEffect(() => {
72
96
  setSidebarOpen(false);
@@ -0,0 +1 @@
1
+ export * from "@agent-native/toolkit/ui/context-menu";
@@ -614,13 +614,12 @@ type PatchDeckFields = Extract<
614
614
  export function applyOpToDeck(deck: Deck, op: PatchDeckOp): Deck {
615
615
  switch (op.op) {
616
616
  case "patch-slide": {
617
- let changed = false;
617
+ const prior = deck.slides.find((s) => s.id === op.slideId);
618
+ if (!prior || !hasChangedFields(prior, op.fields)) return deck;
618
619
  const slides = deck.slides.map((s) => {
619
620
  if (s.id !== op.slideId) return s;
620
- changed = true;
621
621
  return { ...s, ...op.fields };
622
622
  });
623
- if (!changed) return deck; // slide concurrently deleted — skip
624
623
  return { ...deck, slides, updatedAt: new Date().toISOString() };
625
624
  }
626
625
  case "delete-slide": {
@@ -646,6 +645,12 @@ export function applyOpToDeck(deck: Deck, op: PatchDeckOp): Deck {
646
645
  for (const s of deck.slides) {
647
646
  if (!named.has(s.id)) reordered.push(s);
648
647
  }
648
+ if (
649
+ reordered.length === deck.slides.length &&
650
+ reordered.every((slide, index) => slide.id === deck.slides[index]?.id)
651
+ ) {
652
+ return deck;
653
+ }
649
654
  return {
650
655
  ...deck,
651
656
  slides: reordered,
@@ -672,6 +677,7 @@ export function applyOpToDeck(deck: Deck, op: PatchDeckOp): Deck {
672
677
  return { ...deck, slides, updatedAt: new Date().toISOString() };
673
678
  }
674
679
  case "patch-deck-fields": {
680
+ if (!hasChangedFields(deck, op.fields)) return deck;
675
681
  return {
676
682
  ...deck,
677
683
  ...op.fields,
@@ -681,6 +687,27 @@ export function applyOpToDeck(deck: Deck, op: PatchDeckOp): Deck {
681
687
  }
682
688
  }
683
689
 
690
+ function equalDeckValue(left: unknown, right: unknown): boolean {
691
+ if (Object.is(left, right)) return true;
692
+ if (
693
+ left === null ||
694
+ right === null ||
695
+ typeof left !== "object" ||
696
+ typeof right !== "object"
697
+ ) {
698
+ return false;
699
+ }
700
+ return JSON.stringify(left) === JSON.stringify(right);
701
+ }
702
+
703
+ function hasChangedFields(current: object, fields: object): boolean {
704
+ const currentRecord = current as Record<string, unknown>;
705
+ const fieldRecord = fields as Record<string, unknown>;
706
+ return Object.keys(fieldRecord).some(
707
+ (key) => !equalDeckValue(currentRecord[key], fieldRecord[key]),
708
+ );
709
+ }
710
+
684
711
  export function applyUndoOpToDecks(decks: Deck[], op: DeckUndoOp): Deck[] {
685
712
  switch (op.op) {
686
713
  case "delete-deck":
@@ -751,8 +778,11 @@ export function deriveInverseOp(
751
778
  // Capture the prior value for every field this op touches, so undo
752
779
  // restores exactly what changed (including clearing fields back to
753
780
  // undefined).
754
- (priorFields as Record<string, unknown>)[key] = prior[key];
781
+ if (!equalDeckValue(prior[key], op.fields[key])) {
782
+ (priorFields as Record<string, unknown>)[key] = prior[key];
783
+ }
755
784
  }
785
+ if (Object.keys(priorFields).length === 0) return null;
756
786
  return [{ op: "patch-slide", slideId: op.slideId, fields: priorFields }];
757
787
  }
758
788
  case "delete-slide": {
@@ -786,6 +816,7 @@ export function deriveInverseOp(
786
816
  }
787
817
  case "add-slide": {
788
818
  // Inverse of adding a slide is deleting it.
819
+ if (before.slides.some((slide) => slide.id === op.slideId)) return null;
789
820
  return [
790
821
  {
791
822
  op: "delete-slide",
@@ -796,6 +827,7 @@ export function deriveInverseOp(
796
827
  }
797
828
  case "reorder-slides": {
798
829
  // Inverse reorder = the order the slides were in before.
830
+ if (applyOpToDeck(before, op) === before) return null;
799
831
  return [
800
832
  { op: "reorder-slides", orderedIds: before.slides.map((s) => s.id) },
801
833
  ];
@@ -803,9 +835,13 @@ export function deriveInverseOp(
803
835
  case "patch-deck-fields": {
804
836
  const priorFields: Record<string, unknown> = {};
805
837
  const beforeRecord = before as unknown as Record<string, unknown>;
838
+ const nextRecord = op.fields as Record<string, unknown>;
806
839
  for (const key of Object.keys(op.fields)) {
807
- priorFields[key] = beforeRecord[key];
840
+ if (!equalDeckValue(beforeRecord[key], nextRecord[key])) {
841
+ priorFields[key] = beforeRecord[key];
842
+ }
808
843
  }
844
+ if (Object.keys(priorFields).length === 0) return null;
809
845
  return [
810
846
  {
811
847
  op: "patch-deck-fields",
@@ -1734,7 +1770,8 @@ export function DeckProvider({ children }: { children: ReactNode }) {
1734
1770
  target.tagName === "TEXTAREA" ||
1735
1771
  target.tagName === "INPUT" ||
1736
1772
  target.isContentEditable;
1737
- if ((e.metaKey || e.ctrlKey) && e.key === "z") {
1773
+ const key = e.key.toLowerCase();
1774
+ if ((e.metaKey || e.ctrlKey) && key === "z") {
1738
1775
  if (isTyping) return;
1739
1776
  e.preventDefault();
1740
1777
  if (e.shiftKey) {
@@ -1743,7 +1780,7 @@ export function DeckProvider({ children }: { children: ReactNode }) {
1743
1780
  undo();
1744
1781
  }
1745
1782
  }
1746
- if ((e.metaKey || e.ctrlKey) && e.key === "y") {
1783
+ if ((e.metaKey || e.ctrlKey) && key === "y") {
1747
1784
  if (isTyping) return;
1748
1785
  e.preventDefault();
1749
1786
  redo();
@@ -1982,17 +2019,7 @@ export function DeckProvider({ children }: { children: ReactNode }) {
1982
2019
 
1983
2020
  const updateDeck = useCallback(
1984
2021
  (id: string, updates: Partial<Omit<Deck, "id" | "createdAt">>) => {
1985
- // Clear the external-update suppression window so a rename/update that
1986
- // happens within 2s of page load (or an SSE event) is not silently dropped.
1987
- markDeckDirty(id);
1988
2022
  const before = decksRef.current.find((d) => d.id === id);
1989
- setDecks((prev) =>
1990
- prev.map((d) =>
1991
- d.id === id
1992
- ? { ...d, ...updates, updatedAt: new Date().toISOString() }
1993
- : d,
1994
- ),
1995
- );
1996
2023
  // Enqueue a granular patch-deck-fields op — only the changed fields are
1997
2024
  // sent to the server, so concurrent edits to slides are never clobbered.
1998
2025
  // Exclude internal/derived fields that live only in client state.
@@ -2007,11 +2034,26 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2007
2034
  };
2008
2035
  void _slides;
2009
2036
  _ca;
2010
- if (Object.keys(persistableUpdates).length > 0) {
2011
- const op: PatchDeckOp = {
2012
- op: "patch-deck-fields",
2013
- fields: persistableUpdates as PatchDeckFields,
2014
- };
2037
+ const hasPersistableUpdates = Object.keys(persistableUpdates).length > 0;
2038
+ const op: PatchDeckOp | null = hasPersistableUpdates
2039
+ ? {
2040
+ op: "patch-deck-fields",
2041
+ fields: persistableUpdates as PatchDeckFields,
2042
+ }
2043
+ : null;
2044
+ if (before && op && !deriveInverseOp(before, op)) return;
2045
+
2046
+ // Clear the external-update suppression window so a rename/update that
2047
+ // happens within 2s of page load (or an SSE event) is not silently dropped.
2048
+ markDeckDirty(id);
2049
+ setDecks((prev) =>
2050
+ prev.map((d) =>
2051
+ d.id === id
2052
+ ? { ...d, ...updates, updatedAt: new Date().toISOString() }
2053
+ : d,
2054
+ ),
2055
+ );
2056
+ if (op) {
2015
2057
  enqueueDeckOp(id, op);
2016
2058
  if (before) {
2017
2059
  // Coalesce rapid deck-field edits (e.g. title typing, tweak sliders)
@@ -2087,7 +2129,6 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2087
2129
  updates: Partial<Omit<Slide, "id">>,
2088
2130
  options?: UpdateSlideOptions,
2089
2131
  ) => {
2090
- markDeckDirty(deckId);
2091
2132
  const label = updates.layout
2092
2133
  ? "Change layout"
2093
2134
  : updates.background
@@ -2096,6 +2137,9 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2096
2137
  ? "Update content"
2097
2138
  : "Edit slide";
2098
2139
  const before = decksRef.current.find((d) => d.id === deckId);
2140
+ const op: PatchDeckOp = { op: "patch-slide", slideId, fields: updates };
2141
+ if (before && !deriveInverseOp(before, op)) return;
2142
+ markDeckDirty(deckId);
2099
2143
  setDecksLocal((prev: Deck[]) =>
2100
2144
  prev.map((d) => {
2101
2145
  if (d.id !== deckId) return d;
@@ -2109,7 +2153,6 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2109
2153
  }),
2110
2154
  );
2111
2155
  // Granular op — only this slide's changed fields reach the server.
2112
- const op: PatchDeckOp = { op: "patch-slide", slideId, fields: updates };
2113
2156
  enqueueDeckOp(deckId, op, options);
2114
2157
  if (before) {
2115
2158
  // Coalesce a burst of edits to the SAME slide's SAME field-set into one
@@ -2223,11 +2266,18 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2223
2266
 
2224
2267
  const setDeckSlides = useCallback(
2225
2268
  (deckId: string, slides: Slide[]) => {
2226
- markDeckDirty(deckId);
2227
2269
  const before = decksRef.current.find((deck) => deck.id === deckId);
2228
2270
  const after = before
2229
2271
  ? { ...before, slides, updatedAt: new Date().toISOString() }
2230
2272
  : null;
2273
+ if (
2274
+ before &&
2275
+ after &&
2276
+ deckContentSignature(before) === deckContentSignature(after)
2277
+ ) {
2278
+ return;
2279
+ }
2280
+ markDeckDirty(deckId);
2231
2281
  // setDeckSlides replaces ALL slides wholesale (used by AI generation and
2232
2282
  // imports), so its undo entry is a deck-level full replacement instead of
2233
2283
  // a fine-grained slide patch.
@@ -257,7 +257,7 @@ button[title="Open agent sidebar"] {
257
257
 
258
258
  .deck-editor-context-toolbar-host {
259
259
  min-width: 0;
260
- overflow: hidden;
260
+ overflow: auto;
261
261
  }
262
262
 
263
263
  .deck-editor-context-toolbar-host--wide {
@@ -48,7 +48,8 @@ const messages = {
48
48
  },
49
49
  agent: {
50
50
  emptyState: "Ask me anything about your presentations",
51
- thisDeck: "This Deck",
51
+ thisSlide: "This Slide",
52
+ currentSelection: "Current Selection",
52
53
  suggestionPitch: "Build a 10-slide pitch from this doc",
53
54
  suggestionBrand: "Apply our brand to this deck",
54
55
  suggestionHero: "Generate a hero image for this slide",
@@ -436,6 +437,10 @@ const messages = {
436
437
  order: "Order",
437
438
  bringToFront: "Bring to front",
438
439
  sendToBack: "Send to back",
440
+ copyStyle: "Copy style",
441
+ pasteStyle: "Paste style",
442
+ copy: "Copy",
443
+ paste: "Paste",
439
444
  },
440
445
  animations: {
441
446
  appear: "Appear",
@@ -373,7 +373,7 @@ export async function startDeckGeneration({
373
373
  : [
374
374
  "This is a new deck. Keep it empty until generation begins; attached reference files must not seed it with imported slides.",
375
375
  "Start a `manage-progress` run so progress appears in the app header. Add the first slide as soon as it is ready, then continue one slide at a time so the editor visibly fills in.",
376
- `After reading any requested or attached reference material, but before adding the first slide, choose a concise, specific deck title from the user's request and source material. Never use the deck id, run id, file id, or another opaque alphanumeric token as the title. Call \`patch-deck\` with \`deckId: \"${deckId}\"\` and \`operations: [{ \"op\": \"patch-deck-fields\", \"fields\": { \"title\": \"<generated title>\" } }]\`. Include only \`title\` in \`fields\`; omit all other optional fields. Never leave a generated deck named \"Untitled Deck\" or another placeholder.`,
376
+ `After reading any requested or attached reference material, but before adding the first slide, choose a concise, specific deck title from the user's request and source material. Never use the deck id, run id, file id, uploaded filename, or another opaque alphanumeric token as the title. Do not reuse a generic placeholder like "Untitled scene" when the content or reference context gives you a better title. Call \`patch-deck\` with \`deckId: \"${deckId}\"\` and \`operations: [{ \"op\": \"patch-deck-fields\", \"fields\": { \"title\": \"<generated title>\" } }]\`. Include only \`title\` in \`fields\`; omit all other optional fields. Never leave a generated deck named \"Untitled Deck\" or another placeholder.`,
377
377
  "If the user asks for a standalone visual, diagram, hero, one-pager, poster, or a couple of visuals, create only the requested one/few polished visual slides. Do not pad the result into a full presentation.",
378
378
  "If the request is for a presentation or deck and does not explicitly ask for one slide, infer a coherent multi-slide outline from the scope and keep adding slides until that outline is complete. Do not stop after the first slide just because the prompt has few explicit instructions.",
379
379
  `Add slides ONE AT A TIME using the \`add-slide\` action with --deckId=${deckId}. Wait for each \`add-slide\` result before calling it again; do not batch or parallelize slide writes.`,
@@ -20,11 +20,13 @@ export type ImageDropAgentPayload =
20
20
  | {
21
21
  kind: "hosted";
22
22
  message: string;
23
+ context: string;
23
24
  referenceImagePaths: string[];
24
25
  }
25
26
  | {
26
27
  kind: "inline";
27
28
  message: string;
29
+ context: string;
28
30
  images: string[];
29
31
  };
30
32
 
@@ -52,21 +54,18 @@ export function buildImageDropAgentPayload(args: {
52
54
  args.intent.trim().length > 0
53
55
  ? args.intent.trim()
54
56
  : "Use this image on the current slide.";
55
- const lines = [intentLine];
57
+ const contextLines: string[] = [];
56
58
  if (args.contextHint && args.contextHint.trim().length > 0) {
57
- lines.push(args.contextHint.trim());
59
+ contextLines.push(args.contextHint.trim());
58
60
  }
59
- lines.push(`Filename: ${args.filename}`);
61
+ contextLines.push(`Filename: ${args.filename}`);
60
62
 
61
63
  if (args.upload.ok && args.upload.url) {
62
- lines.splice(
63
- lines.length - 1,
64
- 0,
65
- `Image URL (already uploaded): ${args.upload.url}`,
66
- );
64
+ contextLines.push(`Image URL (already uploaded): ${args.upload.url}`);
67
65
  return {
68
66
  kind: "hosted",
69
- message: lines.join("\n\n"),
67
+ message: intentLine,
68
+ context: contextLines.join("\n\n"),
70
69
  referenceImagePaths: [args.upload.url],
71
70
  };
72
71
  }
@@ -81,18 +80,19 @@ export function buildImageDropAgentPayload(args: {
81
80
  if (!isMissingUploadProviderError(args.upload.status, args.upload.error)) {
82
81
  // Unexpected upload failure — still try the inline path so the user can
83
82
  // keep working, but tell the agent the hosted upload didn't land.
84
- lines.push(
83
+ contextLines.push(
85
84
  "Hosted upload failed; the image is attached inline as a data URL. Call upload-image on it before placing it on the slide if a durable URL is required.",
86
85
  );
87
86
  } else {
88
- lines.push(
87
+ contextLines.push(
89
88
  "No file upload provider is configured, so the image is attached inline as a data URL. Call upload-image to obtain a hosted URL before inserting it into slide HTML.",
90
89
  );
91
90
  }
92
91
 
93
92
  return {
94
93
  kind: "inline",
95
- message: lines.join("\n\n"),
94
+ message: intentLine,
95
+ context: contextLines.join("\n\n"),
96
96
  images: [args.dataUrl],
97
97
  };
98
98
  }
@@ -0,0 +1,52 @@
1
+ export const SLIDES_SELECTION_CHANGED_EVENT = "slides:selection-changed";
2
+
3
+ export interface SlidesAgentSelection {
4
+ deckId?: string;
5
+ slideId?: string;
6
+ slideIndex?: number;
7
+ slideNumber?: number;
8
+ items?: readonly unknown[];
9
+ }
10
+
11
+ interface SlidesSelectionWindow extends Window {
12
+ __slidesAgentSelection?: SlidesAgentSelection | null;
13
+ }
14
+
15
+ function getSlidesWindow(): SlidesSelectionWindow | null {
16
+ return typeof window === "undefined"
17
+ ? null
18
+ : (window as SlidesSelectionWindow);
19
+ }
20
+
21
+ /**
22
+ * Keep the visible scope chip responsive without making the browser event the
23
+ * source of truth for agent context. The app-state write remains canonical.
24
+ */
25
+ export function publishSlidesSelection(
26
+ selection: SlidesAgentSelection | null,
27
+ ): void {
28
+ const slidesWindow = getSlidesWindow();
29
+ if (!slidesWindow) return;
30
+ slidesWindow.__slidesAgentSelection = selection;
31
+ slidesWindow.dispatchEvent(
32
+ new CustomEvent<SlidesAgentSelection | null>(
33
+ SLIDES_SELECTION_CHANGED_EVENT,
34
+ { detail: selection },
35
+ ),
36
+ );
37
+ }
38
+
39
+ export function readPublishedSlidesSelection(): SlidesAgentSelection | null {
40
+ return getSlidesWindow()?.__slidesAgentSelection ?? null;
41
+ }
42
+
43
+ export function hasCurrentSlideSelection(
44
+ selection: SlidesAgentSelection | null,
45
+ deckId: string,
46
+ ): boolean {
47
+ return (
48
+ selection?.deckId === deckId &&
49
+ Array.isArray(selection.items) &&
50
+ selection.items.length > 0
51
+ );
52
+ }