@agent-native/core 0.149.3 → 0.149.5

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 (33) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +23 -4
  3. package/corpus/templates/slides/actions/get-deck.ts +25 -9
  4. package/corpus/templates/slides/actions/patch-deck.ts +51 -0
  5. package/corpus/templates/slides/actions/update-slide.ts +144 -14
  6. package/corpus/templates/slides/app/lib/create-deck-generation.ts +12 -4
  7. package/corpus/templates/slides/changelog/2026-08-11-pdf-requests-to-restyle-and-preserve-a-source-deck-now-impor.md +6 -0
  8. package/corpus/templates/slides/changelog/2026-08-11-slide-edits-preserve-their-layout-without-introducing-hidden.md +6 -0
  9. package/corpus/templates/slides/changelog/2026-08-11-slides-reliably-imports-uploaded-pdfs-and-presentations.md +6 -0
  10. package/corpus/templates/slides/changelog/2026-08-11-slides-support-atomic-code-style-html-patches-without-regene.md +6 -0
  11. package/corpus/templates/slides/package.json +1 -0
  12. package/corpus/templates/slides/server/lib/slide-content-patch.ts +355 -0
  13. package/corpus/templates/slides/server/lib/uploaded-reference-storage.ts +28 -9
  14. package/corpus/templates/slides/server/plugins/agent-chat.ts +12 -3
  15. package/dist/cli/create.js +49 -0
  16. package/dist/client/chat-first/index.d.ts +1 -1
  17. package/dist/client/chat-first/index.js +1 -1
  18. package/dist/client/chat-first/primary-nav.d.ts +3 -1
  19. package/dist/client/chat-first/primary-nav.js +8 -2
  20. package/dist/collab/struct-routes.d.ts +1 -1
  21. package/dist/deploy/build.d.ts +4 -0
  22. package/dist/deploy/build.js +5 -0
  23. package/dist/deploy/workspace-deploy.js +5 -0
  24. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  25. package/dist/notifications/routes.d.ts +6 -6
  26. package/dist/private-blob/registry.js +67 -4
  27. package/dist/secrets/routes.d.ts +9 -9
  28. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  29. package/dist/templates/default/pnpm-workspace.yaml +44 -0
  30. package/dist/templates/workspace-root/pnpm-workspace.yaml +44 -0
  31. package/package.json +1 -1
  32. package/src/templates/default/pnpm-workspace.yaml +44 -0
  33. package/src/templates/workspace-root/pnpm-workspace.yaml +44 -0
