@agent-native/core 0.149.1 → 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 +122 -9
- package/corpus/templates/slides/actions/save-deck.ts +24 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +69 -60
- 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 +3 -3
- 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/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 +4 -27
- package/corpus/templates/slides/app/pages/Index.tsx +240 -89
- 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/lib/chat-attachments.ts +1 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
- 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/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/collab/struct-routes.d.ts +1 -1
- package/dist/integrations/webhook-handler.js +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- 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,7 +449,7 @@ 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,
|
|
@@ -391,10 +477,22 @@ export default defineAction({
|
|
|
391
477
|
"Granular deck patch used by the browser editor for concurrent-safe writes. " +
|
|
392
478
|
"Each operation touches only the target slide or field — concurrent writers " +
|
|
393
479
|
"on different slides never overwrite each other's work. For a deck-wide " +
|
|
394
|
-
"
|
|
395
|
-
"
|
|
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.",
|
|
396
487
|
schema: z.object({
|
|
397
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
|
+
),
|
|
398
496
|
operations: z
|
|
399
497
|
.array(OperationSchema)
|
|
400
498
|
.min(1)
|
|
@@ -414,7 +512,10 @@ export default defineAction({
|
|
|
414
512
|
),
|
|
415
513
|
}),
|
|
416
514
|
agentInputSchema: AgentPatchDeckInputSchema,
|
|
417
|
-
run: async (
|
|
515
|
+
run: async (
|
|
516
|
+
{ deckId, operations, requireAllSourceSlides, creativeContext },
|
|
517
|
+
ctx,
|
|
518
|
+
) => {
|
|
418
519
|
await assertAccess("deck", deckId, "editor");
|
|
419
520
|
const isAgentCaller = isAgentPatchCaller(ctx?.caller);
|
|
420
521
|
|
|
@@ -450,6 +551,11 @@ export default defineAction({
|
|
|
450
551
|
const sourceImport = sourceImportForDeck(deck.sourceImport);
|
|
451
552
|
if (isAgentCaller) {
|
|
452
553
|
assertSourceImportOperationsPreserved(sourceImport, operations);
|
|
554
|
+
assertSourceImportSlidesCovered(
|
|
555
|
+
sourceImport,
|
|
556
|
+
operations,
|
|
557
|
+
requireAllSourceSlides,
|
|
558
|
+
);
|
|
453
559
|
}
|
|
454
560
|
for (const op of operations) {
|
|
455
561
|
if (
|
|
@@ -482,6 +588,13 @@ export default defineAction({
|
|
|
482
588
|
resolveDeckColumnUpdates(
|
|
483
589
|
{ title: row.title, designSystemId: row.designSystemId },
|
|
484
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,
|
|
485
598
|
);
|
|
486
599
|
|
|
487
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
|
+
}
|