@kywk/open-slide-core 2.3.1 → 2.3.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.
Files changed (50) hide show
  1. package/dist/{build-CZ6Qtrng.js → build-46dO14h1.js} +1 -1
  2. package/dist/cli/bin.js +4 -4
  3. package/dist/{config-CpmGqpQi.d.ts → config-CDrYgaEj.d.ts} +6 -2
  4. package/dist/{config-B5LArqpp.js → config-KyYvWqq-.js} +2061 -355
  5. package/dist/{dev-CCgdakbZ.js → dev-C0n-o0-c.js} +2 -2
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +73 -32
  8. package/dist/locale/index.d.ts +1 -1
  9. package/dist/locale/index.js +1 -1
  10. package/dist/{preview-AwAqvG17.js → preview-CDWAGSfB.js} +1 -1
  11. package/dist/{types-CvAqFqsY.d.ts → types-DFK02XVv.d.ts} +110 -0
  12. package/dist/{update-C_RlnKu4.js → update-CqZqQRQk.js} +128 -7
  13. package/dist/vite/index.d.ts +1 -1
  14. package/dist/vite/index.js +1 -1
  15. package/dist/{zh-tw-B7_Oqv2q.js → zh-tw-B1pkKQsc.js} +535 -0
  16. package/package.json +9 -2
  17. package/src/agent/prompt.ts +141 -0
  18. package/src/agent/provider.ts +123 -0
  19. package/src/agent/types.ts +151 -0
  20. package/src/app/components/agent/agent-dock.tsx +35 -0
  21. package/src/app/components/agent/agent-markdown.tsx +45 -0
  22. package/src/app/components/agent/agent-panel.tsx +692 -0
  23. package/src/app/components/agent/agent-presence.tsx +74 -0
  24. package/src/app/components/agent/composer.tsx +120 -0
  25. package/src/app/components/agent/connect-view.tsx +184 -0
  26. package/src/app/components/agent/history-view.tsx +135 -0
  27. package/src/app/components/agent/provider-logo.tsx +34 -0
  28. package/src/app/components/agent/transcript.tsx +609 -0
  29. package/src/app/components/command/slide-command-menu.tsx +10 -0
  30. package/src/app/components/inline-svg.tsx +52 -21
  31. package/src/app/components/inspector/comment-layer.tsx +20 -1
  32. package/src/app/components/inspector/image-crop-dialog.tsx +1 -5
  33. package/src/app/components/inspector/inspector-panel.tsx +1 -1
  34. package/src/app/components/mermaid.tsx +42 -11
  35. package/src/app/components/panel/editor-sidebar.tsx +85 -16
  36. package/src/app/components/panel/panel-shell.tsx +2 -2
  37. package/src/app/components/panel/resize-handle.tsx +129 -0
  38. package/src/app/lib/agent/use-agent.ts +543 -0
  39. package/src/app/lib/use-auto-grow.ts +15 -0
  40. package/src/app/lib/utils.ts +4 -0
  41. package/src/app/routes/home.tsx +43 -7
  42. package/src/app/routes/slide.tsx +103 -104
  43. package/src/app/styles.css +170 -0
  44. package/src/editing/placeholder-id.ts +1 -0
  45. package/src/locale/en.ts +106 -0
  46. package/src/locale/es.ts +108 -0
  47. package/src/locale/ja.ts +99 -0
  48. package/src/locale/types.ts +109 -0
  49. package/src/locale/zh-cn.ts +96 -0
  50. package/src/locale/zh-tw.ts +96 -0
@@ -1,18 +1,1987 @@
1
1
  import { n as defaultDesign } from "./design-CoZ0cWDN.js";
2
- import { C as reorderDefaultExportPagesInSource, D as updateMetaTitleInSource, E as rmSlideDir, O as validateSlideName, S as removePageFromDefaultExportInSource, T as resolveSlideEntry, _ as SLIDE_ID_RE, a as readBody, b as duplicateSlideDir, c as FOLDER_ID_RE, d as readManifest, f as validateIcon, g as shortId, h as writeManifest, i as makeContext, j as ownPackageName, k as validateMutationRequest, l as foldersManifestPath, m as validateReorder, n as registerUpdateRoutes, o as readSlideSource, p as validateName, r as json, s as resolveSlideEntryPath, u as newFolderId, v as duplicateNotesElementInSource, w as reorderNotesArrayInSource, x as removeNotesElementInSource, y as duplicatePageInDefaultExportInSource } from "./update-C_RlnKu4.js";
2
+ import { A as reorderNotesArrayInSource, C as duplicatePageInDefaultExportInSource, D as removePageFromDefaultExportInSource, E as removeNotesElementInSource, F as validateSlideName, I as isLoopbackRequest, L as validateMutationRequest, M as rmSlideDir, N as slugifyTitle, O as renameSlideDir, P as updateMetaTitleInSource, S as duplicateNotesElementInSource, T as readMetaTitleInSource, _ as writeManifest, a as json, b as SLIDE_ID_RE, c as readSlideSource, d as foldersManifestPath, f as newFolderId, g as validateReorder, h as validateName, i as BodyTooLargeError, j as resolveSlideEntry, k as reorderDefaultExportPagesInSource, l as resolveSlideEntryPath, m as validateIcon, n as registerUpdateRoutes, o as makeContext, p as readManifest, r as runCommand, s as readBody, t as detectPackageManager, u as FOLDER_ID_RE, v as shortId, w as duplicateSlideDir, x as createBlankSlideDir, z as ownPackageName } from "./update-CqZqQRQk.js";
3
3
  import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { existsSync, readFileSync, rmSync } from "node:fs";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { existsSync, readFileSync, rmSync, statSync } from "node:fs";
7
+ import { spawn } from "node:child_process";
7
8
  import fs from "node:fs/promises";
8
9
  import { parse } from "@babel/parser";
9
- import { randomUUID } from "node:crypto";
10
+ import { randomBytes, randomUUID } from "node:crypto";
10
11
  import tailwindcss from "@tailwindcss/vite";
11
12
  import react from "@vitejs/plugin-react";
12
- import * as t from "@babel/types";
13
- import { isJSXElement, isJSXFragment } from "@babel/types";
13
+ import os from "node:os";
14
14
  import fg from "fast-glob";
15
15
  import { loadConfigFromFile, normalizePath } from "vite";