@@ -0,0 +1,355 @@
1
+ export type SlideContentEdit =
2
+ | {
3
+ op?: "replace";
4
+ find: string;
5
+ replace: string;
6
+ all?: boolean;
7
+ occurrence?: number;
8
+ expectedMatches?: number;
9
+ required?: boolean;
10
+ }
11
+ | {
12
+ op: "insert-before" | "insert-after";
13
+ marker: string;
14
+ content: string;
15
+ occurrence?: number;
16
+ expectedMatches?: number;
17
+ required?: boolean;
18
+ }
19
+ | {
20
+ op: "replace-between";
21
+ start: string;
22
+ end: string;
23
+ content: string;
24
+ includeDelimiters?: boolean;
25
+ expectedMatches?: number;
26
+ required?: boolean;
27
+ }
28
+ | {
29
+ op: "regex-replace";
30
+ pattern: string;
31
+ replace: string;
32
+ flags?: string;
33
+ all?: boolean;
34
+ expectedMatches?: number;
35
+ required?: boolean;
36
+ };
37
+
38
+ export class SlideContentEditError extends Error {
39
+ readonly code = "slide_content_edit_failed";
40
+
41
+ constructor(message: string) {
42
+ super(message);
43
+ this.name = "SlideContentEditError";
44
+ }
45
+ }
46
+
47
+ export interface SlideContentPatchResult {
48
+ content: string;
49
+ applied: string[];
50
+ formatted: boolean;
51
+ changed: boolean;
52
+ }
53
+
54
+ /**
55
+ * Applies every edit to an in-memory string before the caller persists it.
56
+ * A failed edit throws, so callers never write a partially applied patch list.
57
+ */
58
+ export async function applySlideContentEdits(
59
+ currentContent: string,
60
+ edits: readonly SlideContentEdit[],
61
+ format = false,
62
+ ): Promise<SlideContentPatchResult> {
63
+ try {
64
+ let content = currentContent;
65
+ const applied: string[] = [];
66
+
67
+ for (const edit of edits) {
68
+ const result = applyEdit(content, edit);
69
+ content = result.content;
70
+ applied.push(result.summary);
71
+ }
72
+
73
+ const changed = content !== currentContent;
74
+
75
+ if (format) {
76
+ content = await formatSlideHtml(content);
77
+ }
78
+
79
+ return { content, applied, formatted: format, changed };
80
+ } catch (error) {
81
+ if (error instanceof SlideContentEditError) throw error;
82
+ const message = error instanceof Error ? error.message : String(error);
83
+ throw new SlideContentEditError(message);
84
+ }
85
+ }
86
+
87
+ export async function formatSlideHtml(content: string): Promise<string> {
88
+ try {
89
+ const prettier = await import("prettier");
90
+ return await prettier.format(content, {
91
+ parser: "html",
92
+ htmlWhitespaceSensitivity: "ignore",
93
+ });
94
+ } catch (error) {
95
+ const message = error instanceof Error ? error.message : String(error);
96
+ if (
97
+ message.includes("Cannot find package 'prettier'") ||
98
+ message.includes('Cannot find package "prettier"') ||
99
+ message.includes("Cannot find module 'prettier'") ||
100
+ message.includes('Cannot find module "prettier"')
101
+ ) {
102
+ throw new SlideContentEditError(
103
+ "HTML formatting is unavailable because Prettier is not installed",
104
+ );
105
+ }
106
+ throw new SlideContentEditError(`Unable to format slide HTML: ${message}`);
107
+ }
108
+ }
109
+
110
+ function applyEdit(
111
+ content: string,
112
+ edit: SlideContentEdit,
113
+ ): { content: string; summary: string } {
114
+ switch (edit.op ?? "replace") {
115
+ case "replace":
116
+ return applyLiteralReplace(
117
+ content,
118
+ edit as Extract<SlideContentEdit, { op?: "replace" }>,
119
+ );
120
+ case "insert-before":
121
+ case "insert-after":
122
+ return applyInsert(
123
+ content,
124
+ edit as Extract<
125
+ SlideContentEdit,
126
+ { op: "insert-before" | "insert-after" }
127
+ >,
128
+ );
129
+ case "replace-between":
130
+ return applyReplaceBetween(
131
+ content,
132
+ edit as Extract<SlideContentEdit, { op: "replace-between" }>,
133
+ );
134
+ case "regex-replace":
135
+ return applyRegexReplace(
136
+ content,
137
+ edit as Extract<SlideContentEdit, { op: "regex-replace" }>,
138
+ );
139
+ default:
140
+ throw new SlideContentEditError(
141
+ `Unsupported slide content edit operation: ${String(edit.op)}`,
142
+ );
143
+ }
144
+ }
145
+
146
+ function applyLiteralReplace(
147
+ content: string,
148
+ edit: Extract<SlideContentEdit, { op?: "replace" }>,
149
+ ): { content: string; summary: string } {
150
+ const matches = countOccurrences(content, edit.find);
151
+ assertMatchCount("replace", matches, edit.expectedMatches, edit.required);
152
+ if (matches === 0) return { content, summary: "replace:0" };
153
+
154
+ if (edit.occurrence !== undefined) {
155
+ return {
156
+ content: replaceNth(content, edit.find, edit.replace, edit.occurrence),
157
+ summary: `replace:nth:${edit.occurrence}`,
158
+ };
159
+ }
160
+
161
+ if (edit.all) {
162
+ return {
163
+ content: content.split(edit.find).join(edit.replace),
164
+ summary: `replace:all:${matches}`,
165
+ };
166
+ }
167
+
168
+ return {
169
+ content: content.replace(edit.find, edit.replace),
170
+ summary: "replace:first",
171
+ };
172
+ }
173
+
174
+ function applyInsert(
175
+ content: string,
176
+ edit: Extract<SlideContentEdit, { op: "insert-before" | "insert-after" }>,
177
+ ): { content: string; summary: string } {
178
+ const matches = countOccurrences(content, edit.marker);
179
+ assertMatchCount(edit.op, matches, edit.expectedMatches, edit.required);
180
+ if (matches === 0) return { content, summary: `${edit.op}:0` };
181
+
182
+ const occurrence = edit.occurrence ?? 1;
183
+ const index = nthIndexOf(content, edit.marker, occurrence);
184
+ if (index < 0) {
185
+ throw new SlideContentEditError(
186
+ `${edit.op} could not find occurrence ${occurrence}`,
187
+ );
188
+ }
189
+ const insertAt =
190
+ edit.op === "insert-before" ? index : index + edit.marker.length;
191
+ return {
192
+ content:
193
+ content.slice(0, insertAt) + edit.content + content.slice(insertAt),
194
+ summary: `${edit.op}:${occurrence}`,
195
+ };
196
+ }
197
+
198
+ function applyReplaceBetween(
199
+ content: string,
200
+ edit: Extract<SlideContentEdit, { op: "replace-between" }>,
201
+ ): { content: string; summary: string } {
202
+ const ranges = findBetweenRanges(content, edit.start, edit.end);
203
+ assertMatchCount(
204
+ "replace-between",
205
+ ranges.length,
206
+ edit.expectedMatches,
207
+ edit.required,
208
+ );
209
+ if (!ranges.length) return { content, summary: "replace-between:0" };
210
+ if (ranges.length > 1 && edit.expectedMatches === undefined) {
211
+ throw new SlideContentEditError(
212
+ `replace-between matched ${ranges.length} ranges; pass expectedMatches to confirm`,
213
+ );
214
+ }
215
+
216
+ let next = content;
217
+ for (const range of ranges.slice().reverse()) {
218
+ const start = edit.includeDelimiters ? range.start : range.innerStart;
219
+ const end = edit.includeDelimiters ? range.end : range.innerEnd;
220
+ next = next.slice(0, start) + edit.content + next.slice(end);
221
+ }
222
+ return { content: next, summary: `replace-between:${ranges.length}` };
223
+ }
224
+
225
+ function applyRegexReplace(
226
+ content: string,
227
+ edit: Extract<SlideContentEdit, { op: "regex-replace" }>,
228
+ ): { content: string; summary: string } {
229
+ const flags = normalizeRegexFlags(edit.flags, edit.all);
230
+ const regex = new RegExp(edit.pattern, flags);
231
+ const countRegex = new RegExp(edit.pattern, ensureGlobal(flags));
232
+ const matches = Array.from(content.matchAll(countRegex)).length;
233
+ assertMatchCount(
234
+ "regex-replace",
235
+ matches,
236
+ edit.expectedMatches,
237
+ edit.required,
238
+ );
239
+ if (matches === 0) return { content, summary: "regex-replace:0" };
240
+ return {
241
+ content: content.replace(regex, edit.replace),
242
+ summary: `regex-replace:${edit.all ? "all" : "first"}:${matches}`,
243
+ };
244
+ }
245
+
246
+ function assertMatchCount(
247
+ op: string,
248
+ actual: number,
249
+ expected: number | undefined,
250
+ required: boolean | undefined,
251
+ ): void {
252
+ if (expected !== undefined && actual !== expected) {
253
+ throw new SlideContentEditError(
254
+ `${op} expected ${expected} match(es), found ${actual}`,
255
+ );
256
+ }
257
+ if (expected === undefined && required !== false && actual === 0) {
258
+ throw new SlideContentEditError(`${op} found no matches`);
259
+ }
260
+ }
261
+
262
+ function countOccurrences(content: string, needle: string): number {
263
+ if (!needle) {
264
+ throw new SlideContentEditError("Patch find/marker text cannot be empty");
265
+ }
266
+ let count = 0;
267
+ let index = 0;
268
+ while (true) {
269
+ index = content.indexOf(needle, index);
270
+ if (index < 0) return count;
271
+ count += 1;
272
+ index += needle.length;
273
+ }
274
+ }
275
+
276
+ function nthIndexOf(
277
+ content: string,
278
+ needle: string,
279
+ occurrence: number,
280
+ ): number {
281
+ if (!Number.isInteger(occurrence) || occurrence < 1) {
282
+ throw new SlideContentEditError("occurrence must be a positive integer");
283
+ }
284
+ let index = -1;
285
+ let from = 0;
286
+ for (let i = 0; i < occurrence; i += 1) {
287
+ index = content.indexOf(needle, from);
288
+ if (index < 0) return -1;
289
+ from = index + needle.length;
290
+ }
291
+ return index;
292
+ }
293
+
294
+ function replaceNth(
295
+ content: string,
296
+ find: string,
297
+ replace: string,
298
+ occurrence: number,
299
+ ): string {
300
+ const index = nthIndexOf(content, find, occurrence);
301
+ if (index < 0) {
302
+ throw new SlideContentEditError(
303
+ `replace could not find occurrence ${occurrence}`,
304
+ );
305
+ }
306
+ return content.slice(0, index) + replace + content.slice(index + find.length);
307
+ }
308
+
309
+ function findBetweenRanges(
310
+ content: string,
311
+ startMarker: string,
312
+ endMarker: string,
313
+ ): Array<{ start: number; innerStart: number; innerEnd: number; end: number }> {
314
+ if (!startMarker || !endMarker) {
315
+ throw new SlideContentEditError(
316
+ "replace-between requires non-empty start and end markers",
317
+ );
318
+ }
319
+ const ranges: Array<{
320
+ start: number;
321
+ innerStart: number;
322
+ innerEnd: number;
323
+ end: number;
324
+ }> = [];
325
+ let cursor = 0;
326
+ while (cursor < content.length) {
327
+ const start = content.indexOf(startMarker, cursor);
328
+ if (start < 0) break;
329
+ const innerStart = start + startMarker.length;
330
+ const innerEnd = content.indexOf(endMarker, innerStart);
331
+ if (innerEnd < 0) {
332
+ throw new SlideContentEditError(
333
+ "replace-between found a start marker without an end",
334
+ );
335
+ }
336
+ const end = innerEnd + endMarker.length;
337
+ ranges.push({ start, innerStart, innerEnd, end });
338
+ cursor = end;
339
+ }
340
+ return ranges;
341
+ }
342
+
343
+ function normalizeRegexFlags(flags: string | undefined, all?: boolean): string {
344
+ const unique = new Set((flags ?? "").split("").filter(Boolean));
345
+ if (all) {
346
+ unique.add("g");
347
+ } else {
348
+ unique.delete("g");
349
+ }
350
+ return Array.from(unique).join("");
351
+ }
352
+
353
+ function ensureGlobal(flags: string): string {
354
+ return flags.includes("g") ? flags : `${flags}g`;
355
+ }
@@ -84,25 +84,44 @@ export async function readUploadedReferenceBlob(
84
84
  decryptSecretValue(reference.slice(UPLOADED_REFERENCE_PREFIX.length)),
85
85
  ) as UploadedReferenceDescriptor;
