@agent-native/core 0.149.4 → 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.
- package/corpus/README.md +1 -1
- package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +23 -4
- package/corpus/templates/slides/actions/get-deck.ts +25 -9
- package/corpus/templates/slides/actions/patch-deck.ts +51 -0
- package/corpus/templates/slides/actions/update-slide.ts +144 -14
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +12 -4
- package/corpus/templates/slides/changelog/2026-08-11-pdf-requests-to-restyle-and-preserve-a-source-deck-now-impor.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-slide-edits-preserve-their-layout-without-introducing-hidden.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-slides-reliably-imports-uploaded-pdfs-and-presentations.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-slides-support-atomic-code-style-html-patches-without-regene.md +6 -0
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/server/lib/slide-content-patch.ts +355 -0
- package/corpus/templates/slides/server/lib/uploaded-reference-storage.ts +28 -9
- package/corpus/templates/slides/server/plugins/agent-chat.ts +12 -3
- package/dist/cli/create.js +49 -0
- package/dist/client/chat-first/index.d.ts +1 -1
- package/dist/client/chat-first/index.js +1 -1
- package/dist/client/chat-first/primary-nav.d.ts +3 -1
- package/dist/client/chat-first/primary-nav.js +8 -2
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/private-blob/registry.js +67 -4
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/templates/default/pnpm-workspace.yaml +44 -0
- package/dist/templates/workspace-root/pnpm-workspace.yaml +44 -0
- package/package.json +1 -1
- package/src/templates/default/pnpm-workspace.yaml +44 -0
- 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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
descriptor
|
|
97
|
-
typeof descriptor
|
|
98
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
package/dist/cli/create.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
states: {
|
|
66
67
|
clientId: number;
|
|
67
68
|
state: string;
|
|
68
69
|
}[];
|
|
69
|
-
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
+
error?: undefined;
|
|
80
81
|
users: {
|
|
81
82
|
clientId: number;
|
|
82
83
|
lastSeen: number;
|
|
83
84
|
}[];
|
|
84
|
-
error?: undefined;
|
|
85
85
|
}>>;
|
|
@@ -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;
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
|
|
14
14
|
count: number;
|
|
15
15
|
updated?: undefined;
|
|
16
|
-
error?: undefined;
|
|
17
16
|
ok?: undefined;
|
|
17
|
+
error?: undefined;
|
|
18
18
|
} | {
|
|
19
19
|
updated: number;
|
|
20
|
-
error?: undefined;
|
|
21
20
|
ok?: undefined;
|
|
21
|
+
error?: undefined;
|
|
22
22
|
count?: undefined;
|
|
23
23
|
} | {
|
|
24
24
|
updated?: undefined;
|
|
@@ -27,7 +27,7 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
|
|
|
27
27
|
count?: undefined;
|
|
28
28
|
} | {
|
|
29
29
|
updated?: undefined;
|
|
30
|
-
error?: undefined;
|
|
31
30
|
ok: boolean;
|
|
31
|
+
error?: undefined;
|
|
32
32
|
count?: undefined;
|
|
33
33
|
}>>;
|