16
+ import * as t from "@babel/types";
17
+ import { isJSXElement, isJSXFragment } from "@babel/types";
18
+ //#region src/editing/placeholder-id.ts
19
+ const UNTITLED_SLIDE_ID_RE = /^untitled(-\d+)?$/;
20
+ //#endregion
21
+ //#region src/agent/provider.ts
22
+ var ProviderUnavailableError = class extends Error {
23
+ constructor(provider) {
24
+ super(`${provider.name} is not connected: install ${provider.sdkPackage} in this workspace to use it from the agent panel.`);
25
+ this.name = "ProviderUnavailableError";
26
+ }
27
+ };
28
+ const DETAIL_MAX = 400;
29
+ function clipDetail(value) {
30
+ if (value === void 0 || value === null) return null;
31
+ const text = typeof value === "string" ? value : safeJson(value);
32
+ if (!text) return null;
33
+ const oneLine = text.replace(/\s+/g, " ").trim();
34
+ return oneLine.length > DETAIL_MAX ? `${oneLine.slice(0, 399)}…` : oneLine;
35
+ }
36
+ function safeJson(value) {
37
+ try {
38
+ return JSON.stringify(value);
39
+ } catch {
40
+ return String(value);
41
+ }
42
+ }
43
+ function isInsideRoots(cwd, file, roots) {
44
+ if (typeof file !== "string" || !file) return false;
45
+ const resolved = path.resolve(cwd, file);
46
+ return roots.some((root) => {
47
+ const rel = path.relative(root, resolved);
48
+ return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
49
+ });
50
+ }
51
+ function relativeToCwd(cwd, file) {
52
+ if (typeof file !== "string" || !file) return null;
53
+ const normalizedCwd = cwd.replace(/[\\/]+$/, "");
54
+ if (file.startsWith(normalizedCwd)) return file.slice(normalizedCwd.length).replace(/^[\\/]+/, "") || ".";
55
+ return file;
56
+ }
57
+ function parseQuestions(input) {
58
+ const raw = Array.isArray(input.questions) ? input.questions : [];
59
+ const questions = [];
60
+ for (const q of raw) {
61
+ if (!q || typeof q !== "object") continue;
62
+ const r = q;
63
+ const options = Array.isArray(r.options) ? r.options.map((o) => {
64
+ if (!o || typeof o !== "object") return null;
65
+ const opt = o;
66
+ if (typeof opt.label !== "string") return null;
67
+ return {
68
+ label: opt.label,
69
+ description: typeof opt.description === "string" ? opt.description : void 0
70
+ };
71
+ }).filter((o) => o !== null) : [];
72
+ if (typeof r.question !== "string") continue;
73
+ questions.push({
74
+ header: typeof r.header === "string" ? r.header : "",
75
+ question: r.question,
76
+ options,
77
+ multiSelect: r.multiSelect === true
78
+ });
79
+ }
80
+ return questions;
81
+ }
82
+ //#endregion
83
+ //#region src/agent/prompt.ts
84
+ const PAGE_BY_PAGE = "Write a new deck page by page so the user can follow along on the canvas: first replace the file with the imports, meta (keeping its existing createdAt), design and the cover page only, then add each remaining page with its own separate edit, keeping the file compiling after every write.";
85
+ const DECK_SLUG = "This deck's folder still has a placeholder name. Never rename or move the folder yourself: once the deck has content, end your final reply with a slug for it on a line of its own as <deck-slug>short-kebab-case-english-slug</deck-slug>, and open-slide renames the folder from it.";
86
+ function systemPromptAppend(askInstruction) {
87
+ return [
88
+ "You are running inside the open-slide dev server, driven from its in-app agent panel in the browser rather than a terminal.",
89
+ "The workspace is an open-slide project: decks live under slides/<id>/index.tsx and the dev server hot-reloads every edit, so the user sees changes immediately.",
90
+ "Use the project skills: create-slide to draft a new deck, apply-comments to resolve @slide-comment markers left by the inspector, create-theme for themes, and slide-authoring as the technical reference before writing a page.",
91
+ PAGE_BY_PAGE,
92
+ "Keep replies short and concrete: the panel is narrow. Prefer doing the work over describing it, and finish with a one-line summary of what changed.",
93
+ askInstruction
94
+ ].join(" ");
95
+ }
96
+ function buildTurnPrompt(text, context) {
97
+ const lines = contextLines(context);
98
+ if (lines.length === 0) return text;
99
+ return `${text}\n\n<open-slide-context>\n${lines.join("\n")}\n</open-slide-context>`;
100
+ }
101
+ const escapeTag = (s) => s.replace(/</g, "&lt;");
102
+ function contextLines(context) {
103
+ if (!context?.slideId) return [];
104
+ const file = `slides/${context.slideId}/index.tsx`;
105
+ const lines = [];
106
+ if (context.newDeck) {
107
+ lines.push(`${file} is a blank deck created for this request. Draft the deck the user describes into it with the create-slide skill: replace the placeholder page, keep the slide id, and do not create another folder.`, "Ask the create-slide scoping questions in a single round, only for what the request leaves unclear; skip any it already answers.", PAGE_BY_PAGE, DECK_SLUG);
108
+ return lines;
109
+ }
110
+ lines.push(`The user is viewing slide "${context.slideId}": ${file}.`);
111
+ if (UNTITLED_SLIDE_ID_RE.test(context.slideId)) lines.push(DECK_SLUG);
112
+ if (context.slideTitle) lines.push(`<slide-title>${escapeTag(context.slideTitle)}</slide-title>`);
113
+ if (context.pageNumber && context.totalPages) lines.push(`They are on page ${context.pageNumber} of ${context.totalPages} (0-based index ${context.pageNumber - 1} in the default export array).`);
114
+ if (context.selection) {
115
+ const { line, column, tagName, text } = context.selection;
116
+ lines.push(`They have selected a <${escapeTag(tagName)}> element at ${file}:${line}:${column}.${text ? " Its text is in <selected-text>." : ""} "This element" refers to it.`);
117
+ if (text) lines.push(`<selected-text>${escapeTag(text)}</selected-text>`);
118
+ }
119
+ if (context.pendingComments) lines.push(`There are ${context.pendingComments} pending @slide-comment markers in this slide's source.`);
120
+ return lines;
121
+ }
122
+ const DECK_SLUG_RE = /\s*<deck-slug>\s*([^<]*?)\s*<\/deck-slug>\s*/gi;
123
+ function extractDeckSlug(text) {
124
+ let slug = null;
125
+ for (const match of text.matchAll(DECK_SLUG_RE)) slug = match[1] || slug;
126
+ return slug;
127
+ }
128
+ function stripDeckSlug(text) {
129
+ return text.replace(DECK_SLUG_RE, "\n").replace(/\s*<deck-slug>[^<]*$/i, "").trimEnd();
130
+ }
131
+ const QUESTIONS_RE = /<open-slide-questions>([\s\S]*?)<\/open-slide-questions>/i;
132
+ const TEXT_QUESTIONS_INSTRUCTION = "You cannot ask the user anything interactively here, and nothing you print in the middle of a turn can be answered. When you need answers before continuing, stop working and end your turn with a final message holding only an <open-slide-questions> block: a JSON array of at most 4 questions shaped like [{\"header\":\"Pages\",\"question\":\"How many pages?\",\"multiSelect\":false,\"options\":[{\"label\":\"6-10 (Recommended)\",\"description\":\"Standard length\"}]}], each with 2 to 4 options, written in the user's language. The panel shows them as a form and sends the answers back as your next message. Write nothing after the block and do not continue the task in the same turn.";
133
+ function extractQuestionBlock(text) {
134
+ const match = QUESTIONS_RE.exec(text);
135
+ if (!match) return null;
136
+ const body = match[1].trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
137
+ let raw;
138
+ try {
139
+ raw = JSON.parse(body);
140
+ } catch {
141
+ return null;
142
+ }
143
+ const questions = parseQuestions({ questions: Array.isArray(raw) ? raw : raw?.questions });
144
+ if (questions.length === 0) return null;
145
+ return {
146
+ text: text.replace(QUESTIONS_RE, "").trim(),
147
+ questions
148
+ };
149
+ }
150
+ function answersPrompt(questions, answers) {
151
+ if (!answers) return "I skipped your questions. Continue the task with your best judgement and sensible defaults.";
152
+ return `My answers:\n${questions.map((q) => `- ${q.question} ${answers[q.question] || "(no answer)"}`).join("\n")}\n\nContinue the task with these answers.`;
153
+ }
154
+ function editedPrompt(original, text) {
155
+ return `I went back and edited an earlier message of mine. Treat everything after it in this conversation, including your replies, as discarded, and respond to the edited version instead. Files keep whatever changes were already made.\n\n<original-message>\n${escapeTag(original)}\n</original-message>\n\n${text}`;
156
+ }
157
+ function rewoundPrompt(text) {
158
+ return `${text}\n\n<open-slide-note>I went back and edited this message. Files keep the changes made after it in the discarded part of this conversation, so read the current files before editing them.</open-slide-note>`;
159
+ }
160
+ //#endregion
161
+ //#region src/agent/detect.ts
162
+ const IS_WINDOWS = process.platform === "win32";
163
+ const VERSION_TIMEOUT_MS = 5e3;
164
+ function findOnPath(command, env = process.env) {
165
+ const dirs = (env.PATH ?? env.Path ?? "").split(path.delimiter).filter(Boolean);
166
+ const exts = IS_WINDOWS ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
167
+ for (const dir of dirs) for (const ext of exts) {
168
+ const candidate = path.join(dir, command + ext.toLowerCase());
169
+ try {
170
+ if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) return candidate;
171
+ } catch {}
172
+ }
173
+ return null;
174
+ }
175
+ function findSpawnableCli(command, env = process.env) {
176
+ const found = findOnPath(command, env);
177
+ if (!found || !IS_WINDOWS) return found;
178
+ return found.toLowerCase().endsWith(".exe") ? found : null;
179
+ }
180
+ const versionCache = /* @__PURE__ */ new Map();
181
+ function commandVersion(file) {
182
+ let cached = versionCache.get(file);
183
+ if (!cached) {
184
+ cached = readVersion(file);
185
+ versionCache.set(file, cached);
186
+ }
187
+ return cached;
188
+ }
189
+ function readVersion(file) {
190
+ return new Promise((resolve) => {
191
+ let out = "";
192
+ let child;
193
+ try {
194
+ child = spawn(file, ["--version"], {
195
+ stdio: [
196
+ "ignore",
197
+ "pipe",
198
+ "pipe"
199
+ ],
200
+ shell: IS_WINDOWS,
201
+ windowsHide: true
202
+ });
203
+ } catch {
204
+ resolve(null);
205
+ return;
206
+ }
207
+ const timer = setTimeout(() => {
208
+ child.kill();
209
+ resolve(null);
210
+ }, VERSION_TIMEOUT_MS);
211
+ child.stdout?.on("data", (c) => {
212
+ out += c.toString("utf8");
213
+ });
214
+ child.stderr?.on("data", (c) => {
215
+ out += c.toString("utf8");
216
+ });
217
+ child.on("error", () => {
218
+ clearTimeout(timer);
219
+ resolve(null);
220
+ });
221
+ child.on("close", () => {
222
+ clearTimeout(timer);
223
+ resolve(parseVersion(out));
224
+ });
225
+ });
226
+ }
227
+ function parseVersion(output) {
228
+ const m = /(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/.exec(output);
229
+ return m ? m[1] : null;
230
+ }
231
+ function entryFromPackageJson(pkg) {
232
+ const root = pickExport(pkg.exports);
233
+ if (root) return root;
234
+ return pkg.module ?? pkg.main ?? null;
235
+ }
236
+ function pickExport(exportsField) {
237
+ if (!exportsField) return null;
238
+ if (typeof exportsField === "string") return exportsField;
239
+ if (typeof exportsField !== "object") return null;
240
+ const record = exportsField;
241
+ return pickCondition(Object.keys(record).some((k) => k.startsWith(".")) ? record["."] : exportsField);
242
+ }
243
+ function pickCondition(target) {
244
+ if (!target) return null;
245
+ if (typeof target === "string") return target;
246
+ if (Array.isArray(target)) {
247
+ for (const t of target) {
248
+ const hit = pickCondition(t);
249
+ if (hit) return hit;
250
+ }
251
+ return null;
252
+ }
253
+ if (typeof target !== "object") return null;
254
+ const record = target;
255
+ for (const key of [
256
+ "import",
257
+ "node",
258
+ "default",
259
+ "require"
260
+ ]) if (key in record) {
261
+ const hit = pickCondition(record[key]);
262
+ if (hit) return hit;
263
+ }
264
+ return null;
265
+ }
266
+ function resolveWorkspacePackage(userCwd, name) {
267
+ let dir = path.resolve(userCwd);
268
+ while (true) {
269
+ const pkgJsonPath = path.join(dir, "node_modules", name, "package.json");
270
+ if (existsSync(pkgJsonPath)) {
271
+ let pkg;
272
+ try {
273
+ pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
274
+ } catch {
275
+ return null;
276
+ }
277
+ const entry = entryFromPackageJson(pkg);
278
+ if (!entry) return null;
279
+ const pkgDir = path.dirname(pkgJsonPath);
280
+ return {
281
+ dir: pkgDir,
282
+ version: pkg.version ?? "0.0.0",
283
+ entry: path.resolve(pkgDir, entry)
284
+ };
285
+ }
286
+ const parent = path.dirname(dir);
287
+ if (parent === dir) return null;
288
+ dir = parent;
289
+ }
290
+ }
291
+ async function importWorkspacePackage(userCwd, name) {
292
+ const resolved = resolveWorkspacePackage(userCwd, name);
293
+ if (!resolved) return null;
294
+ return await import(pathToFileURL(resolved.entry).href);
295
+ }
296
+ //#endregion
297
+ //#region src/agent/providers/claude.ts
298
+ const CLAUDE_SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
299
+ const CLAUDE_EFFORTS = [
300
+ "low",
301
+ "medium",
302
+ "high",
303
+ "xhigh",
304
+ "max"
305
+ ];
306
+ const CLAUDE_MODELS = [
307
+ {
308
+ id: "opus",
309
+ label: "Opus",
310
+ efforts: CLAUDE_EFFORTS
311
+ },
312
+ {
313
+ id: "sonnet",
314
+ label: "Sonnet",
315
+ efforts: CLAUDE_EFFORTS
316
+ },
317
+ {
318
+ id: "haiku",
319
+ label: "Haiku",
320
+ efforts: []
321
+ }
322
+ ];
323
+ const EDIT_TOOLS = /* @__PURE__ */ new Set([
324
+ "Edit",
325
+ "MultiEdit",
326
+ "Write",
327
+ "NotebookEdit"
328
+ ]);
329
+ function claudeEffort(effort) {
330
+ return effort && effort !== "minimal" ? effort : void 0;
331
+ }
332
+ function describeClaudeTool(name, input, cwd) {
333
+ const file = relativeToCwd(cwd, input.file_path ?? input.path ?? input.notebook_path);
334
+ switch (name) {
335
+ case "Read": return {
336
+ title: "Read",
337
+ detail: file
338
+ };
339
+ case "Edit":
340
+ case "MultiEdit":
341
+ case "Write":
342
+ case "NotebookEdit": return {
343
+ title: name === "Write" ? "Write" : "Edit",
344
+ detail: file
345
+ };
346
+ case "Bash": return {
347
+ title: "Run command",
348
+ detail: clipDetail(input.command)
349
+ };
350
+ case "Glob":
351
+ case "Grep": return {
352
+ title: "Search",
353
+ detail: clipDetail(input.pattern)
354
+ };
355
+ case "WebFetch": return {
356
+ title: "Fetch",
357
+ detail: clipDetail(input.url)
358
+ };
359
+ case "WebSearch": return {
360
+ title: "Web search",
361
+ detail: clipDetail(input.query)
362
+ };
363
+ case "Skill": return {
364
+ title: "Skill",
365
+ detail: clipDetail(input.skill ?? input.name)
366
+ };
367
+ case "Task": return {
368
+ title: "Subagent",
369
+ detail: clipDetail(input.description)
370
+ };
371
+ case "AskUserQuestion": return {
372
+ title: "Question",
373
+ detail: null
374
+ };
375
+ case "TodoWrite": return {
376
+ title: "Plan",
377
+ detail: null
378
+ };
379
+ default: return {
380
+ title: name,
381
+ detail: clipDetail(input)
382
+ };
383
+ }
384
+ }
385
+ function createClaudeMapper(emit, cwd) {
386
+ let counter = 0;
387
+ const nextId = () => `t-${Date.now().toString(36)}-${(counter++).toString(36)}`;
388
+ const textByIndex = /* @__PURE__ */ new Map();
389
+ const openTextIds = [];
390
+ let resultSeen = false;
391
+ const handleBlocks = (blocks) => {
392
+ for (const block of blocks) if (block.type === "text") {
393
+ const { text } = block;
394
+ const id = openTextIds.shift();
395
+ if (id) emit({
396
+ type: "text-end",
397
+ id,
398
+ text
399
+ });
400
+ else if (text.trim()) {
401
+ const fresh = nextId();
402
+ emit({
403
+ type: "text-start",
404
+ id: fresh
405
+ });
406
+ emit({
407
+ type: "text-end",
408
+ id: fresh,
409
+ text
410
+ });
411
+ }
412
+ } else if (block.type === "tool_use") {
413
+ const { title, detail } = describeClaudeTool(block.name, block.input ?? {}, cwd);
414
+ emit({
415
+ type: "tool-start",
416
+ id: block.id,
417
+ name: block.name,
418
+ title,
419
+ detail
420
+ });
421
+ }
422
+ textByIndex.clear();
423
+ openTextIds.length = 0;
424
+ };
425
+ return {
426
+ get resultSeen() {
427
+ return resultSeen;
428
+ },
429
+ handle(msg) {
430
+ if (!msg || typeof msg !== "object") return;
431
+ if (msg.type === "system" && msg.subtype === "init") {
432
+ emit({
433
+ type: "session-id",
434
+ id: msg.session_id
435
+ });
436
+ return;
437
+ }
438
+ if (msg.type === "stream_event") {
439
+ if (msg.parent_tool_use_id) return;
440
+ const ev = msg.event;
441
+ if (ev.type === "content_block_start") {
442
+ if (ev.content_block.type === "text") {
443
+ const id = nextId();
444
+ textByIndex.set(ev.index, id);
445
+ openTextIds.push(id);
446
+ emit({
447
+ type: "text-start",
448
+ id
449
+ });
450
+ }
451
+ } else if (ev.type === "content_block_delta") {
452
+ const id = textByIndex.get(ev.index);
453
+ if (id && ev.delta.type === "text_delta" && ev.delta.text) emit({
454
+ type: "text-delta",
455
+ id,
456
+ text: ev.delta.text
457
+ });
458
+ }
459
+ return;
460
+ }
461
+ if (msg.type === "assistant") {
462
+ if (msg.parent_tool_use_id) return;
463
+ handleBlocks(msg.message?.content ?? []);
464
+ if (msg.uuid) emit({
465
+ type: "checkpoint",
466
+ id: msg.uuid
467
+ });
468
+ return;
469
+ }
470
+ if (msg.type === "user") {
471
+ if (msg.parent_tool_use_id) return;
472
+ if (msg.uuid) emit({
473
+ type: "checkpoint",
474
+ id: msg.uuid
475
+ });
476
+ const content = msg.message?.content;
477
+ if (!Array.isArray(content)) return;
478
+ for (const block of content) if (block.type === "tool_result") emit({
479
+ type: "tool-end",
480
+ id: block.tool_use_id,
481
+ ok: !block.is_error
482
+ });
483
+ return;
484
+ }
485
+ if (msg.type === "result") {
486
+ resultSeen = true;
487
+ if (msg.session_id) emit({
488
+ type: "session-id",
489
+ id: msg.session_id
490
+ });
491
+ if (msg.is_error || msg.subtype && msg.subtype !== "success") emit({
492
+ type: "error",
493
+ text: (Array.isArray(msg.errors) && msg.errors.length ? msg.errors.join("\n") : null) ?? (typeof msg.result === "string" && msg.result ? msg.result : null) ?? `Claude Code stopped: ${msg.subtype ?? "error"}`
494
+ });
495
+ emit({
496
+ type: "result",
497
+ durationMs: typeof msg.duration_ms === "number" ? msg.duration_ms : null,
498
+ costUsd: typeof msg.total_cost_usd === "number" ? msg.total_cost_usd : null
499
+ });
500
+ }
501
+ }
502
+ };
503
+ }
504
+ function createClaudeProvider() {
505
+ return {
506
+ id: "claude",
507
+ name: "Claude Code",
508
+ sdkPackage: CLAUDE_SDK_PACKAGE,
509
+ cliCommand: "claude",
510
+ askInstruction: "Ask the user questions with the AskUserQuestion tool, never as plain text.",
511
+ canRewind: true,
512
+ async models() {
513
+ return CLAUDE_MODELS;
514
+ },
515
+ async run(opts) {
516
+ const sdk = await importWorkspacePackage(opts.cwd, CLAUDE_SDK_PACKAGE);
517
+ if (!sdk || typeof sdk.query !== "function") throw new ProviderUnavailableError(this);
518
+ const mapper = createClaudeMapper(opts.emit, opts.cwd);
519
+ if (opts.signal.aborted) return;
520
+ const abortController = new AbortController();
521
+ opts.signal.addEventListener("abort", () => abortController.abort(), { once: true });
522
+ const canUseTool = async (toolName, input) => {
523
+ if (toolName === "AskUserQuestion") {
524
+ const questions = parseQuestions(input);
525
+ const answers = await opts.askUser(questions);
526
+ if (!answers) return {
527
+ behavior: "deny",
528
+ message: "The user dismissed the question."
529
+ };
530
+ return {
531
+ behavior: "allow",
532
+ updatedInput: {
533
+ ...input,
534
+ answers
535
+ }
536
+ };
537
+ }
538
+ const target = input.file_path ?? input.notebook_path;
539
+ if (EDIT_TOOLS.has(toolName) && isInsideRoots(opts.cwd, target, opts.writableRoots)) return {
540
+ behavior: "allow",
541
+ updatedInput: input
542
+ };
543
+ const { title, detail } = describeClaudeTool(toolName, input, opts.cwd);
544
+ return await opts.requestPermission({
545
+ tool: toolName,
546
+ title,
547
+ detail
548
+ }) ? {
549
+ behavior: "allow",
550
+ updatedInput: input
551
+ } : {
552
+ behavior: "deny",
553
+ message: "The user declined this action in the open-slide agent panel."
554
+ };
555
+ };
556
+ const localCli = findSpawnableCli("claude");
557
+ const effort = claudeEffort(opts.effort);
558
+ let stderr = "";
559
+ const stream = sdk.query({
560
+ prompt: opts.prompt,
561
+ options: {
562
+ cwd: opts.cwd,
563
+ permissionMode: "default",
564
+ settingSources: [
565
+ "user",
566
+ "project",
567
+ "local"
568
+ ],
569
+ systemPrompt: {
570
+ type: "preset",
571
+ preset: "claude_code",
572
+ append: opts.systemPromptAppend
573
+ },
574
+ includePartialMessages: true,
575
+ abortController,
576
+ canUseTool,
577
+ ...opts.resume ? { resume: opts.resume } : {},
578
+ ...opts.resume && opts.resumeAt ? { resumeSessionAt: opts.resumeAt } : {},
579
+ ...opts.model ? { model: opts.model } : {},
580
+ ...effort ? { effort } : {},
581
+ ...localCli ? { pathToClaudeCodeExecutable: localCli } : {},
582
+ stderr: (data) => {
583
+ stderr = (stderr + data).slice(-2e3);
584
+ }
585
+ }
586
+ });
587
+ try {
588
+ for await (const message of stream) mapper.handle(message);
589
+ } catch (err) {
590
+ if (opts.signal.aborted || mapper.resultSeen) return;
591
+ const base = err instanceof Error ? err.message : String(err);
592
+ const tail = stderr.trim();
593
+ throw new Error(tail && !base.includes(tail) ? `${base}\n${tail}` : base);
594
+ }
595
+ }
596
+ };
597
+ }
598
+ //#endregion
599
+ //#region src/agent/types.ts
600
+ const AGENT_PROVIDER_IDS = ["claude", "codex"];
601
+ const AGENT_EFFORTS = [
602
+ "minimal",
603
+ "low",
604
+ "medium",
605
+ "high",
606
+ "xhigh",
607
+ "max"
608
+ ];
609
+ function isAgentProviderId(value) {
610
+ return typeof value === "string" && AGENT_PROVIDER_IDS.includes(value);
611
+ }
612
+ function isAgentEffort(value) {
613
+ return typeof value === "string" && AGENT_EFFORTS.includes(value);
614
+ }
615
+ function sessionSummary(session) {
616
+ const { providerSessionId: _p, providerCheckpoint: _c, trimmed: _t, items: _i, ...summary } = session;
617
+ return summary;
618
+ }
619
+ //#endregion
620
+ //#region src/agent/providers/codex.ts
621
+ const CODEX_SDK_PACKAGE = "@openai/codex-sdk";
622
+ const CODEX_DEFAULT_MODEL = {
623
+ id: "",
624
+ label: "Default",
625
+ efforts: [
626
+ "low",
627
+ "medium",
628
+ "high"
629
+ ]
630
+ };
631
+ function parseCodexModelsCache(raw) {
632
+ const list = raw?.models;
633
+ if (!Array.isArray(list)) return [];
634
+ const models = [];
635
+ for (const entry of list) {
636
+ if (!entry || typeof entry !== "object") continue;
637
+ const m = entry;
638
+ if (typeof m.slug !== "string" || !m.slug) continue;
639
+ if (m.visibility !== void 0 && m.visibility !== "list") continue;
640
+ const efforts = (Array.isArray(m.supported_reasoning_levels) ? m.supported_reasoning_levels : []).map((l) => l && typeof l === "object" ? l.effort : l).filter(isAgentEffort);
641
+ models.push({
642
+ option: {
643
+ id: m.slug,
644
+ label: typeof m.display_name === "string" && m.display_name ? m.display_name : m.slug,
645
+ efforts
646
+ },
647
+ priority: typeof m.priority === "number" ? m.priority : Number.MAX_SAFE_INTEGER
648
+ });
649
+ }
650
+ return models.sort((a, b) => a.priority - b.priority).map((m) => m.option);
651
+ }
652
+ async function readCodexModels() {
653
+ const home = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
654
+ try {
655
+ const models = parseCodexModelsCache(JSON.parse(await fs.readFile(path.join(home, "models_cache.json"), "utf8")));
656
+ return models.length > 0 ? models : [CODEX_DEFAULT_MODEL];
657
+ } catch {
658
+ return [CODEX_DEFAULT_MODEL];
659
+ }
660
+ }
661
+ function createCodexMapper(emit) {
662
+ const started = /* @__PURE__ */ new Set();
663
+ let resultSeen = false;
664
+ const startTool = (item, name, title, detail) => {
665
+ if (started.has(item.id)) return;
666
+ started.add(item.id);
667
+ emit({
668
+ type: "tool-start",
669
+ id: item.id,
670
+ name,
671
+ title,
672
+ detail
673
+ });
674
+ };
675
+ const describe = (item) => {
676
+ switch (item.type) {
677
+ case "command_execution": return {
678
+ name: "command",
679
+ title: "Run command",
680
+ detail: clipDetail(item.command)
681
+ };
682
+ case "file_change": return {
683
+ name: "file_change",
684
+ title: "Edit",
685
+ detail: clipDetail(item.changes.map((c) => c.path).join(", "))
686
+ };
687
+ case "mcp_tool_call": return {
688
+ name: "mcp",
689
+ title: `${item.server}: ${item.tool}`,
690
+ detail: null
691
+ };
692
+ case "web_search": return {
693
+ name: "web_search",
694
+ title: "Web search",
695
+ detail: clipDetail(item.query)
696
+ };
697
+ default: return {
698
+ name: item.type,
699
+ title: item.type,
700
+ detail: null
701
+ };
702
+ }
703
+ };
704
+ const handleItem = (phase, item) => {
705
+ switch (item.type) {
706
+ case "agent_message": {
707
+ if (phase !== "completed" || !item.text.trim()) return;
708
+ const asked = extractQuestionBlock(item.text);
709
+ const text = asked ? asked.text : item.text;
710
+ if (text) {
711
+ emit({
712
+ type: "text-start",
713
+ id: item.id
714
+ });
715
+ emit({
716
+ type: "text-end",
717
+ id: item.id,
718
+ text
719
+ });
720
+ }
721
+ if (asked) emit({
722
+ type: "questions",
723
+ questions: asked.questions
724
+ });
725
+ return;
726
+ }
727
+ case "error":
728
+ if (phase === "completed") emit({
729
+ type: "error",
730
+ text: item.message
731
+ });
732
+ return;
733
+ case "command_execution":
734
+ case "file_change":
735
+ case "mcp_tool_call":
736
+ case "web_search": {
737
+ const { name, title, detail } = describe(item);
738
+ startTool(item, name, title, detail);
739
+ if (phase === "completed") {
740
+ const status = "status" in item ? item.status : "completed";
741
+ emit({
742
+ type: "tool-end",
743
+ id: item.id,
744
+ ok: status !== "failed"
745
+ });
746
+ }
747
+ return;
748
+ }
749
+ default: return;
750
+ }
751
+ };
752
+ return {
753
+ get resultSeen() {
754
+ return resultSeen;
755
+ },
756
+ handle(event) {
757
+ if (!event || typeof event !== "object") return;
758
+ switch (event.type) {
759
+ case "thread.started":
760
+ emit({
761
+ type: "session-id",
762
+ id: event.thread_id
763
+ });
764
+ return;
765
+ case "item.started":
766
+ handleItem("started", event.item);
767
+ return;
768
+ case "item.updated":
769
+ handleItem("updated", event.item);
770
+ return;
771
+ case "item.completed":
772
+ handleItem("completed", event.item);
773
+ return;
774
+ case "turn.failed":
775
+ resultSeen = true;
776
+ emit({
777
+ type: "error",
778
+ text: event.error.message
779
+ });
780
+ emit({
781
+ type: "result",
782
+ durationMs: null,
783
+ costUsd: null
784
+ });
785
+ return;
786
+ case "error":
787
+ emit({
788
+ type: "error",
789
+ text: event.message
790
+ });
791
+ return;
792
+ case "turn.completed":
793
+ resultSeen = true;
794
+ emit({
795
+ type: "result",
796
+ durationMs: null,
797
+ costUsd: null
798
+ });
799
+ return;
800
+ default: return;
801
+ }
802
+ }
803
+ };
804
+ }
805
+ function createCodexProvider() {
806
+ return {
807
+ id: "codex",
808
+ name: "Codex",
809
+ sdkPackage: CODEX_SDK_PACKAGE,
810
+ askInstruction: TEXT_QUESTIONS_INSTRUCTION,
811
+ canRewind: false,
812
+ cliCommand: "codex",
813
+ models: readCodexModels,
814
+ async run(opts) {
815
+ const sdk = await importWorkspacePackage(opts.cwd, CODEX_SDK_PACKAGE);
816
+ if (!sdk || typeof sdk.Codex !== "function") throw new ProviderUnavailableError(this);
817
+ const localCli = findSpawnableCli("codex");
818
+ const codex = new sdk.Codex(localCli ? { codexPathOverride: localCli } : {});
819
+ const threadOptions = {
820
+ workingDirectory: opts.cwd,
821
+ skipGitRepoCheck: true,
822
+ sandboxMode: "workspace-write",
823
+ approvalPolicy: "never",
824
+ ...opts.model ? { model: opts.model } : {},
825
+ ...opts.effort ? { modelReasoningEffort: opts.effort } : {}
826
+ };
827
+ const thread = opts.resume ? codex.resumeThread(opts.resume, threadOptions) : codex.startThread(threadOptions);
828
+ const input = opts.resume ? opts.prompt : `${opts.systemPromptAppend}\n\n${opts.prompt}`;
829
+ const mapper = createCodexMapper(opts.emit);
830
+ const { events } = await thread.runStreamed(input, { signal: opts.signal });
831
+ try {
832
+ for await (const event of events) mapper.handle(event);
833
+ } catch (err) {
834
+ if (opts.signal.aborted || mapper.resultSeen) return;
835
+ throw err;
836
+ }
837
+ if (thread.id) opts.emit({
838
+ type: "session-id",
839
+ id: thread.id
840
+ });
841
+ }
842
+ };
843
+ }
844
+ //#endregion
845
+ //#region src/agent/providers/index.ts
846
+ function createProviders() {
847
+ return {
848
+ claude: createClaudeProvider(),
849
+ codex: createCodexProvider()
850
+ };
851
+ }
852
+ //#endregion
853
+ //#region src/agent/store.ts
854
+ const MAX_SESSIONS = 30;
855
+ const MAX_ITEMS_PER_SESSION = 400;
856
+ const SAVE_DEBOUNCE_MS = 150;
857
+ const TITLE_MAX = 60;
858
+ function newId(prefix) {
859
+ return `${prefix}-${randomBytes(6).toString("hex")}`;
860
+ }
861
+ function titleFromPrompt(text) {
862
+ const line = text.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
863
+ if (!line) return "Untitled";
864
+ return line.length > TITLE_MAX ? `${line.slice(0, 59)}…` : line;
865
+ }
866
+ function readPersisted(file) {
867
+ if (!existsSync(file)) return [];
868
+ try {
869
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
870
+ if (parsed?.version !== 1 || !Array.isArray(parsed.sessions)) return [];
871
+ return parsed.sessions.map((s) => ({
872
+ ...s,
873
+ status: "idle",
874
+ items: s.items.map(closePending)
875
+ }));
876
+ } catch {
877
+ return [];
878
+ }
879
+ }
880
+ function closePending(item) {
881
+ if (item.kind === "assistant" && item.streaming) return {
882
+ ...item,
883
+ streaming: false
884
+ };
885
+ if (item.kind === "tool" && item.status === "running") return {
886
+ ...item,
887
+ status: "error"
888
+ };
889
+ if (item.kind === "question" && item.status === "pending") return {
890
+ ...item,
891
+ status: "cancelled"
892
+ };
893
+ if (item.kind === "permission" && item.status === "pending") return {
894
+ ...item,
895
+ status: "denied"
896
+ };
897
+ return item;
898
+ }
899
+ function createSessionStore(opts) {
900
+ const sessions = /* @__PURE__ */ new Map();
901
+ const listeners = /* @__PURE__ */ new Set();
902
+ let saveTimer = null;
903
+ let seq = 0;
904
+ const order = /* @__PURE__ */ new Map();
905
+ if (opts.file) for (const s of readPersisted(opts.file)) {
906
+ sessions.set(s.id, s);
907
+ order.set(s.id, seq++);
908
+ }
909
+ const emit = (event) => {
910
+ for (const l of listeners) l(event);
911
+ };
912
+ const scheduleSave = () => {
913
+ if (!opts.file) return;
914
+ if (saveTimer) clearTimeout(saveTimer);
915
+ saveTimer = setTimeout(() => {
916
+ saveTimer = null;
917
+ save();
918
+ }, SAVE_DEBOUNCE_MS);
919
+ };
920
+ const writeSnapshot = async (file) => {
921
+ const body = {
922
+ version: 1,
923
+ sessions: list().flatMap((s) => sessions.get(s.id) ?? [])
924
+ };
925
+ const tmp = `${file}.tmp`;
926
+ try {
927
+ await fs.mkdir(path.dirname(file), { recursive: true });
928
+ await fs.writeFile(tmp, JSON.stringify(body), {
929
+ encoding: "utf8",
930
+ mode: 384
931
+ });
932
+ await fs.rename(tmp, file);
933
+ } catch (err) {
934
+ console.error("[open-slide] failed to save agent sessions:", err);
935
+ await fs.rm(tmp, { force: true }).catch(() => {});
936
+ }
937
+ };
938
+ let saving = Promise.resolve();
939
+ const save = () => {
940
+ const file = opts.file;
941
+ if (!file) return saving;
942
+ saving = saving.then(() => writeSnapshot(file));
943
+ return saving;
944
+ };
945
+ const touch = (session) => {
946
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
947
+ order.set(session.id, seq++);
948
+ emit({
949
+ type: "session",
950
+ session: sessionSummary(session)
951
+ });
952
+ scheduleSave();
953
+ };
954
+ const require = (id) => {
955
+ const session = sessions.get(id);
956
+ if (!session) throw new Error(`unknown agent session: ${id}`);
957
+ return session;
958
+ };
959
+ function list() {
960
+ return Array.from(sessions.values()).sort((a, b) => {
961
+ if (a.updatedAt !== b.updatedAt) return a.updatedAt < b.updatedAt ? 1 : -1;
962
+ return (order.get(b.id) ?? 0) - (order.get(a.id) ?? 0);
963
+ }).map(sessionSummary);
964
+ }
965
+ function get(id) {
966
+ return sessions.get(id) ?? null;
967
+ }
968
+ function create(provider, slideId) {
969
+ const now = (/* @__PURE__ */ new Date()).toISOString();
970
+ const session = {
971
+ id: newId("s"),
972
+ provider,
973
+ title: "New session",
974
+ createdAt: now,
975
+ updatedAt: now,
976
+ status: "idle",
977
+ slideId,
978
+ providerSessionId: null,
979
+ items: []
980
+ };
981
+ sessions.set(session.id, session);
982
+ order.set(session.id, seq++);
983
+ const idle = list().filter((s) => s.status === "idle");
984
+ for (const stale of idle.slice(MAX_SESSIONS)) {
985
+ sessions.delete(stale.id);
986
+ order.delete(stale.id);
987
+ emit({
988
+ type: "session-removed",
989
+ sessionId: stale.id
990
+ });
991
+ }
992
+ touch(session);
993
+ return session;
994
+ }
995
+ function remove(id) {
996
+ if (!sessions.delete(id)) return false;
997
+ order.delete(id);
998
+ emit({
999
+ type: "session-removed",
1000
+ sessionId: id
1001
+ });
1002
+ scheduleSave();
1003
+ return true;
1004
+ }
1005
+ function patch(id, changes) {
1006
+ const session = require(id);
1007
+ Object.assign(session, changes);
1008
+ touch(session);
1009
+ return session;
1010
+ }
1011
+ function appendItem(sessionId, item) {
1012
+ const session = require(sessionId);
1013
+ session.items.push(item);
1014
+ if (session.items.length > MAX_ITEMS_PER_SESSION) {
1015
+ session.items.splice(0, session.items.length - MAX_ITEMS_PER_SESSION);
1016
+ session.trimmed = true;
1017
+ }
1018
+ emit({
1019
+ type: "item",
1020
+ sessionId,
1021
+ item
1022
+ });
1023
+ touch(session);
1024
+ return item;
1025
+ }
1026
+ function updateItem(sessionId, itemId, kind, changes) {
1027
+ const session = require(sessionId);
1028
+ const index = session.items.findIndex((i) => i.id === itemId && i.kind === kind);
1029
+ if (index === -1) return null;
1030
+ const next = {
1031
+ ...session.items[index],
1032
+ ...changes
1033
+ };
1034
+ session.items[index] = next;
1035
+ emit({
1036
+ type: "item",
1037
+ sessionId,
1038
+ item: next
1039
+ });
1040
+ touch(session);
1041
+ return next;
1042
+ }
1043
+ function truncateFrom(sessionId, itemId) {
1044
+ const session = require(sessionId);
1045
+ const index = session.items.findIndex((i) => i.id === itemId);
1046
+ if (index === -1) return false;
1047
+ session.items.splice(index);
1048
+ emit({
1049
+ type: "truncate",
1050
+ sessionId,
1051
+ itemId
1052
+ });
1053
+ touch(session);
1054
+ return true;
1055
+ }
1056
+ function appendDelta(sessionId, itemId, text) {
1057
+ if (!text) return;
1058
+ const item = require(sessionId).items.find((i) => i.id === itemId);
1059
+ if (item?.kind !== "assistant") return;
1060
+ item.text += text;
1061
+ emit({
1062
+ type: "delta",
1063
+ sessionId,
1064
+ itemId,
1065
+ text
1066
+ });
1067
+ scheduleSave();
1068
+ }
1069
+ function subscribe(listener) {
1070
+ listeners.add(listener);
1071
+ return () => {
1072
+ listeners.delete(listener);
1073
+ };
1074
+ }
1075
+ async function flush() {
1076
+ if (saveTimer) {
1077
+ clearTimeout(saveTimer);
1078
+ saveTimer = null;
1079
+ }
1080
+ await save();
1081
+ }
1082
+ return {
1083
+ list,
1084
+ get,
1085
+ create,
1086
+ remove,
1087
+ patch,
1088
+ appendItem,
1089
+ updateItem,
1090
+ truncateFrom,
1091
+ appendDelta,
1092
+ subscribe,
1093
+ flush
1094
+ };
1095
+ }
1096
+ //#endregion
1097
+ //#region src/agent/runner.ts
1098
+ function cancelPrompt(prompt) {
1099
+ if (prompt.kind === "question") prompt.resolve(null);
1100
+ else prompt.resolve(false);
1101
+ }
1102
+ function createAgentRunner(opts) {
1103
+ const { userCwd, writableRoots, store, providers, onTurnEnd } = opts;
1104
+ const running = /* @__PURE__ */ new Map();
1105
+ const pending = /* @__PURE__ */ new Map();
1106
+ const lastOptions = /* @__PURE__ */ new Map();
1107
+ const now = () => (/* @__PURE__ */ new Date()).toISOString();
1108
+ function isRunning(sessionId) {
1109
+ return running.has(sessionId);
1110
+ }
1111
+ async function send(sessionId, options) {
1112
+ const session = store.get(sessionId);
1113
+ if (!session) throw new Error(`unknown agent session: ${sessionId}`);
1114
+ if (running.has(sessionId)) throw new Error("session is already running");
1115
+ const text = options.text.trim();
1116
+ if (!text) throw new Error("empty prompt");
1117
+ const provider = providers[session.provider];
1118
+ let context = options.context;
1119
+ let promptText = text;
1120
+ let resumeAt = null;
1121
+ if (options.edit) {
1122
+ const index = session.items.findIndex((i) => i.id === options.edit);
1123
+ const target = session.items[index];
1124
+ if (target?.kind !== "user") throw new Error("unknown message");
1125
+ if (!session.trimmed && !session.items.slice(0, index).some((i) => i.kind === "user")) {
1126
+ store.patch(sessionId, {
1127
+ providerSessionId: null,
1128
+ providerCheckpoint: null
1129
+ });
1130
+ if (target.newDeck && context) context = {
1131
+ ...context,
1132
+ newDeck: true
1133
+ };
1134
+ } else if (provider.canRewind && target.resumeAt) {
1135
+ resumeAt = target.resumeAt;
1136
+ store.patch(sessionId, { providerCheckpoint: resumeAt });
1137
+ promptText = rewoundPrompt(text);
1138
+ } else promptText = editedPrompt(target.text, text);
1139
+ store.truncateFrom(sessionId, target.id);
1140
+ }
1141
+ const controller = new AbortController();
1142
+ running.set(sessionId, controller);
1143
+ lastOptions.set(sessionId, {
1144
+ ...options,
1145
+ context,
1146
+ edit: void 0
1147
+ });
1148
+ if (session.items.length === 0) store.patch(sessionId, { title: titleFromPrompt(text) });
1149
+ if (options.context?.slideId && !session.slideId) store.patch(sessionId, { slideId: options.context.slideId });
1150
+ for (const item of session.items) if (item.kind === "question" && item.status === "pending" && item.id !== options.answering) store.updateItem(sessionId, item.id, "question", { status: "cancelled" });
1151
+ const userItemId = options.answering ?? newId("u");
1152
+ if (!options.answering) store.appendItem(sessionId, {
1153
+ kind: "user",
1154
+ id: userItemId,
1155
+ ts: now(),
1156
+ text,
1157
+ ...session.providerCheckpoint ? { resumeAt: session.providerCheckpoint } : {},
1158
+ ...context?.newDeck ? { newDeck: true } : {}
1159
+ });
1160
+ store.patch(sessionId, { status: "running" });
1161
+ const openText = /* @__PURE__ */ new Set();
1162
+ const openTools = /* @__PURE__ */ new Set();
1163
+ const startedAt = Date.now();
1164
+ let resultSeen = false;
1165
+ const appendQuestion = (questions) => store.appendItem(sessionId, {
1166
+ kind: "question",
1167
+ id: newId("q"),
1168
+ ts: now(),
1169
+ questions,
1170
+ answers: null,
1171
+ status: "pending"
1172
+ });
1173
+ const emit = (event) => {
1174
+ if (!store.get(sessionId)) return;
1175
+ switch (event.type) {
1176
+ case "session-id":
1177
+ store.patch(sessionId, { providerSessionId: event.id });
1178
+ return;
1179
+ case "checkpoint":
1180
+ store.patch(sessionId, { providerCheckpoint: event.id });
1181
+ return;
1182
+ case "text-start":
1183
+ openText.add(event.id);
1184
+ store.appendItem(sessionId, {
1185
+ kind: "assistant",
1186
+ id: event.id,
1187
+ ts: now(),
1188
+ text: "",
1189
+ streaming: true
1190
+ });
1191
+ return;
1192
+ case "text-delta":
1193
+ if (!openText.has(event.id)) emit({
1194
+ type: "text-start",
1195
+ id: event.id
1196
+ });
1197
+ store.appendDelta(sessionId, event.id, event.text);
1198
+ return;
1199
+ case "text-end": {
1200
+ if (!openText.has(event.id)) {
1201
+ if (!event.text) return;
1202
+ emit({
1203
+ type: "text-start",
1204
+ id: event.id
1205
+ });
1206
+ }
1207
+ openText.delete(event.id);
1208
+ const changes = event.text !== null ? {
1209
+ streaming: false,
1210
+ text: event.text
1211
+ } : { streaming: false };
1212
+ store.updateItem(sessionId, event.id, "assistant", changes);
1213
+ return;
1214
+ }
1215
+ case "tool-start":
1216
+ openTools.add(event.id);
1217
+ store.appendItem(sessionId, {
1218
+ kind: "tool",
1219
+ id: event.id,
1220
+ ts: now(),
1221
+ name: event.name,
1222
+ title: event.title,
1223
+ detail: event.detail,
1224
+ status: "running"
1225
+ });
1226
+ return;
1227
+ case "tool-end":
1228
+ openTools.delete(event.id);
1229
+ store.updateItem(sessionId, event.id, "tool", { status: event.ok ? "done" : "error" });
1230
+ return;
1231
+ case "error":
1232
+ store.appendItem(sessionId, {
1233
+ kind: "error",
1234
+ id: newId("e"),
1235
+ ts: now(),
1236
+ text: event.text
1237
+ });
1238
+ return;
1239
+ case "questions":
1240
+ appendQuestion(event.questions);
1241
+ return;
1242
+ case "result":
1243
+ resultSeen = true;
1244
+ store.appendItem(sessionId, {
1245
+ kind: "result",
1246
+ id: newId("r"),
1247
+ ts: now(),
1248
+ durationMs: event.durationMs ?? Date.now() - startedAt,
1249
+ costUsd: event.costUsd,
1250
+ aborted: false
1251
+ });
1252
+ return;
1253
+ }
1254
+ };
1255
+ const askUser = (questions) => new Promise((resolve) => {
1256
+ if (controller.signal.aborted) return resolve(null);
1257
+ const item = appendQuestion(questions);
1258
+ pending.set(item.id, {
1259
+ kind: "question",
1260
+ resolve: (answers) => {
1261
+ pending.delete(item.id);
1262
+ store.updateItem(sessionId, item.id, "question", {
1263
+ answers,
1264
+ status: answers ? "answered" : "cancelled"
1265
+ });
1266
+ resolve(answers);
1267
+ }
1268
+ });
1269
+ });
1270
+ const requestPermission = (request) => new Promise((resolve) => {
1271
+ if (controller.signal.aborted) return resolve(false);
1272
+ const item = store.appendItem(sessionId, {
1273
+ kind: "permission",
1274
+ id: newId("p"),
1275
+ ts: now(),
1276
+ tool: request.tool,
1277
+ title: request.title,
1278
+ detail: request.detail,
1279
+ status: options.autoApprove ? "allowed" : "pending"
1280
+ });
1281
+ if (options.autoApprove) return resolve(true);
1282
+ pending.set(item.id, {
1283
+ kind: "permission",
1284
+ resolve: (allow) => {
1285
+ pending.delete(item.id);
1286
+ store.updateItem(sessionId, item.id, "permission", { status: allow ? "allowed" : "denied" });
1287
+ resolve(allow);
1288
+ }
1289
+ });
1290
+ });
1291
+ const finish = () => {
1292
+ running.delete(sessionId);
1293
+ if (!store.get(sessionId)) return;
1294
+ for (const id of openText) store.updateItem(sessionId, id, "assistant", { streaming: false });
1295
+ for (const id of openTools) store.updateItem(sessionId, id, "tool", { status: "error" });
1296
+ if (!resultSeen) store.appendItem(sessionId, {
1297
+ kind: "result",
1298
+ id: newId("r"),
1299
+ ts: now(),
1300
+ durationMs: Date.now() - startedAt,
1301
+ costUsd: null,
1302
+ aborted: controller.signal.aborted
1303
+ });
1304
+ store.patch(sessionId, { status: "idle" });
1305
+ if (onTurnEnd) Promise.resolve().then(() => onTurnEnd({
1306
+ sessionId,
1307
+ userItemId
1308
+ })).catch((err) => {
1309
+ console.error("[open-slide] agent turn-end handler failed:", err);
1310
+ });
1311
+ };
1312
+ const prompt = buildTurnPrompt(promptText, context);
1313
+ provider.run({
1314
+ cwd: userCwd,
1315
+ writableRoots,
1316
+ prompt,
1317
+ systemPromptAppend: systemPromptAppend(provider.askInstruction),
1318
+ resume: session.providerSessionId,
1319
+ resumeAt,
1320
+ model: options.model ?? null,
1321
+ effort: options.effort ?? null,
1322
+ signal: controller.signal,
1323
+ emit,
1324
+ askUser,
1325
+ requestPermission
1326
+ }).catch((err) => {
1327
+ if (controller.signal.aborted) return;
1328
+ emit({
1329
+ type: "error",
1330
+ text: err instanceof Error ? err.message : String(err)
1331
+ });
1332
+ }).finally(finish);
1333
+ }
1334
+ function stop(sessionId) {
1335
+ const controller = running.get(sessionId);
1336
+ if (!controller) return false;
1337
+ const session = store.get(sessionId);
1338
+ if (session) for (const item of session.items) {
1339
+ const prompt = pending.get(item.id);
1340
+ if (prompt) cancelPrompt(prompt);
1341
+ }
1342
+ controller.abort();
1343
+ return true;
1344
+ }
1345
+ function respond(sessionId, itemId, payload, next = {}) {
1346
+ if (!store.get(sessionId)?.items.some((i) => i.id === itemId)) return false;
1347
+ const prompt = pending.get(itemId);
1348
+ if (!prompt) return answerInNextTurn(sessionId, itemId, payload, next);
1349
+ if ("cancel" in payload) {
1350
+ cancelPrompt(prompt);
1351
+ return true;
1352
+ }
1353
+ if (prompt.kind === "question" && "answers" in payload) {
1354
+ prompt.resolve(payload.answers);
1355
+ return true;
1356
+ }
1357
+ if (prompt.kind === "permission" && "allow" in payload) {
1358
+ prompt.resolve(payload.allow);
1359
+ return true;
1360
+ }
1361
+ return false;
1362
+ }
1363
+ function answerInNextTurn(sessionId, itemId, payload, next) {
1364
+ const item = store.get(sessionId)?.items.find((i) => i.id === itemId);
1365
+ if (item?.kind !== "question" || item.status !== "pending" || running.has(sessionId)) return false;
1366
+ if (!("answers" in payload) && !("cancel" in payload)) return false;
1367
+ const answers = "answers" in payload ? payload.answers : null;
1368
+ store.updateItem(sessionId, itemId, "question", {
1369
+ answers,
1370
+ status: answers ? "answered" : "cancelled"
1371
+ });
1372
+ const previous = {
1373
+ ...lastOptions.get(sessionId),
1374
+ ...next
1375
+ };
1376
+ const context = previous.context ? {
1377
+ ...previous.context,
1378
+ newDeck: void 0
1379
+ } : void 0;
1380
+ send(sessionId, {
1381
+ ...previous,
1382
+ context,
1383
+ text: answersPrompt(item.questions, answers),
1384
+ answering: itemId
1385
+ }).catch((err) => {
1386
+ console.error("[open-slide] agent turn failed:", err);
1387
+ });
1388
+ return true;
1389
+ }
1390
+ function stopAll() {
1391
+ for (const id of Array.from(running.keys())) stop(id);
1392
+ }
1393
+ return {
1394
+ send,
1395
+ stop,
1396
+ respond,
1397
+ isRunning,
1398
+ stopAll
1399
+ };
1400
+ }
1401
+ //#endregion
1402
+ //#region src/agent/status.ts
1403
+ const INSTALL_TIMEOUT_MS = 6e5;
1404
+ async function providerStatus(userCwd, provider) {
1405
+ const sdk = resolveWorkspacePackage(userCwd, provider.sdkPackage);
1406
+ const cliPath = findOnPath(provider.cliCommand);
1407
+ const [cliVersion, models] = await Promise.all([cliPath ? commandVersion(cliPath) : null, provider.models()]);
1408
+ return {
1409
+ id: provider.id,
1410
+ name: provider.name,
1411
+ sdkPackage: provider.sdkPackage,
1412
+ sdkVersion: sdk?.version ?? null,
1413
+ cliCommand: provider.cliCommand,
1414
+ cliFound: cliPath !== null,
1415
+ cliVersion,
1416
+ ready: sdk !== null,
1417
+ models
1418
+ };
1419
+ }
1420
+ async function agentStatus(userCwd, providers, packageManager) {
1421
+ return {
1422
+ enabled: true,
1423
+ packageManager,
1424
+ providers: await Promise.all(Object.values(providers).map((provider) => providerStatus(userCwd, provider)))
1425
+ };
1426
+ }
1427
+ function installCommandFor(packageManager, pkg) {
1428
+ switch (packageManager) {
1429
+ case "pnpm": return {
1430
+ cmd: "pnpm",
1431
+ args: [
1432
+ "add",
1433
+ "-D",
1434
+ pkg
1435
+ ]
1436
+ };
1437
+ case "yarn": return {
1438
+ cmd: "yarn",
1439
+ args: [
1440
+ "add",
1441
+ "-D",
1442
+ pkg
1443
+ ]
1444
+ };
1445
+ case "bun": return {
1446
+ cmd: "bun",
1447
+ args: [
1448
+ "add",
1449
+ "-d",
1450
+ pkg
1451
+ ]
1452
+ };
1453
+ case "npm": return {
1454
+ cmd: "npm",
1455
+ args: [
1456
+ "install",
1457
+ "-D",
1458
+ pkg
1459
+ ]
1460
+ };
1461
+ }
1462
+ }
1463
+ function installSdk(packageManager, pkg, cwd) {
1464
+ return runCommand(installCommandFor(packageManager, pkg), cwd, INSTALL_TIMEOUT_MS);
1465
+ }
1466
+ //#endregion
1467
+ //#region src/vite/recent-writes.ts
1468
+ const recentWrites = /* @__PURE__ */ new Map();
1469
+ /** Record that `file` was just written by us. */
1470
+ function recordWrite(file, now = Date.now()) {
1471
+ recentWrites.set(file, now);
1472
+ }
1473
+ /**
1474
+ * True if `file` was recorded within the recent-write window. Expired entries
1475
+ * are pruned so a stale path can no longer suppress a later genuine edit.
1476
+ */
1477
+ function hasRecentWrite(file, now = Date.now()) {
1478
+ const ts = recentWrites.get(file);
1479
+ if (ts == null) return false;
1480
+ if (now - ts < 1500) return true;
1481
+ recentWrites.delete(file);
1482
+ return false;
1483
+ }
1484
+ //#endregion
1485
+ //#region src/vite/open-slide-plugin.ts
1486
+ const CONFIG_FILE = "open-slide.config.ts";
1487
+ const SLIDES_VMOD = "virtual:open-slide/slides";
1488
+ const CONFIG_VMOD = "virtual:open-slide/config";
1489
+ const FOLDERS_VMOD = "virtual:open-slide/folders";
1490
+ function resolved$1(id) {
1491
+ return `\0${id}`;
1492
+ }
1493
+ async function findSlides(userCwd, slidesDir) {
1494
+ const abs = path.resolve(userCwd, slidesDir);
1495
+ if (!existsSync(abs)) return [];
1496
+ return (await fg("*/index.{tsx,jsx,ts,js}", {
1497
+ cwd: abs,
1498
+ absolute: true,
1499
+ onlyFiles: true
1500
+ })).sort();
1501
+ }
1502
+ function toId(absFile, slidesRoot) {
1503
+ return path.relative(slidesRoot, absFile).split(path.sep)[0];
1504
+ }
1505
+ const META_THEME_RE = /(?:^|[\s,{])theme\s*:\s*['"]([^'"]+)['"]/;
1506
+ const META_CREATED_AT_RE = /(?:^|[\s,{])createdAt\s*:\s*['"]([^'"]+)['"]/;
1507
+ function extractMeta(src) {
1508
+ const empty = {
1509
+ theme: null,
1510
+ createdAt: null
1511
+ };
1512
+ const metaStart = src.search(/export\s+const\s+meta\b/);
1513
+ if (metaStart === -1) return empty;
1514
+ const eqIdx = src.indexOf("=", metaStart);
1515
+ if (eqIdx === -1) return empty;
1516
+ const openBrace = src.indexOf("{", eqIdx);
1517
+ if (openBrace === -1) return empty;
1518
+ let depth = 0;
1519
+ let closeBrace = -1;
1520
+ for (let i = openBrace; i < src.length; i++) {
1521
+ const ch = src[i];
1522
+ if (ch === "{") depth++;
1523
+ else if (ch === "}") {
1524
+ depth--;
1525
+ if (depth === 0) {
1526
+ closeBrace = i;
1527
+ break;
1528
+ }
1529
+ }
1530
+ }
1531
+ if (closeBrace === -1) return empty;
1532
+ const body = src.slice(openBrace + 1, closeBrace);
1533
+ const themeMatch = body.match(META_THEME_RE);
1534
+ const createdAtMatch = body.match(META_CREATED_AT_RE);
1535
+ return {
1536
+ theme: themeMatch ? themeMatch[1] : null,
1537
+ createdAt: createdAtMatch ? createdAtMatch[1] : null
1538
+ };
1539
+ }
1540
+ async function readSlideMeta(abs) {
1541
+ try {
1542
+ return extractMeta(await fs.readFile(abs, "utf8"));
1543
+ } catch {
1544
+ return {
1545
+ theme: null,
1546
+ createdAt: null
1547
+ };
1548
+ }
1549
+ }
1550
+ function parseCreatedAtMs(iso) {
1551
+ if (!iso) return null;
1552
+ const ms = Date.parse(iso);
1553
+ return Number.isFinite(ms) ? ms : null;
1554
+ }
1555
+ const warnedInvalidSlideIds = /* @__PURE__ */ new Set();
1556
+ async function generateSlidesModule(files, slidesRoot, isDev) {
1557
+ const scanned = await Promise.all(files.map(async (abs) => {
1558
+ const id = toId(abs, slidesRoot);
1559
+ const importPath = isDev ? `@fs/${normalizePath(abs).replace(/^\/+/, "")}` : abs;
1560
+ const meta = await readSlideMeta(abs);
1561
+ return {
1562
+ id,
1563
+ importPath,
1564
+ theme: meta.theme,
1565
+ createdAt: parseCreatedAtMs(meta.createdAt)
1566
+ };
1567
+ }));
1568
+ const entries = scanned.filter((e) => SLIDE_ID_RE.test(e.id));
1569
+ const ignored = scanned.filter((e) => !SLIDE_ID_RE.test(e.id)).map((e) => e.id);
1570
+ const ids = JSON.stringify(entries.map((e) => e.id).sort());
1571
+ const themesMap = {};
1572
+ const createdAtMap = {};
1573
+ for (const e of entries) {
1574
+ if (e.theme) themesMap[e.id] = e.theme;
1575
+ if (e.createdAt !== null) createdAtMap[e.id] = e.createdAt;
1576
+ }
1577
+ const themesJson = JSON.stringify(themesMap);
1578
+ const createdAtJson = JSON.stringify(createdAtMap);
1579
+ const importTokens = JSON.stringify(Object.fromEntries(entries.map((e) => [e.id, 0])));
1580
+ return {
1581
+ code: `// virtual:open-slide/slides — generated
1582
+ export const slideIds = ${ids};
1583
+ export const slideThemes = ${themesJson};
1584
+ export const slideCreatedAt = ${createdAtJson};
1585
+ ${isDev ? `
1586
+ const slideImportTokens = ${importTokens};
1587
+ if (import.meta.hot) {
1588
+ import.meta.hot.on('open-slide:slide-changed', (data) => {
1589
+ const ids = Array.isArray(data?.slideIds) ? data.slideIds : [];
1590
+ const token = Date.now();
1591
+ for (const id of ids) {
1592
+ if (Object.prototype.hasOwnProperty.call(slideImportTokens, id)) slideImportTokens[id] = token;
1593
+ }
1594
+ });
1595
+ }
1596
+ ` : ""}
1597
+
1598
+ export async function loadSlide(id) {
1599
+ switch (id) {
1600
+ ${entries.map((e) => {
1601
+ const importExpr = isDev ? `import(/* @vite-ignore */ import.meta.env.BASE_URL + ${JSON.stringify(`${e.importPath}?t=`)} + slideImportTokens[${JSON.stringify(e.id)}])` : `import(${JSON.stringify(e.importPath)})`;
1602
+ return ` case ${JSON.stringify(e.id)}: return ${importExpr};`;
1603
+ }).join("\n")}
1604
+ default: throw new Error('Slide not found: ' + id);
1605
+ }
1606
+ }
1607
+ `,
1608
+ ignored
1609
+ };
1610
+ }
1611
+ function openSlidePlugin(opts) {
1612
+ const { userCwd, config, coreVersion } = opts;
1613
+ const slidesDir = config.slidesDir ?? "slides";
1614
+ const slidesRoot = path.resolve(userCwd, slidesDir);
1615
+ const manifestPath = foldersManifestPath(slidesRoot);
1616
+ let isDev = false;
1617
+ const slideIdForEntry = (p) => {
1618
+ const rel = path.relative(slidesRoot, p);
1619
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
1620
+ const parts = rel.split(path.sep);
1621
+ if (parts.length !== 2) return null;
1622
+ if (!/^index\.(tsx|jsx|ts|js)$/.test(parts[1])) return null;
1623
+ return parts[0];
1624
+ };
1625
+ let slideChangeTimer = null;
1626
+ const pendingSlideChanges = /* @__PURE__ */ new Set();
1627
+ const queueSlideChanged = (server, id) => {
1628
+ pendingSlideChanges.add(id);
1629
+ if (slideChangeTimer) clearTimeout(slideChangeTimer);
1630
+ slideChangeTimer = setTimeout(() => {
1631
+ slideChangeTimer = null;
1632
+ const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
1633
+ if (mod) server.moduleGraph.invalidateModule(mod);
1634
+ const slideIds = Array.from(pendingSlideChanges);
1635
+ pendingSlideChanges.clear();
1636
+ server.ws.send({
1637
+ type: "custom",
1638
+ event: "open-slide:slide-changed",
1639
+ data: { slideIds }
1640
+ });
1641
+ }, 100);
1642
+ };
1643
+ return {
1644
+ name: "open-slide",
1645
+ config(_c, env) {
1646
+ isDev = env.command === "serve";
1647
+ },
1648
+ resolveId(id) {
1649
+ if (id === SLIDES_VMOD) return resolved$1(SLIDES_VMOD);
1650
+ if (id === CONFIG_VMOD) return resolved$1(CONFIG_VMOD);
1651
+ if (id === FOLDERS_VMOD) return resolved$1(FOLDERS_VMOD);
1652
+ return null;
1653
+ },
1654
+ async load(id) {
1655
+ if (id === resolved$1(SLIDES_VMOD)) {
1656
+ const { code, ignored } = await generateSlidesModule(await findSlides(userCwd, slidesDir), slidesRoot, isDev);
1657
+ for (const slideId of ignored) {
1658
+ if (warnedInvalidSlideIds.has(slideId)) continue;
1659
+ warnedInvalidSlideIds.add(slideId);
1660
+ this.warn(`Ignoring slide folder "${slideId}": slide ids must match ${SLIDE_ID_RE} (lowercase/uppercase letters, digits, "-", "_"). Rename the folder under "${slidesDir}/" to a kebab-case id so it appears in the browser and can be moved into folders.`);
1661
+ }
1662
+ return code;
1663
+ }
1664
+ if (id === resolved$1(CONFIG_VMOD)) {
1665
+ const userBuild = config.build ?? {};
1666
+ const buildResolved = isDev ? {
1667
+ showSlideBrowser: true,
1668
+ showSlideUi: true,
1669
+ allowHtmlDownload: true
1670
+ } : {
1671
+ showSlideBrowser: userBuild.showSlideBrowser ?? true,
1672
+ showSlideUi: userBuild.showSlideUi ?? true,
1673
+ allowHtmlDownload: userBuild.allowHtmlDownload ?? true
1674
+ };
1675
+ const resolvedConfig = {
1676
+ ...config,
1677
+ build: buildResolved,
1678
+ version: coreVersion
1679
+ };
1680
+ return `export default ${JSON.stringify(resolvedConfig)};\n`;
1681
+ }
1682
+ if (id === resolved$1(FOLDERS_VMOD)) {
1683
+ const manifest = await readManifest(manifestPath);
1684
+ return `export default ${JSON.stringify(manifest)};\n`;
1685
+ }
1686
+ return null;
1687
+ },
1688
+ handleHotUpdate(ctx) {
1689
+ const slideId = slideIdForEntry(ctx.file);
1690
+ if (!slideId) return;
1691
+ if (hasRecentWrite(ctx.file)) return [];
1692
+ queueSlideChanged(ctx.server, slideId);
1693
+ return [];
1694
+ },
1695
+ configureServer(server) {
1696
+ const isSlideEntry = (p) => slideIdForEntry(p) !== null;
1697
+ let reloadTimer = null;
1698
+ const reload = () => {
1699
+ if (reloadTimer) clearTimeout(reloadTimer);
1700
+ reloadTimer = setTimeout(() => {
1701
+ reloadTimer = null;
1702
+ const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
1703
+ if (mod) server.moduleGraph.invalidateModule(mod);
1704
+ server.ws.send({ type: "full-reload" });
1705
+ }, 150);
1706
+ };
1707
+ if (existsSync(slidesRoot)) server.watcher.add(slidesRoot);
1708
+ server.watcher.on("add", (p) => {
1709
+ if (isSlideEntry(p) && !hasRecentWrite(p)) reload();
1710
+ });
1711
+ server.watcher.on("unlink", (p) => {
1712
+ if (isSlideEntry(p) && !hasRecentWrite(p)) reload();
1713
+ });
1714
+ let foldersTimer = null;
1715
+ const invalidateFolders = () => {
1716
+ if (foldersTimer) clearTimeout(foldersTimer);
1717
+ foldersTimer = setTimeout(() => {
1718
+ foldersTimer = null;
1719
+ const mod = server.moduleGraph.getModuleById(resolved$1(FOLDERS_VMOD));
1720
+ if (mod) server.moduleGraph.invalidateModule(mod);
1721
+ }, 100);
1722
+ };
1723
+ server.watcher.add(manifestPath);
1724
+ server.watcher.on("change", (p) => {
1725
+ if (p === manifestPath) invalidateFolders();
1726
+ });
1727
+ server.watcher.on("add", (p) => {
1728
+ if (p === manifestPath) invalidateFolders();
1729
+ });
1730
+ server.watcher.on("unlink", (p) => {
1731
+ if (p === manifestPath) invalidateFolders();
1732
+ });
1733
+ }
1734
+ };
1735
+ }
1736
+ function invalidateSlidesModule(server) {
1737
+ const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
1738
+ if (mod) server.moduleGraph.invalidateModule(mod);
1739
+ }
1740
+ async function loadUserConfig(userCwd) {
1741
+ const file = path.join(userCwd, CONFIG_FILE);
1742
+ if (!existsSync(file)) return {};
1743
+ return (await loadConfigFromFile({
1744
+ command: "serve",
1745
+ mode: "development"
1746
+ }, file, userCwd, "silent"))?.config ?? {};
1747
+ }
1748
+ //#endregion
1749
+ //#region src/vite/routes/agent.ts
1750
+ function parseContext(raw) {
1751
+ if (!raw || typeof raw !== "object") return void 0;
1752
+ const r = raw;
1753
+ const ctx = {};
1754
+ if (typeof r.slideId === "string" && SLIDE_ID_RE.test(r.slideId)) ctx.slideId = r.slideId;
1755
+ if (typeof r.slideTitle === "string") ctx.slideTitle = r.slideTitle.slice(0, 120);
1756
+ if (typeof r.pageNumber === "number" && Number.isFinite(r.pageNumber)) ctx.pageNumber = Math.max(1, Math.floor(r.pageNumber));
1757
+ if (typeof r.totalPages === "number" && Number.isFinite(r.totalPages)) ctx.totalPages = Math.max(1, Math.floor(r.totalPages));
1758
+ if (typeof r.pendingComments === "number" && Number.isFinite(r.pendingComments)) ctx.pendingComments = Math.max(0, Math.floor(r.pendingComments));
1759
+ if (r.newDeck === true) ctx.newDeck = true;
1760
+ const sel = r.selection;
1761
+ if (sel && typeof sel === "object") {
1762
+ const s = sel;
1763
+ if (typeof s.line === "number" && typeof s.column === "number") ctx.selection = {
1764
+ line: Math.max(1, Math.floor(s.line)),
1765
+ column: Math.max(0, Math.floor(s.column)),
1766
+ tagName: typeof s.tagName === "string" ? s.tagName.slice(0, 32) : "element",
1767
+ text: typeof s.text === "string" ? s.text.slice(0, 120) : ""
1768
+ };
1769
+ }
1770
+ return ctx;
1771
+ }
1772
+ const MODEL_RE = /^[\w.:[\]-]{1,80}$/;
1773
+ function parseModel(raw) {
1774
+ return typeof raw === "string" && MODEL_RE.test(raw) ? raw : null;
1775
+ }
1776
+ async function renamePlaceholderDeck(server, ctx, store, broadcast, turn) {
1777
+ const session = store.get(turn.sessionId);
1778
+ const slideId = session?.slideId;
1779
+ if (!session || !slideId || !UNTITLED_SLIDE_ID_RE.test(slideId)) return;
1780
+ const start = session.items.findIndex((i) => i.id === turn.userItemId);
1781
+ let marked = null;
1782
+ for (const item of session.items.slice(start + 1)) {
1783
+ if (item.kind !== "assistant") continue;
1784
+ const slug = extractDeckSlug(item.text);
1785
+ if (slug === null) continue;
1786
+ marked = slug;
1787
+ store.updateItem(session.id, item.id, "assistant", { text: stripDeckSlug(item.text) });
1788
+ }
1789
+ if (store.list().some((s) => s.slideId === slideId && s.status === "running")) return;
1790
+ const entry = resolveSlideEntry(ctx.slidesRoot, slideId);
1791
+ if (!entry) return;
1792
+ let source;
1793
+ try {
1794
+ source = await fs.readFile(entry, "utf8");
1795
+ } catch {
1796
+ return;
1797
+ }
1798
+ const meta = readMetaTitleInSource(source);
1799
+ if (meta.kind !== "found" || meta.title === "Untitled") return;
1800
+ const desired = slugifyTitle(marked ?? "") || slugifyTitle(meta.title);
1801
+ if (!desired || UNTITLED_SLIDE_ID_RE.test(desired)) return;
1802
+ if (store.list().some((s) => s.slideId === slideId && s.status === "running")) return;
1803
+ recordWrite(entry);
1804
+ const renamed = await renameSlideDir(ctx.slidesRoot, slideId, desired);
1805
+ if (!renamed.ok) return;
1806
+ recordWrite(path.join(renamed.to, "index.tsx"));
1807
+ const manifest = await readManifest(ctx.manifestPath);
1808
+ const folderId = manifest.assignments[slideId];
1809
+ if (folderId) {
1810
+ delete manifest.assignments[slideId];
1811
+ manifest.assignments[renamed.slideId] = folderId;
1812
+ await writeManifest(ctx.manifestPath, manifest);
1813
+ }
1814
+ for (const s of store.list()) if (s.slideId === slideId) store.patch(s.id, { slideId: renamed.slideId });
1815
+ invalidateSlidesModule(server);
1816
+ broadcast({
1817
+ type: "slide-renamed",
1818
+ from: slideId,
1819
+ to: renamed.slideId
1820
+ });
1821
+ }
1822
+ function parseRespond(raw) {
1823
+ if (!raw || typeof raw !== "object") return null;
1824
+ const r = raw;
1825
+ if (r.cancel === true) return { cancel: true };
1826
+ if (typeof r.allow === "boolean") return { allow: r.allow };
1827
+ if (r.answers && typeof r.answers === "object") {
1828
+ const answers = {};
1829
+ for (const [k, v] of Object.entries(r.answers)) if (typeof v === "string") answers[k] = v;
1830
+ return { answers };
1831
+ }
1832
+ return null;
1833
+ }
1834
+ function parseTurnSettings(raw) {
1835
+ const settings = {};
1836
+ const context = parseContext(raw.context);
1837
+ if (context) settings.context = context;
1838
+ if (typeof raw.autoApprove === "boolean") settings.autoApprove = raw.autoApprove;
1839
+ if ("model" in raw) settings.model = parseModel(raw.model);
1840
+ if ("effort" in raw) settings.effort = isAgentEffort(raw.effort) ? raw.effort : null;
1841
+ return settings;
1842
+ }
1843
+ const AGENT_BODY_LIMIT = 1048576;
1844
+ function registerAgentRoutes(server, ctx) {
1845
+ const providers = createProviders();
1846
+ const store = createSessionStore({ file: path.join(ctx.userCwd, "node_modules", ".open-slide", "agent-sessions.json") });
1847
+ const streams = /* @__PURE__ */ new Set();
1848
+ const broadcast = (event) => {
1849
+ const frame = `data: ${JSON.stringify(event)}\n\n`;
1850
+ for (const stream of streams) stream.write(frame);
1851
+ };
1852
+ const heartbeat = setInterval(() => {
1853
+ for (const stream of streams) stream.write(": ping\n\n");
1854
+ }, 25e3);
1855
+ heartbeat.unref();
1856
+ const runner = createAgentRunner({
1857
+ userCwd: ctx.userCwd,
1858
+ writableRoots: [ctx.slidesRoot, ctx.globalAssetsRoot],
1859
+ store,
1860
+ providers,
1861
+ onTurnEnd: (turn) => renamePlaceholderDeck(server, ctx, store, broadcast, turn)
1862
+ });
1863
+ store.subscribe(broadcast);
1864
+ server.httpServer?.on("close", () => {
1865
+ clearInterval(heartbeat);
1866
+ for (const stream of streams) stream.end();
1867
+ runner.stopAll();
1868
+ store.flush();
1869
+ });
1870
+ const installsInFlight = /* @__PURE__ */ new Map();
1871
+ server.middlewares.use("/__agent", async (req, res, next) => {
1872
+ const url = new URL(req.url ?? "/", "http://local");
1873
+ const method = req.method ?? "GET";
1874
+ const segments = url.pathname.split("/").filter(Boolean);
1875
+ const enabled = ctx.agentEnabled && isLoopbackRequest(req);
1876
+ try {
1877
+ if (method === "GET" && url.pathname === "/status") {
1878
+ res.setHeader("cache-control", "no-store");
1879
+ if (!enabled) return json(res, 200, {
1880
+ enabled: false,
1881
+ packageManager: "",
1882
+ providers: []
1883
+ });
1884
+ const pm = await detectPackageManager(ctx.userCwd);
1885
+ return json(res, 200, await agentStatus(ctx.userCwd, providers, pm));
1886
+ }
1887
+ if (!enabled) return json(res, 404, { error: "agent panel disabled" });
1888
+ if (method === "GET" && url.pathname === "/events") {
1889
+ res.writeHead(200, {
1890
+ "content-type": "text/event-stream",
1891
+ "cache-control": "no-store",
1892
+ connection: "keep-alive"
1893
+ });
1894
+ res.write(": connected\n\n");
1895
+ streams.add(res);
1896
+ const drop = () => streams.delete(res);
1897
+ res.on("close", drop);
1898
+ res.on("error", drop);
1899
+ return;
1900
+ }
1901
+ if (method === "POST" && url.pathname === "/install") {
1902
+ const guard = validateMutationRequest(req, { requireJsonBody: true });
1903
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
1904
+ const body = await readBody(req, AGENT_BODY_LIMIT);
1905
+ if (!isAgentProviderId(body.provider)) return json(res, 400, { error: "invalid provider" });
1906
+ const provider = providers[body.provider];
1907
+ const pm = await detectPackageManager(ctx.userCwd);
1908
+ const pkg = provider.sdkPackage;
1909
+ let install = installsInFlight.get(pkg);
1910
+ if (!install) {
1911
+ install = installSdk(pm, pkg, ctx.userCwd).finally(() => installsInFlight.delete(pkg));
1912
+ installsInFlight.set(pkg, install);
1913
+ }
1914
+ await install;
1915
+ return json(res, 200, { ok: true });
1916
+ }
1917
+ if (method === "GET" && url.pathname === "/sessions") {
1918
+ res.setHeader("cache-control", "no-store");
1919
+ return json(res, 200, { sessions: store.list() });
1920
+ }
1921
+ if (method === "POST" && url.pathname === "/sessions") {
1922
+ const guard = validateMutationRequest(req, { requireJsonBody: true });
1923
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
1924
+ const body = await readBody(req, AGENT_BODY_LIMIT);
1925
+ if (!isAgentProviderId(body.provider)) return json(res, 400, { error: "invalid provider" });
1926
+ const slideId = typeof body.slideId === "string" && SLIDE_ID_RE.test(body.slideId) ? body.slideId : null;
1927
+ return json(res, 200, store.create(body.provider, slideId));
1928
+ }
1929
+ if (segments[0] === "sessions" && segments[1]) {
1930
+ const id = segments[1];
1931
+ const action = segments[2];
1932
+ const session = store.get(id);
1933
+ if (!session) return json(res, 404, { error: "session not found" });
1934
+ if (method === "GET" && !action) {
1935
+ res.setHeader("cache-control", "no-store");
1936
+ return json(res, 200, session);
1937
+ }
1938
+ if (method === "DELETE" && !action) {
1939
+ const guard = validateMutationRequest(req);
1940
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
1941
+ runner.stop(id);
1942
+ store.remove(id);
1943
+ return json(res, 200, { ok: true });
1944
+ }
1945
+ if (method === "POST" && action === "messages") {
1946
+ const guard = validateMutationRequest(req, { requireJsonBody: true });
1947
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
1948
+ const body = await readBody(req, AGENT_BODY_LIMIT);
1949
+ if (typeof body.text !== "string" || !body.text.trim()) return json(res, 400, { error: "missing text" });
1950
+ if (runner.isRunning(id)) return json(res, 409, { error: "session is already running" });
1951
+ if (body.edit !== void 0 && !session.items.some((i) => i.id === body.edit && i.kind === "user")) return json(res, 400, { error: "unknown message" });
1952
+ await runner.send(id, {
1953
+ text: body.text,
1954
+ context: parseContext(body.context),
1955
+ autoApprove: body.autoApprove === true,
1956
+ model: parseModel(body.model),
1957
+ effort: isAgentEffort(body.effort) ? body.effort : null,
1958
+ edit: typeof body.edit === "string" ? body.edit : void 0
1959
+ });
1960
+ return json(res, 202, { ok: true });
1961
+ }
1962
+ if (method === "POST" && action === "stop") {
1963
+ const guard = validateMutationRequest(req);
1964
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
1965
+ return json(res, 200, { ok: runner.stop(id) });
1966
+ }
1967
+ if (method === "POST" && action === "respond") {
1968
+ const guard = validateMutationRequest(req, { requireJsonBody: true });
1969
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
1970
+ const body = await readBody(req, AGENT_BODY_LIMIT);
1971
+ const payload = parseRespond(body);
1972
+ if (typeof body.itemId !== "string" || !payload) return json(res, 400, { error: "invalid response" });
1973
+ const ok = runner.respond(id, body.itemId, payload, parseTurnSettings(body));
1974
+ return json(res, ok ? 200 : 409, ok ? { ok } : { error: "nothing pending" });
1975
+ }
1976
+ }
1977
+ next();
1978
+ } catch (err) {
1979
+ if (err instanceof BodyTooLargeError) return json(res, 413, { error: err.message });
1980
+ json(res, 500, { error: String(err.message ?? err) });
1981
+ }
1982
+ });
1983
+ }
1984
+ //#endregion
16
1985
  //#region src/editing/babel-walk.ts
17
1986
  const SKIP_KEYS = /* @__PURE__ */ new Set([
18
1987
  "loc",
@@ -2120,6 +4089,18 @@ function registerSlideRoutes(server, ctx) {
2120
4089
  const url = new URL(req.url ?? "/", "http://local");
2121
4090
  const method = req.method ?? "GET";
2122
4091
  try {
4092
+ if (url.pathname === "/" && method === "POST") {
4093
+ const requestCheck = validateMutationRequest(req);
4094
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
4095
+ const created = await createBlankSlideDir(ctx.slidesRoot);
4096
+ if (!created.ok) return json(res, created.status, { error: created.error });
4097
+ recordWrite(created.file);
4098
+ invalidateSlidesModule(server);
4099
+ return json(res, 200, {
4100
+ ok: true,
4101
+ slideId: created.slideId
4102
+ });
4103
+ }
2123
4104
  const reorderMatch = url.pathname.match(/^\/([^/]+)\/reorder$/);
2124
4105
  if (reorderMatch && method === "PUT") {
2125
4106
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
@@ -2341,6 +4322,7 @@ function apiPlugin(opts) {
2341
4322
  registerFolderRoutes(server, ctx);
2342
4323
  registerUpdateRoutes(server, ctx);
2343
4324
  registerRestartRoutes(server);
4325
+ registerAgentRoutes(server, ctx);
2344
4326
  }
2345
4327
  };
2346
4328
  }
@@ -2851,24 +4833,6 @@ function locTagsPlugin(opts) {
2851
4833
  };
2852
4834
  }
2853
4835
  //#endregion
2854
- //#region src/vite/recent-writes.ts
2855
- const recentWrites = /* @__PURE__ */ new Map();
2856
- /** Record that `file` was just written by us. */
2857
- function recordWrite(file, now = Date.now()) {
2858
- recentWrites.set(file, now);
2859
- }
2860
- /**
2861
- * True if `file` was recorded within the recent-write window. Expired entries
2862
- * are pruned so a stale path can no longer suppress a later genuine edit.
2863
- */
2864
- function hasRecentWrite(file, now = Date.now()) {
2865
- const ts = recentWrites.get(file);
2866
- if (ts == null) return false;
2867
- if (now - ts < 1500) return true;
2868
- recentWrites.delete(file);
2869
- return false;
2870
- }
2871
- //#endregion
2872
4836
  //#region src/vite/notes-plugin.ts
2873
4837
  function findNotesExport(ast) {
2874
4838
  for (const stmt of ast.program.body) {
@@ -2919,353 +4883,93 @@ function applyNotesEdit(source, index, text) {
2919
4883
  };
2920
4884
  const found = findNotesExport(ast);
2921
4885
  if (found && "error" in found) return {
2922
- ok: false,
2923
- status: 422,
2924
- error: found.error
2925
- };
2926
- const literal = renderNoteLiteral(text);
2927
- if (!found) {
2928
- if (text === "") return {
2929
- ok: true,
2930
- source
2931
- };
2932
- const block = [
2933
- "",
2934
- "",
2935
- "export const notes: (string | undefined)[] = [",
2936
- ...[...Array.from({ length: index }, () => "undefined"), literal].map((s) => ` ${s},`),
2937
- "];",
2938
- ""
2939
- ].join("\n");
2940
- const offset = findInsertionOffset(ast, source);
2941
- return {
2942
- ok: true,
2943
- source: source.slice(0, offset) + block + source.slice(offset)
2944
- };
2945
- }
2946
- const elementTexts = [];
2947
- for (const el of found.elements) {
2948
- if (el === null) {
2949
- elementTexts.push("undefined");
2950
- continue;
2951
- }
2952
- if (typeof el.start !== "number" || typeof el.end !== "number") return {
2953
- ok: false,
2954
- status: 422,
2955
- error: "`notes` element missing source range"
2956
- };
2957
- elementTexts.push(source.slice(el.start, el.end));
2958
- }
2959
- while (elementTexts.length <= index) elementTexts.push("undefined");
2960
- elementTexts[index] = literal;
2961
- while (elementTexts.length > 0 && elementTexts[elementTexts.length - 1] === "undefined") elementTexts.pop();
2962
- const replacement = elementTexts.length === 0 ? "[]" : `[\n${elementTexts.map((s) => ` ${s},`).join("\n")}\n]`;
2963
- return {
2964
- ok: true,
2965
- source: source.slice(0, found.arrayStart) + replacement + source.slice(found.arrayEnd)
2966
- };
2967
- }
2968
- function notesPlugin(opts) {
2969
- const slidesRoot = path.resolve(opts.userCwd, opts.slidesDir ?? "slides");
2970
- return {
2971
- name: "open-slide:notes",
2972
- apply: "serve",
2973
- handleHotUpdate(ctx) {
2974
- if (hasRecentWrite(ctx.file)) return [];
2975
- },
2976
- configureServer(server) {
2977
- server.middlewares.use("/__notes", async (req, res, next) => {
2978
- const url = new URL(req.url ?? "/", "http://local");
2979
- if ((req.method ?? "GET") !== "PUT" || url.pathname !== "/") return next();
2980
- const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2981
- if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2982
- try {
2983
- const body = await readBody(req);
2984
- const slideId = body.slideId ?? "";
2985
- const file = resolveSlideEntry(slidesRoot, slideId);
2986
- if (!file) return json(res, 400, { error: "invalid slideId" });
2987
- if (typeof body.index !== "number") return json(res, 400, { error: "missing index" });
2988
- if (typeof body.text !== "string") return json(res, 400, { error: "missing text" });
2989
- const source = await readSlideSource(file);
2990
- if (source === null) return json(res, 404, { error: "slide not found" });
2991
- const result = applyNotesEdit(source, body.index, body.text);
2992
- if (!result.ok) return json(res, result.status, { error: result.error });
2993
- const changed = result.source !== source;
2994
- if (changed) {
2995
- recordWrite(file);
2996
- await fs.writeFile(file, result.source, "utf8");
2997
- }
2998
- return json(res, 200, {
2999
- ok: true,
3000
- changed
3001
- });
3002
- } catch (err) {
3003
- json(res, 500, { error: String(err.message ?? err) });
3004
- }
3005
- });
3006
- }
3007
- };
3008
- }
3009
- //#endregion
3010
- //#region src/vite/open-slide-plugin.ts
3011
- const CONFIG_FILE = "open-slide.config.ts";
3012
- const SLIDES_VMOD = "virtual:open-slide/slides";
3013
- const CONFIG_VMOD = "virtual:open-slide/config";
3014
- const FOLDERS_VMOD = "virtual:open-slide/folders";
3015
- function resolved$1(id) {
3016
- return `\0${id}`;
3017
- }
3018
- async function findSlides(userCwd, slidesDir) {
3019
- const abs = path.resolve(userCwd, slidesDir);
3020
- if (!existsSync(abs)) return [];
3021
- return (await fg("*/index.{tsx,jsx,ts,js}", {
3022
- cwd: abs,
3023
- absolute: true,
3024
- onlyFiles: true
3025
- })).sort();
3026
- }
3027
- function toId(absFile, slidesRoot) {
3028
- return path.relative(slidesRoot, absFile).split(path.sep)[0];
3029
- }
3030
- const META_THEME_RE = /(?:^|[\s,{])theme\s*:\s*['"]([^'"]+)['"]/;
3031
- const META_CREATED_AT_RE = /(?:^|[\s,{])createdAt\s*:\s*['"]([^'"]+)['"]/;
3032
- function extractMeta(src) {
3033
- const empty = {
3034
- theme: null,
3035
- createdAt: null
3036
- };
3037
- const metaStart = src.search(/export\s+const\s+meta\b/);
3038
- if (metaStart === -1) return empty;
3039
- const eqIdx = src.indexOf("=", metaStart);
3040
- if (eqIdx === -1) return empty;
3041
- const openBrace = src.indexOf("{", eqIdx);
3042
- if (openBrace === -1) return empty;
3043
- let depth = 0;
3044
- let closeBrace = -1;
3045
- for (let i = openBrace; i < src.length; i++) {
3046
- const ch = src[i];
3047
- if (ch === "{") depth++;
3048
- else if (ch === "}") {
3049
- depth--;
3050
- if (depth === 0) {
3051
- closeBrace = i;
3052
- break;
3053
- }
3054
- }
3055
- }
3056
- if (closeBrace === -1) return empty;
3057
- const body = src.slice(openBrace + 1, closeBrace);
3058
- const themeMatch = body.match(META_THEME_RE);
3059
- const createdAtMatch = body.match(META_CREATED_AT_RE);
3060
- return {
3061
- theme: themeMatch ? themeMatch[1] : null,
3062
- createdAt: createdAtMatch ? createdAtMatch[1] : null
4886
+ ok: false,
4887
+ status: 422,
4888
+ error: found.error
3063
4889
  };
3064
- }
3065
- async function readSlideMeta(abs) {
3066
- try {
3067
- return extractMeta(await fs.readFile(abs, "utf8"));
3068
- } catch {
4890
+ const literal = renderNoteLiteral(text);
4891
+ if (!found) {
4892
+ if (text === "") return {
4893
+ ok: true,
4894
+ source
4895
+ };
4896
+ const block = [
4897
+ "",
4898
+ "",
4899
+ "export const notes: (string | undefined)[] = [",
4900
+ ...[...Array.from({ length: index }, () => "undefined"), literal].map((s) => ` ${s},`),
4901
+ "];",
4902
+ ""
4903
+ ].join("\n");
4904
+ const offset = findInsertionOffset(ast, source);
3069
4905
  return {
3070
- theme: null,
3071
- createdAt: null
4906
+ ok: true,
4907
+ source: source.slice(0, offset) + block + source.slice(offset)
3072
4908
  };
3073
4909
  }
3074
- }
3075
- function parseCreatedAtMs(iso) {
3076
- if (!iso) return null;
3077
- const ms = Date.parse(iso);
3078
- return Number.isFinite(ms) ? ms : null;
3079
- }
3080
- const warnedInvalidSlideIds = /* @__PURE__ */ new Set();
3081
- async function generateSlidesModule(files, slidesRoot, isDev) {
3082
- const scanned = await Promise.all(files.map(async (abs) => {
3083
- const id = toId(abs, slidesRoot);
3084
- const importPath = isDev ? `@fs/${normalizePath(abs).replace(/^\/+/, "")}` : abs;
3085
- const meta = await readSlideMeta(abs);
3086
- return {
3087
- id,
3088
- importPath,
3089
- theme: meta.theme,
3090
- createdAt: parseCreatedAtMs(meta.createdAt)
4910
+ const elementTexts = [];
4911
+ for (const el of found.elements) {
4912
+ if (el === null) {
4913
+ elementTexts.push("undefined");
4914
+ continue;
4915
+ }
4916
+ if (typeof el.start !== "number" || typeof el.end !== "number") return {
4917
+ ok: false,
4918
+ status: 422,
4919
+ error: "`notes` element missing source range"
3091
4920
  };
3092
- }));
3093
- const entries = scanned.filter((e) => SLIDE_ID_RE.test(e.id));
3094
- const ignored = scanned.filter((e) => !SLIDE_ID_RE.test(e.id)).map((e) => e.id);
3095
- const ids = JSON.stringify(entries.map((e) => e.id).sort());
3096
- const themesMap = {};
3097
- const createdAtMap = {};
3098
- for (const e of entries) {
3099
- if (e.theme) themesMap[e.id] = e.theme;
3100
- if (e.createdAt !== null) createdAtMap[e.id] = e.createdAt;
4921
+ elementTexts.push(source.slice(el.start, el.end));
3101
4922
  }
3102
- const themesJson = JSON.stringify(themesMap);
3103
- const createdAtJson = JSON.stringify(createdAtMap);
3104
- const importTokens = JSON.stringify(Object.fromEntries(entries.map((e) => [e.id, 0])));
4923
+ while (elementTexts.length <= index) elementTexts.push("undefined");
4924
+ elementTexts[index] = literal;
4925
+ while (elementTexts.length > 0 && elementTexts[elementTexts.length - 1] === "undefined") elementTexts.pop();
4926
+ const replacement = elementTexts.length === 0 ? "[]" : `[\n${elementTexts.map((s) => ` ${s},`).join("\n")}\n]`;
3105
4927
  return {
3106
- code: `// virtual:open-slide/slides — generated
3107
- export const slideIds = ${ids};
3108
- export const slideThemes = ${themesJson};
3109
- export const slideCreatedAt = ${createdAtJson};
3110
- ${isDev ? `
3111
- const slideImportTokens = ${importTokens};
3112
- if (import.meta.hot) {
3113
- import.meta.hot.on('open-slide:slide-changed', (data) => {
3114
- const ids = Array.isArray(data?.slideIds) ? data.slideIds : [];
3115
- const token = Date.now();
3116
- for (const id of ids) {
3117
- if (Object.prototype.hasOwnProperty.call(slideImportTokens, id)) slideImportTokens[id] = token;
3118
- }
3119
- });
3120
- }
3121
- ` : ""}
3122
-
3123
- export async function loadSlide(id) {
3124
- switch (id) {
3125
- ${entries.map((e) => {
3126
- const importExpr = isDev ? `import(/* @vite-ignore */ import.meta.env.BASE_URL + ${JSON.stringify(`${e.importPath}?t=`)} + slideImportTokens[${JSON.stringify(e.id)}])` : `import(${JSON.stringify(e.importPath)})`;
3127
- return ` case ${JSON.stringify(e.id)}: return ${importExpr};`;
3128
- }).join("\n")}
3129
- default: throw new Error('Slide not found: ' + id);
3130
- }
3131
- }
3132
- `,
3133
- ignored
4928
+ ok: true,
4929
+ source: source.slice(0, found.arrayStart) + replacement + source.slice(found.arrayEnd)
3134
4930
  };
3135
4931
  }
3136
- function openSlidePlugin(opts) {
3137
- const { userCwd, config, coreVersion } = opts;
3138
- const slidesDir = config.slidesDir ?? "slides";
3139
- const slidesRoot = path.resolve(userCwd, slidesDir);
3140
- const manifestPath = foldersManifestPath(slidesRoot);
3141
- let isDev = false;
3142
- const slideIdForEntry = (p) => {
3143
- const rel = path.relative(slidesRoot, p);
3144
- if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
3145
- const parts = rel.split(path.sep);
3146
- if (parts.length !== 2) return null;
3147
- if (!/^index\.(tsx|jsx|ts|js)$/.test(parts[1])) return null;
3148
- return parts[0];
3149
- };
3150
- let slideChangeTimer = null;
3151
- const pendingSlideChanges = /* @__PURE__ */ new Set();
3152
- const queueSlideChanged = (server, id) => {
3153
- pendingSlideChanges.add(id);
3154
- if (slideChangeTimer) clearTimeout(slideChangeTimer);
3155
- slideChangeTimer = setTimeout(() => {
3156
- slideChangeTimer = null;
3157
- const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
3158
- if (mod) server.moduleGraph.invalidateModule(mod);
3159
- const slideIds = Array.from(pendingSlideChanges);
3160
- pendingSlideChanges.clear();
3161
- server.ws.send({
3162
- type: "custom",
3163
- event: "open-slide:slide-changed",
3164
- data: { slideIds }
3165
- });
3166
- }, 100);
3167
- };
4932
+ function notesPlugin(opts) {
4933
+ const slidesRoot = path.resolve(opts.userCwd, opts.slidesDir ?? "slides");
3168
4934
  return {
3169
- name: "open-slide",
3170
- config(_c, env) {
3171
- isDev = env.command === "serve";
3172
- },
3173
- resolveId(id) {
3174
- if (id === SLIDES_VMOD) return resolved$1(SLIDES_VMOD);
3175
- if (id === CONFIG_VMOD) return resolved$1(CONFIG_VMOD);
3176
- if (id === FOLDERS_VMOD) return resolved$1(FOLDERS_VMOD);
3177
- return null;
3178
- },
3179
- async load(id) {
3180
- if (id === resolved$1(SLIDES_VMOD)) {
3181
- const { code, ignored } = await generateSlidesModule(await findSlides(userCwd, slidesDir), slidesRoot, isDev);
3182
- for (const slideId of ignored) {
3183
- if (warnedInvalidSlideIds.has(slideId)) continue;
3184
- warnedInvalidSlideIds.add(slideId);
3185
- this.warn(`Ignoring slide folder "${slideId}": slide ids must match ${SLIDE_ID_RE} (lowercase/uppercase letters, digits, "-", "_"). Rename the folder under "${slidesDir}/" to a kebab-case id so it appears in the browser and can be moved into folders.`);
3186
- }
3187
- return code;
3188
- }
3189
- if (id === resolved$1(CONFIG_VMOD)) {
3190
- const userBuild = config.build ?? {};
3191
- const buildResolved = isDev ? {
3192
- showSlideBrowser: true,
3193
- showSlideUi: true,
3194
- allowHtmlDownload: true
3195
- } : {
3196
- showSlideBrowser: userBuild.showSlideBrowser ?? true,
3197
- showSlideUi: userBuild.showSlideUi ?? true,
3198
- allowHtmlDownload: userBuild.allowHtmlDownload ?? true
3199
- };
3200
- const resolvedConfig = {
3201
- ...config,
3202
- build: buildResolved,
3203
- version: coreVersion
3204
- };
3205
- return `export default ${JSON.stringify(resolvedConfig)};\n`;
3206
- }
3207
- if (id === resolved$1(FOLDERS_VMOD)) {
3208
- const manifest = await readManifest(manifestPath);
3209
- return `export default ${JSON.stringify(manifest)};\n`;
3210
- }
3211
- return null;
3212
- },
4935
+ name: "open-slide:notes",
4936
+ apply: "serve",
3213
4937
  handleHotUpdate(ctx) {
3214
- const slideId = slideIdForEntry(ctx.file);
3215
- if (!slideId) return;
3216
4938
  if (hasRecentWrite(ctx.file)) return [];
3217
- queueSlideChanged(ctx.server, slideId);
3218
- return [];
3219
4939
  },
3220
4940
  configureServer(server) {
3221
- const isSlideEntry = (p) => slideIdForEntry(p) !== null;
3222
- let reloadTimer = null;
3223
- const reload = () => {
3224
- if (reloadTimer) clearTimeout(reloadTimer);
3225
- reloadTimer = setTimeout(() => {
3226
- reloadTimer = null;
3227
- const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
3228
- if (mod) server.moduleGraph.invalidateModule(mod);
3229
- server.ws.send({ type: "full-reload" });
3230
- }, 150);
3231
- };
3232
- if (existsSync(slidesRoot)) server.watcher.add(slidesRoot);
3233
- server.watcher.on("add", (p) => {
3234
- if (isSlideEntry(p)) reload();
3235
- });
3236
- server.watcher.on("unlink", (p) => {
3237
- if (isSlideEntry(p)) reload();
3238
- });
3239
- let foldersTimer = null;
3240
- const invalidateFolders = () => {
3241
- if (foldersTimer) clearTimeout(foldersTimer);
3242
- foldersTimer = setTimeout(() => {
3243
- foldersTimer = null;
3244
- const mod = server.moduleGraph.getModuleById(resolved$1(FOLDERS_VMOD));
3245
- if (mod) server.moduleGraph.invalidateModule(mod);
3246
- }, 100);
3247
- };
3248
- server.watcher.add(manifestPath);
3249
- server.watcher.on("change", (p) => {
3250
- if (p === manifestPath) invalidateFolders();
3251
- });
3252
- server.watcher.on("add", (p) => {
3253
- if (p === manifestPath) invalidateFolders();
3254
- });
3255
- server.watcher.on("unlink", (p) => {
3256
- if (p === manifestPath) invalidateFolders();
4941
+ server.middlewares.use("/__notes", async (req, res, next) => {
4942
+ const url = new URL(req.url ?? "/", "http://local");
4943
+ if ((req.method ?? "GET") !== "PUT" || url.pathname !== "/") return next();
4944
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
4945
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
4946
+ try {
4947
+ const body = await readBody(req);
4948
+ const slideId = body.slideId ?? "";
4949
+ const file = resolveSlideEntry(slidesRoot, slideId);
4950
+ if (!file) return json(res, 400, { error: "invalid slideId" });
4951
+ if (typeof body.index !== "number") return json(res, 400, { error: "missing index" });
4952
+ if (typeof body.text !== "string") return json(res, 400, { error: "missing text" });
4953
+ const source = await readSlideSource(file);
4954
+ if (source === null) return json(res, 404, { error: "slide not found" });
4955
+ const result = applyNotesEdit(source, body.index, body.text);
4956
+ if (!result.ok) return json(res, result.status, { error: result.error });
4957
+ const changed = result.source !== source;
4958
+ if (changed) {
4959
+ recordWrite(file);
4960
+ await fs.writeFile(file, result.source, "utf8");
4961
+ }
4962
+ return json(res, 200, {
4963
+ ok: true,
4964
+ changed
4965
+ });
4966
+ } catch (err) {
4967
+ json(res, 500, { error: String(err.message ?? err) });
4968
+ }
3257
4969
  });
3258
4970
  }
3259
4971
  };
3260
4972
  }
3261
- async function loadUserConfig(userCwd) {
3262
- const file = path.join(userCwd, CONFIG_FILE);
3263
- if (!existsSync(file)) return {};
3264
- return (await loadConfigFromFile({
3265
- command: "serve",
3266
- mode: "development"
3267
- }, file, userCwd, "silent"))?.config ?? {};
3268
- }
3269
4973
  //#endregion
3270
4974
  //#region src/vite/server-info-plugin.ts
3271
4975
  function serverInfoUrl(address, https, base) {
@@ -3527,7 +5231,8 @@ async function createViteConfig(opts) {
3527
5231
  userCwd,
3528
5232
  slidesDir,
3529
5233
  assetsDir,
3530
- coreVersion: CORE_VERSION
5234
+ coreVersion: CORE_VERSION,
5235
+ agentEnabled: config.agent?.enabled ?? true
3531
5236
  }),
3532
5237
  notesPlugin({
3533
5238
  userCwd,
@@ -3561,7 +5266,8 @@ async function createViteConfig(opts) {
3561
5266
  "clsx",
3562
5267
  "tailwind-merge",
3563
5268
  "class-variance-authority",
3564
- "emoji-picker-react"
5269
+ "emoji-picker-react",
5270
+ "pele"
3565
5271
  ],
3566
5272
  rolldownOptions: { plugins: [{
3567
5273
  name: "open-slide:virtual-externals",