86
86
  } catch {
87
+ console.warn("[slides-upload] invalid uploaded reference descriptor", {
88
+ reason: "decrypt-or-parse",
89
+ });
87
90
  throw new Error("Invalid uploaded file reference");
88
91
  }
89
92
 
93
+ const requestOrgId = getRequestOrgId() ?? null;
94
+ const ownerMatches = descriptor?.ownerKey === tenantFileKey(email);
95
+ const orgMatches = descriptor?.orgId === requestOrgId;
96
+ const filename = descriptor?.filename;
97
+ const safeFilename =
98
+ typeof filename === "string" && filename === path.basename(filename)
99
+ ? filename
100
+ : null;
101
+ const handleIsOpaque = descriptor?.handle?.opaque === true;
90
102
  if (
91
103
  descriptor?.kind !== "slides-upload" ||
92
- descriptor.version !== 1 ||
93
- descriptor.ownerKey !== tenantFileKey(email) ||
94
- descriptor.orgId !== (getRequestOrgId() ?? null) ||
95
- typeof descriptor.filename !== "string" ||
96
- descriptor.filename !== path.basename(descriptor.filename) ||
97
- typeof descriptor.handle?.id !== "string" ||
98
- typeof descriptor.handle?.provider !== "string" ||
99
- descriptor.handle.opaque !== true
104
+ descriptor?.version !== 1 ||
105
+ !ownerMatches ||
106
+ !orgMatches ||
107
+ safeFilename === null ||
108
+ typeof descriptor?.handle?.id !== "string" ||
109
+ typeof descriptor?.handle?.provider !== "string" ||
110
+ !handleIsOpaque
100
111
  ) {
112
+ console.warn("[slides-upload] uploaded reference rejected", {
113
+ handleIsOpaque,
114
+ hasDescriptorOrg: descriptor?.orgId != null,
115
+ hasRequestOrg: requestOrgId != null,
116
+ orgMatches,
117
+ ownerMatches,
118
+ reason: "scope-or-handle-validation",
119
+ });
101
120
  throw new Error(
102
121
  "Access denied: uploaded file reference is not valid for this user or organization",
103
122
  );
104
123
  }
