@agent-native/core 0.149.0 → 0.149.2
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/analytics/.agents/skills/dashboard-management/SKILL.md +2 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +150 -46
- package/corpus/templates/analytics/app/global.css +1 -1
- package/corpus/templates/analytics/app/i18n-data.ts +37 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
- package/corpus/templates/analytics/changelog/2026-08-10-dashboard-tables-can-be-copied-and-sorted-by-multiple-column.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/assets/app/global.css +1 -1
- package/corpus/templates/assets/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/brain/app/global.css +1 -1
- package/corpus/templates/brain/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-10-clips-desktop-sign-in-now-starts-with-magic-links-and-google.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.css +1 -0
- package/corpus/templates/clips/desktop/src/app.tsx +274 -137
- package/corpus/templates/clips/desktop/src/styles.css +35 -0
- package/corpus/templates/content/app/global.css +1 -0
- package/corpus/templates/crm/app/global.css +1 -1
- package/corpus/templates/crm/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/forms/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/plan/app/global.css +1 -0
- package/corpus/templates/plan/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/plan/server/ui-plan-html.ts +4 -1
- package/corpus/templates/slides/AGENTS.md +11 -10
- package/corpus/templates/slides/actions/add-deck.ts +2 -0
- package/corpus/templates/slides/actions/add-slide.ts +15 -1
- package/corpus/templates/slides/actions/create-deck.ts +18 -6
- package/corpus/templates/slides/actions/get-deck.ts +29 -1
- package/corpus/templates/slides/actions/patch-deck.ts +139 -10
- package/corpus/templates/slides/actions/save-deck.ts +24 -1
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +46 -0
- package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +194 -3
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +10 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +74 -63
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -7
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
- package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
- package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
- package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
- package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
- package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
- package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
- package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +16 -27
- package/corpus/templates/slides/app/pages/Index.tsx +256 -98
- package/corpus/templates/slides/changelog/2026-08-08-google-slides-export-now-tells-you-why-a-deck-fell-back-to-a.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-delete-key-now-removes-a-selected-image.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-editing-a-slide-in-an-imported-deck-no-longer-fails-to-save.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-fixed-pdf-and-custom-size-powerpoint-imports-rendering-with-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-images-from-sites-without-cors-no-longer-export-blank.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-imported-pdf-headings-no-longer-lose-the-space-between-color.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-imported-slide-text-no-longer-renders-too-large-for-its-box.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-large-decks-no-longer-render-slowly-or-glitch-in-the-editor.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-revise-highlighted-slide-text-with-ai.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-the-deck-editor-no-longer-stays-dimmed-after-the-window-narro.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-ai-editing-avatar-badge.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -0
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +115 -22
- package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +11 -2
- package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
- package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
- package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
- package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
- package/corpus/templates/slides/shared/api.ts +2 -1
- package/corpus/templates/slides/shared/deck-title.ts +135 -0
- package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
- package/dist/agent/engine/ai-sdk-engine.js +1 -1
- package/dist/agent/production-agent.d.ts +21 -1
- package/dist/agent/production-agent.js +129 -10
- package/dist/agent/tool-approval-store.d.ts +18 -0
- package/dist/agent/tool-approval-store.js +122 -0
- package/dist/agent/types.d.ts +1 -1
- package/dist/cli/code-agent-executor.js +11 -2
- package/dist/client/AgentPanel.js +31 -6
- package/dist/client/AssistantChat.d.ts +3 -2
- package/dist/client/AssistantChat.js +7 -20
- package/dist/client/agent-chat/index.d.ts +1 -1
- package/dist/client/agent-chat-adapter.js +11 -0
- package/dist/client/agent-chat.d.ts +1 -1
- package/dist/client/chat/markdown-renderer.js +14 -2
- package/dist/client/chat/message-components.d.ts +6 -2
- package/dist/client/chat/message-components.js +25 -47
- package/dist/client/chat/tool-call-display.d.ts +3 -5
- package/dist/client/chat/tool-call-display.js +7 -212
- package/dist/client/chat-first/apps-rail.js +3 -3
- package/dist/client/chat-first/browser-pane.js +4 -1
- package/dist/client/chat-first/chat-history.d.ts +3 -1
- package/dist/client/chat-first/chat-history.js +2 -2
- package/dist/client/chat-first/surface-tabs.d.ts +1 -1
- package/dist/client/chat-first/surface-tabs.js +52 -41
- package/dist/client/chat-first/types.d.ts +3 -0
- package/dist/client/chat-first.d.ts +10 -0
- package/dist/client/chat-first.js +37 -2
- package/dist/client/components/ui/message-scroller.js +4 -1
- package/dist/client/i18n.js +3 -1
- package/dist/client/progress/RunsTray.js +14 -1
- package/dist/client/resources/McpConnectionSuggestion.js +9 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/use-chat-models.js +156 -80
- package/dist/integrations/webhook-handler.js +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/auth.js +262 -31
- package/dist/server/onboarding-html.js +56 -9
- package/dist/shared/reasoning-effort.d.ts +8 -8
- package/dist/shared/reasoning-effort.js +10 -10
- package/dist/shared/streaming-text-smoothing.js +41 -11
- package/dist/styles/agent-native.css +19 -32
- package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/docs/content/template-chat.mdx +1 -1
- package/package.json +2 -2
- package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
|
@@ -12,11 +12,10 @@ Before building common workspace or agent UI, read `agent-native-toolkit` and
|
|
|
12
12
|
|
|
13
13
|
## Core Rules
|
|
14
14
|
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- Never hardcode API keys, tokens, webhook URLs, signing secrets, private Builder/internal data, customer data, or credential-looking literals. Use secrets/OAuth/runtime configuration and obvious placeholders in examples.
|
|
15
|
+
- Keep large files/blobs in configured file storage, not SQL, settings, or
|
|
16
|
+
resources; persist only URLs, ids, or handles.
|
|
17
|
+
- Never hardcode secrets or private/customer data; use vault/OAuth/runtime
|
|
18
|
+
configuration and fake placeholders in examples.
|
|
20
19
|
- Use actions for deck lifecycle, slide edits, imports, exports, images, design
|
|
21
20
|
systems, and sharing. Do not write deck/slide rows directly.
|
|
22
21
|
- In dev, call actions with `pnpm action <name>`; in production, use native
|
|
@@ -25,11 +24,10 @@ Before building common workspace or agent UI, read `agent-native-toolkit` and
|
|
|
25
24
|
current layout is unclear.
|
|
26
25
|
- Preserve deck structure and visual consistency. Prefer focused slide edits over
|
|
27
26
|
regenerating whole decks unless requested.
|
|
28
|
-
- When an uploaded PPTX or PDF
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
use `update-slide` for bounded visual edits and verify with `get-deck`.
|
|
27
|
+
- When improving an uploaded PPTX or PDF, import it into the target deck first.
|
|
28
|
+
Treat `sourceImport` as the source-of-truth contract: preserve slide count,
|
|
29
|
+
order, IDs, copy, notes, images, and positioned objects; verify with
|
|
30
|
+
`get-deck`.
|
|
33
31
|
- A source import with `fidelity: partial` or `imagesSkipped` is not safe to
|
|
34
32
|
restyle automatically. Report the exact warning rather than silently
|
|
35
33
|
replacing missing content.
|
|
@@ -46,6 +44,9 @@ Before building common workspace or agent UI, read `agent-native-toolkit` and
|
|
|
46
44
|
pasted URLs may need a one-time Google reconnect for Drive export. Preserve
|
|
47
45
|
imported PPTX timing metadata, including by-paragraph reveals, on slide
|
|
48
46
|
records.
|
|
47
|
+
- For per-click reveals, use ordered 0-based animation targets and patch the
|
|
48
|
+
complete animation list with content; stale or missing targets disable
|
|
49
|
+
reveals.
|
|
49
50
|
- For images, use `generate-image-api` with provenance; show results as
|
|
50
51
|
``.
|
|
51
52
|
- Ask a sibling app's agent with a natural-language `call-agent` message by
|
|
@@ -16,6 +16,7 @@ import { z } from "zod";
|
|
|
16
16
|
|
|
17
17
|
import { getDb, schema } from "../server/db/index.js";
|
|
18
18
|
import { notifyClients } from "../server/handlers/decks.js";
|
|
19
|
+
import { assertHumanReadableDeckTitle } from "../shared/deck-title.js";
|
|
19
20
|
import {
|
|
20
21
|
assertDesignSystemReadable,
|
|
21
22
|
assertValidAspectRatio,
|
|
@@ -50,6 +51,7 @@ export default defineAction({
|
|
|
50
51
|
const now = new Date().toISOString();
|
|
51
52
|
deck.createdAt = deck.createdAt || now;
|
|
52
53
|
deck.updatedAt = now;
|
|
54
|
+
assertHumanReadableDeckTitle(deckTitle(deck));
|
|
53
55
|
|
|
54
56
|
const designSystemId = deckDesignSystemId(deck);
|
|
55
57
|
await assertDesignSystemReadable(designSystemId);
|
|
@@ -17,6 +17,7 @@ import { normalizeSlidePadding } from "../app/lib/normalize-slide-padding.js";
|
|
|
17
17
|
import { getDb, schema } from "../server/db/index.js";
|
|
18
18
|
import { notifyClients } from "../server/handlers/decks.js";
|
|
19
19
|
import { createDeckVersionSnapshot } from "../server/lib/deck-versions.js";
|
|
20
|
+
import { repairGeneratedDeckTitle } from "../shared/deck-title.js";
|
|
20
21
|
import { hashSlideContent } from "../shared/slide-fit.js";
|
|
21
22
|
import { slideLabelFor, touchAgentSlidePresence } from "./_agent-presence.js";
|
|
22
23
|
import {
|
|
@@ -308,11 +309,20 @@ export default defineAction({
|
|
|
308
309
|
typeof position === "number"
|
|
309
310
|
? Math.max(0, Math.min(position, slides.length))
|
|
310
311
|
: slides.length;
|
|
312
|
+
const shouldRepairTitle = slides.length === 0;
|
|
311
313
|
slides.splice(insertIndex, 0, newSlide);
|
|
312
314
|
|
|
313
315
|
const now = new Date().toISOString();
|
|
314
316
|
deck.slides = slides;
|
|
315
317
|
deck.updatedAt = now;
|
|
318
|
+
const currentTitle =
|
|
319
|
+
typeof row.title === "string" && row.title.trim()
|
|
320
|
+
? row.title
|
|
321
|
+
: deck.title;
|
|
322
|
+
const repairedTitle = shouldRepairTitle
|
|
323
|
+
? repairGeneratedDeckTitle(currentTitle, newSlide.content)
|
|
324
|
+
: null;
|
|
325
|
+
if (repairedTitle) deck.title = repairedTitle;
|
|
316
326
|
deck.creativeContext =
|
|
317
327
|
contextMode === "off" && existingContext
|
|
318
328
|
? existingContext
|
|
@@ -334,7 +344,11 @@ export default defineAction({
|
|
|
334
344
|
await db.transaction(async (tx: any) => {
|
|
335
345
|
await tx
|
|
336
346
|
.update(schema.decks)
|
|
337
|
-
.set({
|
|
347
|
+
.set({
|
|
348
|
+
...(repairedTitle ? { title: repairedTitle } : {}),
|
|
349
|
+
data: JSON.stringify(deck),
|
|
350
|
+
updatedAt: now,
|
|
351
|
+
})
|
|
338
352
|
.where(eq(schema.decks.id, deckId));
|
|
339
353
|
await recordGenerationCreativeContext(
|
|
340
354
|
{
|
|
@@ -20,6 +20,10 @@ import { notifyClients } from "../server/handlers/decks.js";
|
|
|
20
20
|
import { createDeckVersionSnapshot } from "../server/lib/deck-versions.js";
|
|
21
21
|
import { resolveDefaultDesignSystemId } from "../server/workspace-defaults.js";
|
|
22
22
|
import { ASPECT_RATIO_VALUES } from "../shared/aspect-ratios.js";
|
|
23
|
+
import {
|
|
24
|
+
assertHumanReadableDeckTitle,
|
|
25
|
+
repairGeneratedDeckTitle,
|
|
26
|
+
} from "../shared/deck-title.js";
|
|
23
27
|
import { getDeckUrl } from "./_app-url.js";
|
|
24
28
|
|
|
25
29
|
const ReuseLabelSchema = z
|
|
@@ -216,6 +220,9 @@ export default defineAction({
|
|
|
216
220
|
...s,
|
|
217
221
|
content: normalizeSlidePadding(s.content),
|
|
218
222
|
}));
|
|
223
|
+
const firstSlideContent = slides[0]?.content;
|
|
224
|
+
const resolvedTitle =
|
|
225
|
+
repairGeneratedDeckTitle(title, firstSlideContent) ?? title;
|
|
219
226
|
|
|
220
227
|
if (deckId) {
|
|
221
228
|
if (designSystemId) {
|
|
@@ -231,6 +238,10 @@ export default defineAction({
|
|
|
231
238
|
if (!existing[0]) {
|
|
232
239
|
throw new Error(`Deck not found: ${deckId}`);
|
|
233
240
|
}
|
|
241
|
+
const existingDeckTitle =
|
|
242
|
+
repairGeneratedDeckTitle(title, firstSlideContent, existing[0].title) ??
|
|
243
|
+
resolvedTitle;
|
|
244
|
+
assertHumanReadableDeckTitle(existingDeckTitle);
|
|
234
245
|
await createDeckVersionSnapshot(
|
|
235
246
|
{
|
|
236
247
|
id: existing[0].id,
|
|
@@ -242,7 +253,7 @@ export default defineAction({
|
|
|
242
253
|
);
|
|
243
254
|
const prevData = existing[0] ? JSON.parse(existing[0].data) : {};
|
|
244
255
|
const data = {
|
|
245
|
-
title,
|
|
256
|
+
title: existingDeckTitle,
|
|
246
257
|
slides,
|
|
247
258
|
updatedAt: now,
|
|
248
259
|
aspectRatio: aspectRatio ?? prevData.aspectRatio,
|
|
@@ -252,7 +263,7 @@ export default defineAction({
|
|
|
252
263
|
await db
|
|
253
264
|
.update(schema.decks)
|
|
254
265
|
.set({
|
|
255
|
-
title,
|
|
266
|
+
title: existingDeckTitle,
|
|
256
267
|
data: JSON.stringify(data),
|
|
257
268
|
designSystemId: designSystemId ?? existing[0]?.designSystemId ?? null,
|
|
258
269
|
updatedAt: now,
|
|
@@ -271,7 +282,7 @@ export default defineAction({
|
|
|
271
282
|
});
|
|
272
283
|
return {
|
|
273
284
|
id: deckId,
|
|
274
|
-
title,
|
|
285
|
+
title: existingDeckTitle,
|
|
275
286
|
slideCount: slides.length,
|
|
276
287
|
url: getDeckUrl(deckId),
|
|
277
288
|
deepLink: deckDeepLink(deckId),
|
|
@@ -282,6 +293,7 @@ export default defineAction({
|
|
|
282
293
|
|
|
283
294
|
const ownerEmail = getRequestUserEmail();
|
|
284
295
|
if (!ownerEmail) throw new Error("no authenticated user");
|
|
296
|
+
assertHumanReadableDeckTitle(resolvedTitle);
|
|
285
297
|
|
|
286
298
|
let resolvedDesignSystemId = designSystemId;
|
|
287
299
|
if (resolvedDesignSystemId) {
|
|
@@ -293,7 +305,7 @@ export default defineAction({
|
|
|
293
305
|
|
|
294
306
|
const id = `deck-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
295
307
|
const data: Record<string, unknown> = {
|
|
296
|
-
title,
|
|
308
|
+
title: resolvedTitle,
|
|
297
309
|
slides,
|
|
298
310
|
createdAt: now,
|
|
299
311
|
updatedAt: now,
|
|
@@ -303,7 +315,7 @@ export default defineAction({
|
|
|
303
315
|
data.creativeContext = creativeContextProvenance;
|
|
304
316
|
await db.insert(schema.decks).values({
|
|
305
317
|
id,
|
|
306
|
-
title,
|
|
318
|
+
title: resolvedTitle,
|
|
307
319
|
data: JSON.stringify(data),
|
|
308
320
|
designSystemId: resolvedDesignSystemId ?? null,
|
|
309
321
|
ownerEmail,
|
|
@@ -323,7 +335,7 @@ export default defineAction({
|
|
|
323
335
|
});
|
|
324
336
|
return {
|
|
325
337
|
id,
|
|
326
|
-
title,
|
|
338
|
+
title: resolvedTitle,
|
|
327
339
|
slideCount: slides.length,
|
|
328
340
|
url: getDeckUrl(id),
|
|
329
341
|
deepLink: deckDeepLink(id),
|
|
@@ -16,6 +16,31 @@ function stripHtml(html: string): string {
|
|
|
16
16
|
.trim();
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function compactAnimationSummary(value: unknown) {
|
|
20
|
+
if (!Array.isArray(value)) return null;
|
|
21
|
+
return {
|
|
22
|
+
count: value.length,
|
|
23
|
+
steps: value.map((entry, index) => {
|
|
24
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
25
|
+
return { order: index + 1, valid: false };
|
|
26
|
+
}
|
|
27
|
+
const animation = entry as Record<string, unknown>;
|
|
28
|
+
return {
|
|
29
|
+
order: index + 1,
|
|
30
|
+
id: typeof animation.id === "string" ? animation.id : null,
|
|
31
|
+
elementIndex:
|
|
32
|
+
typeof animation.elementIndex === "number"
|
|
33
|
+
? animation.elementIndex
|
|
34
|
+
: null,
|
|
35
|
+
elementPath: Array.isArray(animation.elementPath)
|
|
36
|
+
? animation.elementPath
|
|
37
|
+
: null,
|
|
38
|
+
type: typeof animation.type === "string" ? animation.type : null,
|
|
39
|
+
};
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
19
44
|
function deckDeepLink(deckId: string): string {
|
|
20
45
|
return buildDeepLink({
|
|
21
46
|
app: "slides",
|
|
@@ -26,7 +51,8 @@ function deckDeepLink(deckId: string): string {
|
|
|
26
51
|
|
|
27
52
|
export default defineAction({
|
|
28
53
|
description:
|
|
29
|
-
"Get a specific deck with all slides. Returns full deck JSON including slide content. User-visible slide numbers are 1-based and match the UI: slide 1 is the first slide. Use slideId for edits.",
|
|
54
|
+
"Get a specific deck with all slides. Returns full deck JSON including slide content. User-visible slide numbers are 1-based and match the UI: slide 1 is the first slide. Use slideId for edits. After a mutation or for a large deck when only IDs, count, previews, and animation targets are needed, pass compact=true to avoid retransmitting all slide HTML.",
|
|
55
|
+
timeoutMs: 60_000,
|
|
30
56
|
schema: z.object({
|
|
31
57
|
id: z.string().optional().describe("Deck ID (required)"),
|
|
32
58
|
compact: z
|
|
@@ -89,6 +115,8 @@ export default defineAction({
|
|
|
89
115
|
zeroBasedIndex: i,
|
|
90
116
|
id: s.id,
|
|
91
117
|
layout: s.layout ?? null,
|
|
118
|
+
transition: s.transition ?? null,
|
|
119
|
+
animations: compactAnimationSummary(s.animations),
|
|
92
120
|
textPreview: stripHtml(s.content || "").slice(0, 120),
|
|
93
121
|
})),
|
|
94
122
|
};
|
|
@@ -32,6 +32,10 @@ import {
|
|
|
32
32
|
type SourceImportMetadata,
|
|
33
33
|
} from "../server/lib/source-import.js";
|
|
34
34
|
import { ASPECT_RATIO_VALUES } from "../shared/aspect-ratios.js";
|
|
35
|
+
import {
|
|
36
|
+
assertHumanReadableDeckTitle,
|
|
37
|
+
repairGeneratedDeckTitle,
|
|
38
|
+
} from "../shared/deck-title.js";
|
|
35
39
|
|
|
36
40
|
// ---------------------------------------------------------------------------
|
|
37
41
|
// Per-deck write lock — same pattern as add-slide.ts so all client and agent
|
|
@@ -66,6 +70,27 @@ export function withDeckLock<T>(
|
|
|
66
70
|
// Operation schemas
|
|
67
71
|
// ---------------------------------------------------------------------------
|
|
68
72
|
|
|
73
|
+
const SlideAnimationSchema = z.object({
|
|
74
|
+
id: z.string().min(1).describe("Stable ID for this ordered reveal step"),
|
|
75
|
+
elementIndex: z
|
|
76
|
+
.number()
|
|
77
|
+
.int()
|
|
78
|
+
.min(0)
|
|
79
|
+
.describe(
|
|
80
|
+
"0-based legacy child index. Keep it paired with elementPath for compatibility.",
|
|
81
|
+
),
|
|
82
|
+
elementPath: z
|
|
83
|
+
.array(z.number().int().min(0))
|
|
84
|
+
.min(1)
|
|
85
|
+
.optional()
|
|
86
|
+
.describe(
|
|
87
|
+
"Preferred 0-based child-index path from the outer .fmd-slide wrapper. Re-read final HTML after content edits.",
|
|
88
|
+
),
|
|
89
|
+
type: z
|
|
90
|
+
.enum(["appear", "fade", "slide-up", "zoom"])
|
|
91
|
+
.describe("Animation used when this step is revealed"),
|
|
92
|
+
});
|
|
93
|
+
|
|
69
94
|
const SlideFieldsSchema = z.object({
|
|
70
95
|
content: z.string().optional(),
|
|
71
96
|
notes: z.string().optional(),
|
|
@@ -75,8 +100,16 @@ const SlideFieldsSchema = z.object({
|
|
|
75
100
|
imageLoading: z.boolean().optional(),
|
|
76
101
|
imagePrompt: z.string().optional(),
|
|
77
102
|
excalidrawData: z.string().optional(),
|
|
78
|
-
transition: z
|
|
79
|
-
|
|
103
|
+
transition: z
|
|
104
|
+
.enum(["instant", "none", "fade", "slide", "zoom"])
|
|
105
|
+
.optional()
|
|
106
|
+
.describe("Transition used when entering this slide"),
|
|
107
|
+
animations: z
|
|
108
|
+
.array(SlideAnimationSchema)
|
|
109
|
+
.optional()
|
|
110
|
+
.describe(
|
|
111
|
+
"Complete ordered on-click reveal list. Include every intended target in order; unlisted elements remain visible. Use elementPath from the final HTML and 0-based indexes.",
|
|
112
|
+
),
|
|
80
113
|
});
|
|
81
114
|
|
|
82
115
|
/** Update fields on a single existing slide */
|
|
@@ -171,11 +204,51 @@ export function assertSourceImportOperationsPreserved(
|
|
|
171
204
|
);
|
|
172
205
|
}
|
|
173
206
|
|
|
207
|
+
/**
|
|
208
|
+
* A deck-wide source restyle must be atomic from the agent's point of view:
|
|
209
|
+
* accepting a partial batch makes an apparently successful run indistinguishable
|
|
210
|
+
* from a run that quietly left the tail of the imported deck untouched.
|
|
211
|
+
*/
|
|
212
|
+
export function assertSourceImportSlidesCovered(
|
|
213
|
+
metadata: SourceImportMetadata | null,
|
|
214
|
+
operations: Operation[],
|
|
215
|
+
requireAllSourceSlides: boolean,
|
|
216
|
+
): void {
|
|
217
|
+
if (!metadata || !requireAllSourceSlides) return;
|
|
218
|
+
|
|
219
|
+
const sourceSlideIds =
|
|
220
|
+
Array.isArray(metadata.slideIds) && metadata.slideIds.length > 0
|
|
221
|
+
? metadata.slideIds
|
|
222
|
+
: metadata.slides.map((slide) => slide.id);
|
|
223
|
+
const patchedContentSlideIds = new Set(
|
|
224
|
+
operations.flatMap((operation) =>
|
|
225
|
+
operation.op === "patch-slide" && operation.fields.content !== undefined
|
|
226
|
+
? [operation.slideId]
|
|
227
|
+
: [],
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
const missingSlideIds = sourceSlideIds.filter(
|
|
231
|
+
(slideId) => !patchedContentSlideIds.has(slideId),
|
|
232
|
+
);
|
|
233
|
+
if (missingSlideIds.length === 0) return;
|
|
234
|
+
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Deck-wide source restyle requires one content patch per imported slide. Missing ${missingSlideIds.length} slide(s): ${missingSlideIds.slice(0, 12).join(", ")}${missingSlideIds.length > 12 ? ", …" : ""}. Continue with every source slide ID in one patch-deck call before verifying with get-deck compact=true.`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
174
240
|
// The browser uses the full operation union above. Agents additionally use
|
|
175
241
|
// this action for one bounded, deck-wide layout repair: one patch-slide per
|
|
176
|
-
// slide in a single SQL transaction, followed by
|
|
242
|
+
// source slide in a single SQL transaction, followed by compact verification.
|
|
177
243
|
const AgentPatchDeckInputSchema = z.object({
|
|
178
244
|
deckId: z.string().describe("Deck ID"),
|
|
245
|
+
requireAllSourceSlides: z
|
|
246
|
+
.boolean()
|
|
247
|
+
.optional()
|
|
248
|
+
.default(false)
|
|
249
|
+
.describe(
|
|
250
|
+
"For a deck-wide source-import restyle, require one content patch for every imported slide before any write is committed.",
|
|
251
|
+
),
|
|
179
252
|
operations: z
|
|
180
253
|
.array(
|
|
181
254
|
z.union([
|
|
@@ -192,7 +265,7 @@ const AgentPatchDeckInputSchema = z.object({
|
|
|
192
265
|
)
|
|
193
266
|
.min(1)
|
|
194
267
|
.describe(
|
|
195
|
-
"
|
|
268
|
+
"For a deck-wide source restyle, include one patch-slide operation with content for every existing source slide. Use patch-deck-fields only for a deck title change.",
|
|
196
269
|
),
|
|
197
270
|
});
|
|
198
271
|
|
|
@@ -332,7 +405,19 @@ export function applyOperation(deck: any, op: Operation): void {
|
|
|
332
405
|
|
|
333
406
|
case "patch-deck-fields": {
|
|
334
407
|
const { fields } = op;
|
|
335
|
-
if (fields.title !== undefined)
|
|
408
|
+
if (fields.title !== undefined) {
|
|
409
|
+
const repairedTitle = repairGeneratedDeckTitle(
|
|
410
|
+
fields.title,
|
|
411
|
+
slides[0]?.content,
|
|
412
|
+
deck.title,
|
|
413
|
+
);
|
|
414
|
+
if (repairedTitle) {
|
|
415
|
+
deck.title = repairedTitle;
|
|
416
|
+
} else {
|
|
417
|
+
assertHumanReadableDeckTitle(fields.title);
|
|
418
|
+
deck.title = fields.title;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
336
421
|
if ("designSystemId" in fields)
|
|
337
422
|
deck.designSystemId = fields.designSystemId;
|
|
338
423
|
if (fields.tweaks !== undefined) deck.tweaks = fields.tweaks;
|
|
@@ -353,6 +438,7 @@ export function applyOperation(deck: any, op: Operation): void {
|
|
|
353
438
|
export function resolveDeckColumnUpdates(
|
|
354
439
|
current: { title: string; designSystemId: string | null },
|
|
355
440
|
operations: Operation[],
|
|
441
|
+
resolvedTitle?: string,
|
|
356
442
|
): { title: string; designSystemId: string | null } {
|
|
357
443
|
const fieldOps = operations
|
|
358
444
|
.filter(
|
|
@@ -363,13 +449,25 @@ export function resolveDeckColumnUpdates(
|
|
|
363
449
|
const titleOp = fieldOps.find((op) => typeof op.fields.title === "string");
|
|
364
450
|
const dsOp = fieldOps.find((op) => "designSystemId" in op.fields);
|
|
365
451
|
return {
|
|
366
|
-
title: titleOp?.fields.title ?? current.title,
|
|
452
|
+
title: resolvedTitle ?? titleOp?.fields.title ?? current.title,
|
|
367
453
|
designSystemId: dsOp
|
|
368
454
|
? (dsOp.fields.designSystemId ?? null)
|
|
369
455
|
: current.designSystemId,
|
|
370
456
|
};
|
|
371
457
|
}
|
|
372
458
|
|
|
459
|
+
/**
|
|
460
|
+
* The source-preservation guards (`assertSourceImportOperationsPreserved`,
|
|
461
|
+
* `assertSourceSlidePreserved`) exist for one failure mode: an agent asked to
|
|
462
|
+
* "make it prettier" silently dropping the original PDF/PPTX artwork or
|
|
463
|
+
* factual copy. A human editing their own imported deck in the browser isn't
|
|
464
|
+
* that failure mode, and the browser editor has no way to pass
|
|
465
|
+
* `preserveSource` — so these guards must only run for agent callers.
|
|
466
|
+
*/
|
|
467
|
+
export function isAgentPatchCaller(caller: string | undefined): boolean {
|
|
468
|
+
return caller === "tool" || caller === "mcp" || caller === "a2a";
|
|
469
|
+
}
|
|
470
|
+
|
|
373
471
|
// ---------------------------------------------------------------------------
|
|
374
472
|
// Action definition
|
|
375
473
|
// ---------------------------------------------------------------------------
|
|
@@ -379,10 +477,22 @@ export default defineAction({
|
|
|
379
477
|
"Granular deck patch used by the browser editor for concurrent-safe writes. " +
|
|
380
478
|
"Each operation touches only the target slide or field — concurrent writers " +
|
|
381
479
|
"on different slides never overwrite each other's work. For a deck-wide " +
|
|
382
|
-
"
|
|
383
|
-
"
|
|
480
|
+
"source restyle, set requireAllSourceSlides=true and send one patch-slide " +
|
|
481
|
+
"operation with content for every imported slide in one call; the action " +
|
|
482
|
+
"rejects partial coverage. For animations, inspect the final slide HTML, " +
|
|
483
|
+
"then patch content and the complete ordered animations list together; " +
|
|
484
|
+
"validate every 0-based elementPath and do not invent one-based indexes. " +
|
|
485
|
+
"Then call get-deck with compact=true to verify the persisted slide IDs, " +
|
|
486
|
+
"count, and animation metadata before reporting success.",
|
|
384
487
|
schema: z.object({
|
|
385
488
|
deckId: z.string().describe("Deck ID"),
|
|
489
|
+
requireAllSourceSlides: z
|
|
490
|
+
.boolean()
|
|
491
|
+
.optional()
|
|
492
|
+
.default(false)
|
|
493
|
+
.describe(
|
|
494
|
+
"For a deck-wide source-import restyle, require one content patch for every imported slide before any write is committed.",
|
|
495
|
+
),
|
|
386
496
|
operations: z
|
|
387
497
|
.array(OperationSchema)
|
|
388
498
|
.min(1)
|
|
@@ -402,8 +512,12 @@ export default defineAction({
|
|
|
402
512
|
),
|
|
403
513
|
}),
|
|
404
514
|
agentInputSchema: AgentPatchDeckInputSchema,
|
|
405
|
-
run: async (
|
|
515
|
+
run: async (
|
|
516
|
+
{ deckId, operations, requireAllSourceSlides, creativeContext },
|
|
517
|
+
ctx,
|
|
518
|
+
) => {
|
|
406
519
|
await assertAccess("deck", deckId, "editor");
|
|
520
|
+
const isAgentCaller = isAgentPatchCaller(ctx?.caller);
|
|
407
521
|
|
|
408
522
|
return withDeckLock(deckId, async () => {
|
|
409
523
|
const db = getDb();
|
|
@@ -435,9 +549,17 @@ export default defineAction({
|
|
|
435
549
|
}
|
|
436
550
|
|
|
437
551
|
const sourceImport = sourceImportForDeck(deck.sourceImport);
|
|
438
|
-
|
|
552
|
+
if (isAgentCaller) {
|
|
553
|
+
assertSourceImportOperationsPreserved(sourceImport, operations);
|
|
554
|
+
assertSourceImportSlidesCovered(
|
|
555
|
+
sourceImport,
|
|
556
|
+
operations,
|
|
557
|
+
requireAllSourceSlides,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
439
560
|
for (const op of operations) {
|
|
440
561
|
if (
|
|
562
|
+
!isAgentCaller ||
|
|
441
563
|
op.op !== "patch-slide" ||
|
|
442
564
|
(op.fields.content === undefined && op.fields.notes === undefined)
|
|
443
565
|
) {
|
|
@@ -466,6 +588,13 @@ export default defineAction({
|
|
|
466
588
|
resolveDeckColumnUpdates(
|
|
467
589
|
{ title: row.title, designSystemId: row.designSystemId },
|
|
468
590
|
operations,
|
|
591
|
+
operations.some(
|
|
592
|
+
(operation) =>
|
|
593
|
+
operation.op === "patch-deck-fields" &&
|
|
594
|
+
operation.fields.title !== undefined,
|
|
595
|
+
) && typeof deck.title === "string"
|
|
596
|
+
? deck.title
|
|
597
|
+
: undefined,
|
|
469
598
|
);
|
|
470
599
|
|
|
471
600
|
let generationRecord:
|
|
@@ -19,6 +19,10 @@ import { z } from "zod";
|
|
|
19
19
|
import { getDb, schema } from "../server/db/index.js";
|
|
20
20
|
import { notifyClients } from "../server/handlers/decks.js";
|
|
21
21
|
import { createDeckVersionSnapshot } from "../server/lib/deck-versions.js";
|
|
22
|
+
import {
|
|
23
|
+
assertHumanReadableDeckTitle,
|
|
24
|
+
repairGeneratedDeckTitle,
|
|
25
|
+
} from "../shared/deck-title.js";
|
|
22
26
|
import {
|
|
23
27
|
assertDesignSystemReadable,
|
|
24
28
|
assertValidAspectRatio,
|
|
@@ -71,7 +75,7 @@ export default defineAction({
|
|
|
71
75
|
|
|
72
76
|
deck.id = deckId;
|
|
73
77
|
deck.updatedAt = now;
|
|
74
|
-
const
|
|
78
|
+
const requestedTitle = deckTitle(deck);
|
|
75
79
|
const nextDesignSystemId = deckDesignSystemId(deck);
|
|
76
80
|
|
|
77
81
|
// Resolve access first — this loads the row AND tells us the caller's
|
|
@@ -88,6 +92,11 @@ export default defineAction({
|
|
|
88
92
|
if (!ownerEmail) {
|
|
89
93
|
throw deckHttpError(403, "Sign in to create a deck");
|
|
90
94
|
}
|
|
95
|
+
const title =
|
|
96
|
+
repairGeneratedDeckTitle(requestedTitle, firstSlideContent(deck)) ??
|
|
97
|
+
requestedTitle;
|
|
98
|
+
assertHumanReadableDeckTitle(title);
|
|
99
|
+
deck.title = title;
|
|
91
100
|
await assertDesignSystemReadable(nextDesignSystemId);
|
|
92
101
|
try {
|
|
93
102
|
await db.insert(schema.decks).values({
|
|
@@ -110,6 +119,14 @@ export default defineAction({
|
|
|
110
119
|
access.role === "admin" ||
|
|
111
120
|
access.role === "editor"
|
|
112
121
|
) {
|
|
122
|
+
const title =
|
|
123
|
+
repairGeneratedDeckTitle(
|
|
124
|
+
requestedTitle,
|
|
125
|
+
firstSlideContent(deck),
|
|
126
|
+
access.resource.title,
|
|
127
|
+
) ?? requestedTitle;
|
|
128
|
+
assertHumanReadableDeckTitle(title);
|
|
129
|
+
deck.title = title;
|
|
113
130
|
await assertDesignSystemReadable(nextDesignSystemId);
|
|
114
131
|
if (shouldSnapshotDeckWrite(access.resource, title, deck)) {
|
|
115
132
|
await createDeckVersionSnapshot(
|
|
@@ -142,3 +159,9 @@ export default defineAction({
|
|
|
142
159
|
return deck;
|
|
143
160
|
}),
|
|
144
161
|
});
|
|
162
|
+
|
|
163
|
+
function firstSlideContent(deck: DeckPayload): string | null {
|
|
164
|
+
const slides = Array.isArray(deck.slides) ? deck.slides : [];
|
|
165
|
+
const content = slides[0] && (slides[0] as Record<string, unknown>).content;
|
|
166
|
+
return typeof content === "string" ? content : null;
|
|
167
|
+
}
|
|
@@ -385,6 +385,29 @@ function useSlideAutofit(
|
|
|
385
385
|
|
|
386
386
|
let raf = 0;
|
|
387
387
|
let disposed = false;
|
|
388
|
+
// Measuring costs a full-document reflow per slide (every descendant is
|
|
389
|
+
// read with getBoundingClientRect, interleaved with style writes). A deck
|
|
390
|
+
// with dozens of slides mounts that many renderers at once, so off-screen
|
|
391
|
+
// thumbnails are left unmeasured until they scroll into view. Without an
|
|
392
|
+
// IntersectionObserver there is nothing to defer against, so measure
|
|
393
|
+
// eagerly as before.
|
|
394
|
+
const canDefer = typeof IntersectionObserver !== "undefined";
|
|
395
|
+
// Resolved synchronously rather than waiting for the observer's first
|
|
396
|
+
// callback: a slide that is already on screen must be measured on this
|
|
397
|
+
// pass, and nothing may depend on a callback that a given environment
|
|
398
|
+
// might never deliver. This reads one rect, not one per descendant.
|
|
399
|
+
const isNearViewport = () => {
|
|
400
|
+
const rect = root.getBoundingClientRect();
|
|
401
|
+
const margin = 200;
|
|
402
|
+
return (
|
|
403
|
+
rect.bottom >= -margin &&
|
|
404
|
+
rect.right >= -margin &&
|
|
405
|
+
rect.top <= (window.innerHeight || 0) + margin &&
|
|
406
|
+
rect.left <= (window.innerWidth || 0) + margin
|
|
407
|
+
);
|
|
408
|
+
};
|
|
409
|
+
let visible = !canDefer || isNearViewport();
|
|
410
|
+
let measurePending = false;
|
|
388
411
|
|
|
389
412
|
const resetTarget = (target: HTMLElement) => {
|
|
390
413
|
target.style.setProperty("--fmd-fit-scale", "1");
|
|
@@ -482,6 +505,10 @@ function useSlideAutofit(
|
|
|
482
505
|
|
|
483
506
|
const scheduleMeasure = () => {
|
|
484
507
|
if (disposed) return;
|
|
508
|
+
if (!visible) {
|
|
509
|
+
measurePending = true;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
485
512
|
cancelAnimationFrame(raf);
|
|
486
513
|
raf = requestAnimationFrame(measureNow);
|
|
487
514
|
};
|
|
@@ -502,11 +529,30 @@ function useSlideAutofit(
|
|
|
502
529
|
root.addEventListener("load", scheduleMeasure, true);
|
|
503
530
|
document.fonts?.ready.then(scheduleMeasure).catch(() => {});
|
|
504
531
|
|
|
532
|
+
// `rootMargin` measures a thumbnail just before it scrolls in, so the fit
|
|
533
|
+
// transform is already applied by the time it is on screen.
|
|
534
|
+
const visibilityObserver = canDefer
|
|
535
|
+
? new IntersectionObserver(
|
|
536
|
+
(entries) => {
|
|
537
|
+
const isVisible = entries.some((entry) => entry.isIntersecting);
|
|
538
|
+
if (isVisible === visible) return;
|
|
539
|
+
visible = isVisible;
|
|
540
|
+
if (visible && measurePending) {
|
|
541
|
+
measurePending = false;
|
|
542
|
+
scheduleMeasure();
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
{ rootMargin: "200px" },
|
|
546
|
+
)
|
|
547
|
+
: null;
|
|
548
|
+
visibilityObserver?.observe(root);
|
|
549
|
+
|
|
505
550
|
return () => {
|
|
506
551
|
disposed = true;
|
|
507
552
|
cancelAnimationFrame(raf);
|
|
508
553
|
resizeObserver.disconnect();
|
|
509
554
|
mutationObserver.disconnect();
|
|
555
|
+
visibilityObserver?.disconnect();
|
|
510
556
|
root.removeEventListener("load", scheduleMeasure, true);
|
|
511
557
|
};
|
|
512
558
|
}, [canvasWidth, canvasHeight, fitKey, ref]);
|