105
124
 
106
125
  const blob = await readPrivateBlob(descriptor.handle);
107
- return { data: Buffer.from(blob.data), filename: descriptor.filename };
126
+ return { data: Buffer.from(blob.data), filename: safeFilename };
108
127
  }
@@ -1,10 +1,10 @@
1
- import { getOrgContext } from "@agent-native/core/org";
2
1
  import {
3
2
  createAgentChatPlugin,
4
3
  loadActionsFromStaticRegistry,
5
4
  } from "@agent-native/core/server";
6
5
 
7
6
  import actionsRegistry from "../../.generated/actions-registry.js";
7
+ import { resolveSlidesRequestAuthContext } from "../handlers/request-auth-context.js";
8
8
  import { prepareSlidesChatAttachments } from "../lib/chat-attachments.js";
9
9
  import "../register-secrets.js";
10
10
 
@@ -48,12 +48,21 @@ export default createAgentChatPlugin({
48
48
  // paginate, and reduce provider data through providerFetch() without us
49
49
  // hardcoding one action per Google Drive endpoint.
50
50
  codeExecution: { production: "sandboxed" },
51
- resolveOrgId: async (event) => (await getOrgContext(event)).orgId,
51
+ // Upload routes and action routes must use the same session/org resolver.
52
+ // Reading getOrgContext directly here skipped the upload route's session
53
+ // fallback and could reject a freshly uploaded reference after a transient
54
+ // org lookup or active-org transition.
55
+ resolveOrgId: async (event) => {
56
+ const authContext = await resolveSlidesRequestAuthContext(event);
57
+ return authContext.orgId === undefined ? null : authContext.orgId;
58
+ },
52
59
  prepareRequest: prepareSlidesChatAttachments,
53
60
  systemPrompt: `You are an AI deck assistant. You create, edit, import, export, style, share, and navigate decks through actions and shared application state. For a newly created presentation, use create-deck with slides: [] only when you are creating the deck yourself, then add-slide sequentially with full rendered HTML. The legacy generate-slides-ai action returns Markdown drafts and is not part of the persisted presentation workflow.
54
61
 
55
62
  When the user asks to improve, beautify, restyle, or make an uploaded/existing deck on-brand, treat it as an in-place source-preserving edit unless the user explicitly asks to rewrite the story or change slide count. First call view-screen when the active deck is unclear, then get-deck with compact=true for deck orientation. If you need slide markup, call get-deck with compact=false explicitly and only when the edit requires the full HTML. If get-deck.sourceImport exists, preserve its slide count, order, IDs, factual copy, notes, images, charts, tables, diagrams, freeform objects, and source aspect ratio. For a deck-wide restyle, use one patch-deck call with requireAllSourceSlides=true and one patch-slide operation with fields.content for every source slide ID; the action rejects partial coverage. Do not split a full-deck restyle into arbitrary batches or use one-by-one update-slide calls - reserve update-slide for targeted one-slide edits. After the patch succeeds, verify with get-deck using compact=true so the verification does not retransmit every slide's HTML. Do not claim a partial or initial pass. Do not use add-slide, delete, reorder, or replace source imagery with generic cards for this workflow. If sourceImport.fidelity is partial or imagesSkipped is nonzero, stop and report the exact fidelity warning instead of claiming a reliable improvement.
56
- For a targeted one-slide edit, prefer get-deck with the stable slideId from view-screen or compact get-deck. It returns only that slide's full HTML by default, so do not load the full deck when one slide is enough. Pass compact=true when you only need a lightweight check of that slide.
63
+ For a targeted one-slide edit, prefer get-deck with the stable slideId from view-screen or compact get-deck. It returns only that slide's full HTML by default, so do not load the full deck when one slide is enough. For code-style edits, request compact=false and format=true, then call update-slide with that slide's contentHash as baseContentHash and an ordered edits list. Use exact replace, insert-before/after, replace-between, or regex-replace edits; include expectedMatches for ambiguous markers. The action applies the whole list atomically under the deck lock, so a failed required match writes nothing. Set format=true on update-slide when readable line breaks should be persisted. Use fullContent only for an intentional full rewrite. Pass compact=true when you only need a lightweight check of that slide.
64
+
65
+ For click-to-reveal animations, keep the slide HTML as the visual source and store reveals only in the slide's animations metadata. Read the target slide's full HTML, preserve its existing structure, and patch the complete ordered animations list with elementPath values from that final HTML. Leave labels and headings visible by omitting them from the list. Never simulate reveals by adding duplicate elements, visibility:hidden, fmd-layout-spacer, fmd-freeform-object, absolute positioning, transforms, or placeholder markup. If content and reveals both change, send them together in one patch-deck patch-slide operation. If a user asks to remove or revert reveals, send the existing content with animations: [], then re-read the slide; do not use a simplified replacement or rely on update-slide text alone.
57
66
 
58
67
  When the active Slides editor is already showing the deck you just changed, do not include an "Open the updated deck" or similar link in the final response. The deck is already open and the action updated it in place; say that plainly instead. Only provide an open-deck link when the user is elsewhere, the active deck is different, or the user explicitly asks for a link.
59
68
 
@@ -17,6 +17,49 @@ const STANDALONE_EXACT_DEPENDENCY_OVERRIDES = {
17
17
  "@react-router/fs-routes": "8.1.0",
18
18
  "react-router": "8.1.0",
19
19
  };
20
+ const TIPTAP_WORKSPACE_OVERRIDES = {
21
+ '"@tiptap/core"': '"3.28.0"',
22
+ '"@tiptap/extension-blockquote"': '"3.28.0"',
23
+ '"@tiptap/extension-bold"': '"3.28.0"',
24
+ '"@tiptap/extension-bubble-menu"': '"3.28.0"',
25
+ '"@tiptap/extension-bullet-list"': '"3.28.0"',
26
+ '"@tiptap/extension-code"': '"3.28.0"',
27
+ '"@tiptap/extension-code-block"': '"3.28.0"',
28
+ '"@tiptap/extension-code-block-lowlight"': '"3.28.0"',
29
+ '"@tiptap/extension-collaboration"': '"3.28.0"',
30
+ '"@tiptap/extension-collaboration-caret"': '"3.28.0"',
31
+ '"@tiptap/extension-color"': '"3.28.0"',
32
+ '"@tiptap/extension-document"': '"3.28.0"',
33
+ '"@tiptap/extension-dropcursor"': '"3.28.0"',
34
+ '"@tiptap/extension-floating-menu"': '"3.28.0"',
35
+ '"@tiptap/extension-gapcursor"': '"3.28.0"',
36
+ '"@tiptap/extension-hard-break"': '"3.28.0"',
37
+ '"@tiptap/extension-heading"': '"3.28.0"',
38
+ '"@tiptap/extension-horizontal-rule"': '"3.28.0"',
39
+ '"@tiptap/extension-image"': '"3.28.0"',
40
+ '"@tiptap/extension-italic"': '"3.28.0"',
41
+ '"@tiptap/extension-link"': '"3.28.0"',
42
+ '"@tiptap/extension-list"': '"3.28.0"',
43
+ '"@tiptap/extension-list-item"': '"3.28.0"',
44
+ '"@tiptap/extension-list-keymap"': '"3.28.0"',
45
+ '"@tiptap/extension-ordered-list"': '"3.28.0"',
46
+ '"@tiptap/extension-paragraph"': '"3.28.0"',
47
+ '"@tiptap/extension-placeholder"': '"3.28.0"',
48
+ '"@tiptap/extension-strike"': '"3.28.0"',
49
+ '"@tiptap/extension-table"': '"3.28.0"',
50
+ '"@tiptap/extension-table-cell"': '"3.28.0"',
51
+ '"@tiptap/extension-table-header"': '"3.28.0"',
52
+ '"@tiptap/extension-table-row"': '"3.28.0"',
53
+ '"@tiptap/extension-task-item"': '"3.28.0"',
54
+ '"@tiptap/extension-task-list"': '"3.28.0"',
55
+ '"@tiptap/extension-text"': '"3.28.0"',
56
+ '"@tiptap/extension-text-style"': '"3.28.0"',
57
+ '"@tiptap/extension-underline"': '"3.28.0"',
58
+ '"@tiptap/extensions"': '"3.28.0"',
59
+ '"@tiptap/pm"': '"3.28.0"',
60
+ '"@tiptap/react"': '"3.28.0"',
61
+ '"@tiptap/starter-kit"': '"3.28.0"',
62
+ };
20
63
  const REACT_ROUTER_BUILD_DEPENDENCIES = [
21
64
  "@react-router/dev",
22
65
  "@react-router/fs-routes",
@@ -1457,6 +1500,12 @@ function postProcessStandalone(name, targetDir, templateName, resolution) {
1457
1500
  nf3: '"0.3.17"',
1458
1501
  };
1459
1502
  }
1503
+ if (templateName && getTemplate(templateName)) {
1504
+ sections.overrides = {
1505
+ ...sections.overrides,
1506
+ ...TIPTAP_WORKSPACE_OVERRIDES,
1507
+ };
1508
+ }
1460
1509
  const localToolkit = localToolkitOverride();
1461
1510
  if (localToolkit) {
1462
1511
  sections.overrides ??= {};
@@ -6,7 +6,7 @@ export { defaultChatFirstCopy } from "./copy.js";
6
6
  export { ChatFirstChatHistory, type ChatFirstChatHistoryProps, } from "./chat-history.js";
7
7
  export { ChatFirstSessionWatchPane } from "./session-watch-pane.js";
8
8
  export { ChatFirstSurfacePanel, type ChatFirstSurfacePanelProps, } from "./surface-panel.js";
9
- export { ChatFirstPrimaryNavigation } from "./primary-nav.js";
9
+ export { ChatFirstPrimaryNavigation, type ChatFirstPrimaryTab, } from "./primary-nav.js";
10
10
  export { emitChatFirstOpenApp, resolveChatFirstAppTarget, CHAT_FIRST_DEFAULT_APP_IDS, type ChatFirstOpenAppDetail, } from "../chat-first.js";
11
11
  export { buildChatFirstAppCreationPrompt, titleFromChatFirstAppPrompt, type ChatFirstAppCreationPromptInput, type ChatFirstAppCreationResource, type ChatFirstAppCreationVaultAccessMode, } from "../../shared/chat-first-app-creation.js";
12
12
  export { ChatFirstSurfaceContent, ChatFirstSurfaceTabs, } from "./surface-tabs.js";
@@ -6,7 +6,7 @@ export { defaultChatFirstCopy } from "./copy.js";
6
6
  export { ChatFirstChatHistory, } from "./chat-history.js";
7
7
  export { ChatFirstSessionWatchPane } from "./session-watch-pane.js";
8
8
  export { ChatFirstSurfacePanel, } from "./surface-panel.js";
9
- export { ChatFirstPrimaryNavigation } from "./primary-nav.js";
9
+ export { ChatFirstPrimaryNavigation, } from "./primary-nav.js";
10
10
  export { emitChatFirstOpenApp, resolveChatFirstAppTarget, CHAT_FIRST_DEFAULT_APP_IDS, } from "../chat-first.js";
11
11
  export { buildChatFirstAppCreationPrompt, titleFromChatFirstAppPrompt, } from "../../shared/chat-first-app-creation.js";
12
12
  export { ChatFirstSurfaceContent, ChatFirstSurfaceTabs, } from "./surface-tabs.js";
@@ -1,8 +1,10 @@
1
1
  import type { ChatFirstCopy } from "./types.js";
2
- export declare function ChatFirstPrimaryNavigation({ onNewChat, onOpenIntegrations, onOpenScheduled, onSearch, copy, }: {
2
+ export type ChatFirstPrimaryTab = "new-chat" | "integrations" | "scheduled";
3
+ export declare function ChatFirstPrimaryNavigation({ onNewChat, onOpenIntegrations, onOpenScheduled, onSearch, activeTab, copy, }: {
3
4
  onNewChat?: () => void;
4
5
  onOpenIntegrations: () => void;
5
6
  onOpenScheduled: () => void;
6
7
  onSearch?: () => void;
8
+ activeTab?: ChatFirstPrimaryTab;
7
9
  copy?: ChatFirstCopy;
8
10
  }): import("react").JSX.Element;
@@ -1,6 +1,12 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { IconClock, IconPlus, IconPlugConnected, IconSearch, } from "@tabler/icons-react";
3
3
  import { defaultChatFirstCopy } from "./copy.js";
4
- export function ChatFirstPrimaryNavigation({ onNewChat, onOpenIntegrations, onOpenScheduled, onSearch, copy = defaultChatFirstCopy, }) {
5
- return (_jsxs(_Fragment, { children: [onNewChat ? (_jsxs("button", { type: "button", className: "flex h-8 w-full items-center gap-2 rounded-md px-2 text-[13px] font-medium text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: onNewChat, children: [_jsx(IconPlus, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { children: copy("newChat") })] })) : null, _jsxs("button", { type: "button", className: "flex h-8 w-full items-center gap-2 rounded-md px-2 text-[13px] font-medium text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: onOpenIntegrations, children: [_jsx(IconPlugConnected, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { children: copy("integrations") })] }), _jsxs("button", { type: "button", className: "flex h-8 w-full items-center gap-2 rounded-md px-2 text-[13px] font-medium text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: onOpenScheduled, children: [_jsx(IconClock, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { children: copy("scheduled") })] }), onSearch ? (_jsxs("button", { type: "button", className: "flex h-8 w-full items-center gap-2 rounded-md px-2 text-[13px] font-medium text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: onSearch, children: [_jsx(IconSearch, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { children: copy("search") })] })) : null] }));
4
+ export function ChatFirstPrimaryNavigation({ onNewChat, onOpenIntegrations, onOpenScheduled, onSearch, activeTab, copy = defaultChatFirstCopy, }) {
5
+ const tabClassName = (tab) => `flex h-8 w-full items-center gap-2 rounded-md px-2 text-[13px] font-medium transition-[background-color,color] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${activeTab === tab
6
+ ? "bg-sidebar-accent text-sidebar-accent-foreground"
7
+ : "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"}`;
8
+ const renderTab = (tab, content, onSelect) => (_jsx("button", { type: "button", role: "tab", "aria-selected": activeTab === tab, className: tabClassName(tab), onClick: onSelect, children: content }));
9
+ return (_jsxs("div", { children: [_jsxs("div", { role: "tablist", "aria-label": "Primary navigation", className: "grid gap-px", children: [onNewChat
10
+ ? renderTab("new-chat", _jsxs(_Fragment, { children: [_jsx(IconPlus, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { children: copy("newChat") })] }), onNewChat)
11
+ : null, renderTab("integrations", _jsxs(_Fragment, { children: [_jsx(IconPlugConnected, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { children: copy("integrations") })] }), onOpenIntegrations), renderTab("scheduled", _jsxs(_Fragment, { children: [_jsx(IconClock, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { children: copy("scheduled") })] }), onOpenScheduled)] }), onSearch ? (_jsxs("button", { type: "button", className: "mt-px flex h-8 w-full items-center gap-2 rounded-md px-2 text-[13px] font-medium text-sidebar-foreground/80 transition-[background-color,color] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: onSearch, children: [_jsx(IconSearch, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { children: copy("search") })] })) : null] }));
6
12
  }
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -147,6 +147,10 @@ export declare function emitSingleTemplateNetlifyKeepWarmFunction(projectCwd: st
147
147
  * Emit the durable recurring-job trigger. Netlify's scheduled function only
148
148
  * hands off work; the existing `-background` function owns the long sweep so
149
149
  * a model run is not constrained by the synchronous scheduled-function wall.
150
+ *
151
+ * The entry imports `node:crypto`, so `includedFiles: ["**"]` must stay: the
152
+ * deploy packager only accepts an omitted `includedFiles` for scheduled
153
+ * functions whose entry file has no import/require edge at all.
150
154
  */
151
155
  export declare function emitSingleTemplateNetlifyRecurringJobsFunction(projectCwd: string): void;
152
156
  /**
@@ -2438,6 +2438,10 @@ export const config = {
2438
2438
  * Emit the durable recurring-job trigger. Netlify's scheduled function only
2439
2439
  * hands off work; the existing `-background` function owns the long sweep so
2440
2440
  * a model run is not constrained by the synchronous scheduled-function wall.
2441
+ *
2442
+ * The entry imports `node:crypto`, so `includedFiles: ["**"]` must stay: the
2443
+ * deploy packager only accepts an omitted `includedFiles` for scheduled
2444
+ * functions whose entry file has no import/require edge at all.
2441
2445
  */
2442
2446
  export function emitSingleTemplateNetlifyRecurringJobsFunction(projectCwd) {
2443
2447
  if (!isRecurringJobsDeployEnabled())
@@ -2505,6 +2509,7 @@ export const config = {
2505
2509
  generator: "agent-native build",
2506
2510
  schedule: "* * * * *",
2507
2511
  nodeBundler: "none",
2512
+ includedFiles: ["**"],
2508
2513
  };
2509
2514
  `;
2510
2515
  fs.writeFileSync(path.join(dest, `${functionName}.mjs`), entry);
@@ -482,6 +482,10 @@ function copyNetlifyFunctionIntoWorkspace(workspaceRoot, app, workspaceApps, sta
482
482
  * Emit the per-app Netlify Scheduled Function that hands one recurring-job
483
483
  * sweep to that app's durable background worker. The worker owns the actual
484
484
  * scan so scheduled-function timeouts cannot strand the automation runner.
485
+ *
486
+ * The entry imports `node:crypto`, so `includedFiles: ["**"]` must stay: the
487
+ * deploy packager only accepts an omitted `includedFiles` for scheduled
488
+ * functions whose entry file has no import/require edge at all.
485
489
  */
486
490
  function emitNetlifyRecurringJobsFunction(workspaceRoot, app) {
487
491
  const functionName = `${app}-agent-recurring-jobs`;
@@ -545,6 +549,7 @@ export const config = {
545
549
  generator: "agent-native workspace deploy",
546
550
  schedule: "* * * * *",
547
551
  nodeBundler: "none",
552
+ includedFiles: ["**"],
548
553
  };
549
554
  `;
550
555
  fs.writeFileSync(path.join(dest, `${functionName}.mjs`), entry);