@lemonriver/core 0.1.1 → 0.2.0

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 (82) hide show
  1. package/dist/{build-f-fM2ZY4.js → build-Df_xwIpg.js} +1 -1
  2. package/dist/cli/bin.js +4 -4
  3. package/dist/{config-DO45GU3e.js → config-BjWCLoMQ.js} +2343 -363
  4. package/dist/{config-B6JaKJM4.d.ts → config-rMGCkz6i.d.ts} +6 -2
  5. package/dist/{dev-CqS2xpgE.js → dev-Ctsx9IeO.js} +2 -2
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +4 -3
  8. package/dist/locale/index.d.ts +5 -2
  9. package/dist/locale/index.js +2 -2
  10. package/dist/{preview-BihgMlEl.js → preview-CJgcqsPk.js} +1 -1
  11. package/dist/{types-BjW4glip.d.ts → types-8VUyA2bu.d.ts} +133 -5
  12. package/dist/{update-C_R0ycVC.js → update-BKOA3Z6o.js} +153 -21
  13. package/dist/vite/index.d.ts +1 -1
  14. package/dist/vite/index.js +1 -1
  15. package/dist/{zh-tw-Ci0jK8eg.js → zh-tw-CeLrYwRE.js} +1178 -25
  16. package/package.json +13 -6
  17. package/skills/apply-comments/SKILL.md +10 -6
  18. package/skills/create-slide/SKILL.md +2 -0
  19. package/skills/current-slide/SKILL.md +1 -0
  20. package/skills/slide-authoring/SKILL.md +31 -1
  21. package/src/agent/prompt.ts +141 -0
  22. package/src/agent/provider.ts +123 -0
  23. package/src/agent/types.ts +151 -0
  24. package/src/app/app.tsx +2 -0
  25. package/src/app/assets/open-slide.svg +39 -0
  26. package/src/app/components/agent/agent-dock.tsx +35 -0
  27. package/src/app/components/agent/agent-markdown.tsx +45 -0
  28. package/src/app/components/agent/agent-panel.tsx +692 -0
  29. package/src/app/components/agent/agent-presence.tsx +74 -0
  30. package/src/app/components/agent/composer.tsx +120 -0
  31. package/src/app/components/agent/connect-view.tsx +184 -0
  32. package/src/app/components/agent/history-view.tsx +135 -0
  33. package/src/app/components/agent/provider-logo.tsx +34 -0
  34. package/src/app/components/agent/transcript.tsx +609 -0
  35. package/src/app/components/board/board-canvas.tsx +143 -12
  36. package/src/app/components/board/board.css +51 -0
  37. package/src/app/components/board/layout.test.ts +53 -1
  38. package/src/app/components/board/layout.ts +50 -2
  39. package/src/app/components/board/page-node.tsx +287 -13
  40. package/src/app/components/board/use-board-cards.ts +210 -0
  41. package/src/app/components/board/use-page-order.ts +17 -9
  42. package/src/app/components/command/slide-command-menu.tsx +10 -0
  43. package/src/app/components/icon-switcher-indicator.tsx +1 -1
  44. package/src/app/components/inspector/comment-layer.tsx +1011 -0
  45. package/src/app/components/inspector/image-crop-dialog.tsx +1 -5
  46. package/src/app/components/inspector/inspect-overlay.tsx +2 -39
  47. package/src/app/components/inspector/inspector-panel.tsx +10 -93
  48. package/src/app/components/inspector/inspector-provider.tsx +121 -47
  49. package/src/app/components/panel/editor-sidebar.tsx +85 -16
  50. package/src/app/components/panel/panel-shell.tsx +2 -2
  51. package/src/app/components/panel/resize-handle.tsx +129 -0
  52. package/src/app/components/player.tsx +0 -1
  53. package/src/app/components/preview-step-host.tsx +16 -0
  54. package/src/app/components/sidebar/sidebar.tsx +2 -2
  55. package/src/app/components/style-panel/style-panel.tsx +3 -2
  56. package/src/app/components/thumbnail-rail.tsx +76 -1
  57. package/src/app/favicon.ico +0 -0
  58. package/src/app/favicon.svg +39 -0
  59. package/src/app/index.html +2 -1
  60. package/src/app/lib/agent/use-agent.ts +543 -0
  61. package/src/app/lib/inspector/freeze-motion.ts +61 -0
  62. package/src/app/lib/inspector/use-comments.ts +32 -3
  63. package/src/app/lib/locale-store.ts +6 -1
  64. package/src/app/lib/print-ready.ts +9 -6
  65. package/src/app/lib/use-auto-grow.ts +15 -0
  66. package/src/app/lib/utils.ts +4 -0
  67. package/src/app/routes/board.tsx +48 -11
  68. package/src/app/routes/home.tsx +43 -7
  69. package/src/app/routes/presenter.tsx +1 -16
  70. package/src/app/routes/preview.tsx +169 -0
  71. package/src/app/routes/slide.tsx +221 -173
  72. package/src/app/styles.css +234 -80
  73. package/src/editing/placeholder-id.ts +1 -0
  74. package/src/locale/en.ts +127 -5
  75. package/src/locale/es.ts +692 -0
  76. package/src/locale/index.ts +1 -0
  77. package/src/locale/ja.ts +120 -6
  78. package/src/locale/types.ts +132 -5
  79. package/src/locale/zh-cn.ts +117 -5
  80. package/src/locale/zh-tw.ts +117 -5
  81. package/src/app/assets/open-slide.png +0 -0
  82. package/src/app/components/inspector/comment-widget.tsx +0 -119
@@ -1,18 +1,1986 @@
1
1
  import { n as defaultDesign } from "./design-Cpqx7SJs.js";
2
- import { A as validateSlideName, C as removeNotesElementInSource, D as resolveSlideEntry, E as reorderNotesArrayInSource, I as isFrameworkSpecifier, M as validateMutationRequest, O as rmSlideDir, P as CONFIG_FILES, S as insertNotesElementInSource, T as reorderDefaultExportPagesInSource, _ 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 declaredFrameworkSpecifier, k as updateMetaTitleInSource, 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 removePageFromDefaultExportInSource, x as insertBlankPageInDefaultExportInSource, y as duplicatePageInDefaultExportInSource } from "./update-C_R0ycVC.js";
2
+ import { A as renameSlideDir, B as validateMutationRequest, C as duplicatePageInDefaultExportInSource, D as readMetaTitleInSource, E as insertNotesElementInSource, F as slugifyTitle, H as CONFIG_FILES, I as updateMetaTitleInSource, L as validateSlideName, M as reorderNotesArrayInSource, N as resolveSlideEntry, O as removeNotesElementInSource, P as rmSlideDir, R as declaredFrameworkSpecifier, S as duplicateNotesElementInSource, T as insertBlankPageInDefaultExportInSource, W as isFrameworkSpecifier, _ 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 reorderDefaultExportPagesInSource, k as removePageFromDefaultExportInSource, 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 isLoopbackRequest } from "./update-BKOA3Z6o.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 } 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 LemonRiver renames the folder from it.";
86
+ function systemPromptAppend(askInstruction) {
87
+ return [
88
+ "You are running inside the LemonRiver dev server, driven from its in-app agent panel in the browser rather than a terminal.",
89
+ "The workspace is a LemonRiver 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 LemonRiver 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$1 = 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$1 ? `${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("[lemonriver] 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("[lemonriver] 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("[lemonriver] 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 SLIDES_VMOD = "virtual:open-slide/slides";
1487
+ const CONFIG_VMOD = "virtual:open-slide/config";
1488
+ const FOLDERS_VMOD = "virtual:open-slide/folders";
1489
+ function resolved$1(id) {
1490
+ return `\0${id}`;
1491
+ }
1492
+ async function findSlides(userCwd, slidesDir) {
1493
+ const abs = path.resolve(userCwd, slidesDir);
1494
+ if (!existsSync(abs)) return [];
1495
+ return (await fg("*/index.{tsx,jsx,ts,js}", {
1496
+ cwd: abs,
1497
+ absolute: true,
1498
+ onlyFiles: true
1499
+ })).sort();
1500
+ }
1501
+ function toId(absFile, slidesRoot) {
1502
+ return path.relative(slidesRoot, absFile).split(path.sep)[0];
1503
+ }
1504
+ const META_THEME_RE = /(?:^|[\s,{])theme\s*:\s*['"]([^'"]+)['"]/;
1505
+ const META_CREATED_AT_RE = /(?:^|[\s,{])createdAt\s*:\s*['"]([^'"]+)['"]/;
1506
+ function extractMeta(src) {
1507
+ const empty = {
1508
+ theme: null,
1509
+ createdAt: null
1510
+ };
1511
+ const metaStart = src.search(/export\s+const\s+meta\b/);
1512
+ if (metaStart === -1) return empty;
1513
+ const eqIdx = src.indexOf("=", metaStart);
1514
+ if (eqIdx === -1) return empty;
1515
+ const openBrace = src.indexOf("{", eqIdx);
1516
+ if (openBrace === -1) return empty;
1517
+ let depth = 0;
1518
+ let closeBrace = -1;
1519
+ for (let i = openBrace; i < src.length; i++) {
1520
+ const ch = src[i];
1521
+ if (ch === "{") depth++;
1522
+ else if (ch === "}") {
1523
+ depth--;
1524
+ if (depth === 0) {
1525
+ closeBrace = i;
1526
+ break;
1527
+ }
1528
+ }
1529
+ }
1530
+ if (closeBrace === -1) return empty;
1531
+ const body = src.slice(openBrace + 1, closeBrace);
1532
+ const themeMatch = body.match(META_THEME_RE);
1533
+ const createdAtMatch = body.match(META_CREATED_AT_RE);
1534
+ return {
1535
+ theme: themeMatch ? themeMatch[1] : null,
1536
+ createdAt: createdAtMatch ? createdAtMatch[1] : null
1537
+ };
1538
+ }
1539
+ async function readSlideMeta(abs) {
1540
+ try {
1541
+ return extractMeta(await fs.readFile(abs, "utf8"));
1542
+ } catch {
1543
+ return {
1544
+ theme: null,
1545
+ createdAt: null
1546
+ };
1547
+ }
1548
+ }
1549
+ function parseCreatedAtMs(iso) {
1550
+ if (!iso) return null;
1551
+ const ms = Date.parse(iso);
1552
+ return Number.isFinite(ms) ? ms : null;
1553
+ }
1554
+ const warnedInvalidSlideIds = /* @__PURE__ */ new Set();
1555
+ async function generateSlidesModule(files, slidesRoot, isDev) {
1556
+ const scanned = await Promise.all(files.map(async (abs) => {
1557
+ const id = toId(abs, slidesRoot);
1558
+ const importPath = isDev ? `@fs/${normalizePath(abs).replace(/^\/+/, "")}` : abs;
1559
+ const meta = await readSlideMeta(abs);
1560
+ return {
1561
+ id,
1562
+ importPath,
1563
+ theme: meta.theme,
1564
+ createdAt: parseCreatedAtMs(meta.createdAt)
1565
+ };
1566
+ }));
1567
+ const entries = scanned.filter((e) => SLIDE_ID_RE.test(e.id));
1568
+ const ignored = scanned.filter((e) => !SLIDE_ID_RE.test(e.id)).map((e) => e.id);
1569
+ const ids = JSON.stringify(entries.map((e) => e.id).sort());
1570
+ const themesMap = {};
1571
+ const createdAtMap = {};
1572
+ for (const e of entries) {
1573
+ if (e.theme) themesMap[e.id] = e.theme;
1574
+ if (e.createdAt !== null) createdAtMap[e.id] = e.createdAt;
1575
+ }
1576
+ const themesJson = JSON.stringify(themesMap);
1577
+ const createdAtJson = JSON.stringify(createdAtMap);
1578
+ const importTokens = JSON.stringify(Object.fromEntries(entries.map((e) => [e.id, 0])));
1579
+ return {
1580
+ code: `// virtual:open-slide/slides — generated
1581
+ export const slideIds = ${ids};
1582
+ export const slideThemes = ${themesJson};
1583
+ export const slideCreatedAt = ${createdAtJson};
1584
+ ${isDev ? `
1585
+ const slideImportTokens = ${importTokens};
1586
+ if (import.meta.hot) {
1587
+ import.meta.hot.on('open-slide:slide-changed', (data) => {
1588
+ const ids = Array.isArray(data?.slideIds) ? data.slideIds : [];
1589
+ const token = Date.now();
1590
+ for (const id of ids) {
1591
+ if (Object.prototype.hasOwnProperty.call(slideImportTokens, id)) slideImportTokens[id] = token;
1592
+ }
1593
+ });
1594
+ }
1595
+ ` : ""}
1596
+
1597
+ export async function loadSlide(id) {
1598
+ switch (id) {
1599
+ ${entries.map((e) => {
1600
+ 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)})`;
1601
+ return ` case ${JSON.stringify(e.id)}: return ${importExpr};`;
1602
+ }).join("\n")}
1603
+ default: throw new Error('Slide not found: ' + id);
1604
+ }
1605
+ }
1606
+ `,
1607
+ ignored
1608
+ };
1609
+ }
1610
+ function lemonRiverPlugin(opts) {
1611
+ const { userCwd, config, coreVersion } = opts;
1612
+ const slidesDir = config.slidesDir ?? "slides";
1613
+ const slidesRoot = path.resolve(userCwd, slidesDir);
1614
+ const manifestPath = foldersManifestPath(slidesRoot);
1615
+ let isDev = false;
1616
+ const slideIdForEntry = (p) => {
1617
+ const rel = path.relative(slidesRoot, p);
1618
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
1619
+ const parts = rel.split(path.sep);
1620
+ if (parts.length !== 2) return null;
1621
+ if (!/^index\.(tsx|jsx|ts|js)$/.test(parts[1])) return null;
1622
+ return parts[0];
1623
+ };
1624
+ let slideChangeTimer = null;
1625
+ const pendingSlideChanges = /* @__PURE__ */ new Set();
1626
+ const queueSlideChanged = (server, id) => {
1627
+ pendingSlideChanges.add(id);
1628
+ if (slideChangeTimer) clearTimeout(slideChangeTimer);
1629
+ slideChangeTimer = setTimeout(() => {
1630
+ slideChangeTimer = null;
1631
+ const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
1632
+ if (mod) server.moduleGraph.invalidateModule(mod);
1633
+ const slideIds = Array.from(pendingSlideChanges);
1634
+ pendingSlideChanges.clear();
1635
+ server.ws.send({
1636
+ type: "custom",
1637
+ event: "open-slide:slide-changed",
1638
+ data: { slideIds }
1639
+ });
1640
+ }, 100);
1641
+ };
1642
+ return {
1643
+ name: "lemonriver",
1644
+ config(_c, env) {
1645
+ isDev = env.command === "serve";
1646
+ },
1647
+ resolveId(id) {
1648
+ if (id === SLIDES_VMOD) return resolved$1(SLIDES_VMOD);
1649
+ if (id === CONFIG_VMOD) return resolved$1(CONFIG_VMOD);
1650
+ if (id === FOLDERS_VMOD) return resolved$1(FOLDERS_VMOD);
1651
+ return null;
1652
+ },
1653
+ async load(id) {
1654
+ if (id === resolved$1(SLIDES_VMOD)) {
1655
+ const { code, ignored } = await generateSlidesModule(await findSlides(userCwd, slidesDir), slidesRoot, isDev);
1656
+ for (const slideId of ignored) {
1657
+ if (warnedInvalidSlideIds.has(slideId)) continue;
1658
+ warnedInvalidSlideIds.add(slideId);
1659
+ 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.`);
1660
+ }
1661
+ return code;
1662
+ }
1663
+ if (id === resolved$1(CONFIG_VMOD)) {
1664
+ const userBuild = config.build ?? {};
1665
+ const buildResolved = isDev ? {
1666
+ showSlideBrowser: true,
1667
+ showSlideUi: true,
1668
+ allowHtmlDownload: true
1669
+ } : {
1670
+ showSlideBrowser: userBuild.showSlideBrowser ?? true,
1671
+ showSlideUi: userBuild.showSlideUi ?? true,
1672
+ allowHtmlDownload: userBuild.allowHtmlDownload ?? true
1673
+ };
1674
+ const resolvedConfig = {
1675
+ ...config,
1676
+ build: buildResolved,
1677
+ version: coreVersion
1678
+ };
1679
+ return `export default ${JSON.stringify(resolvedConfig)};\n`;
1680
+ }
1681
+ if (id === resolved$1(FOLDERS_VMOD)) {
1682
+ const manifest = await readManifest(manifestPath);
1683
+ return `export default ${JSON.stringify(manifest)};\n`;
1684
+ }
1685
+ return null;
1686
+ },
1687
+ handleHotUpdate(ctx) {
1688
+ const slideId = slideIdForEntry(ctx.file);
1689
+ if (!slideId) return;
1690
+ if (hasRecentWrite(ctx.file)) return [];
1691
+ queueSlideChanged(ctx.server, slideId);
1692
+ return [];
1693
+ },
1694
+ configureServer(server) {
1695
+ const isSlideEntry = (p) => slideIdForEntry(p) !== null;
1696
+ let reloadTimer = null;
1697
+ const reload = () => {
1698
+ if (reloadTimer) clearTimeout(reloadTimer);
1699
+ reloadTimer = setTimeout(() => {
1700
+ reloadTimer = null;
1701
+ const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
1702
+ if (mod) server.moduleGraph.invalidateModule(mod);
1703
+ server.ws.send({ type: "full-reload" });
1704
+ }, 150);
1705
+ };
1706
+ if (existsSync(slidesRoot)) server.watcher.add(slidesRoot);
1707
+ server.watcher.on("add", (p) => {
1708
+ if (isSlideEntry(p) && !hasRecentWrite(p)) reload();
1709
+ });
1710
+ server.watcher.on("unlink", (p) => {
1711
+ if (isSlideEntry(p) && !hasRecentWrite(p)) reload();
1712
+ });
1713
+ let foldersTimer = null;
1714
+ const invalidateFolders = () => {
1715
+ if (foldersTimer) clearTimeout(foldersTimer);
1716
+ foldersTimer = setTimeout(() => {
1717
+ foldersTimer = null;
1718
+ const mod = server.moduleGraph.getModuleById(resolved$1(FOLDERS_VMOD));
1719
+ if (mod) server.moduleGraph.invalidateModule(mod);
1720
+ }, 100);
1721
+ };
1722
+ server.watcher.add(manifestPath);
1723
+ server.watcher.on("change", (p) => {
1724
+ if (p === manifestPath) invalidateFolders();
1725
+ });
1726
+ server.watcher.on("add", (p) => {
1727
+ if (p === manifestPath) invalidateFolders();
1728
+ });
1729
+ server.watcher.on("unlink", (p) => {
1730
+ if (p === manifestPath) invalidateFolders();
1731
+ });
1732
+ }
1733
+ };
1734
+ }
1735
+ function invalidateSlidesModule(server) {
1736
+ const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
1737
+ if (mod) server.moduleGraph.invalidateModule(mod);
1738
+ }
1739
+ async function loadUserConfig(userCwd) {
1740
+ const file = CONFIG_FILES.map((name) => path.join(userCwd, name)).find((p) => existsSync(p));
1741
+ if (!file) return {};
1742
+ return (await loadConfigFromFile({
1743
+ command: "serve",
1744
+ mode: "development"
1745
+ }, file, userCwd, "silent"))?.config ?? {};
1746
+ }
1747
+ //#endregion
1748
+ //#region src/vite/routes/agent.ts
1749
+ function parseContext(raw) {
1750
+ if (!raw || typeof raw !== "object") return void 0;
1751
+ const r = raw;
1752
+ const ctx = {};
1753
+ if (typeof r.slideId === "string" && SLIDE_ID_RE.test(r.slideId)) ctx.slideId = r.slideId;
1754
+ if (typeof r.slideTitle === "string") ctx.slideTitle = r.slideTitle.slice(0, 120);
1755
+ if (typeof r.pageNumber === "number" && Number.isFinite(r.pageNumber)) ctx.pageNumber = Math.max(1, Math.floor(r.pageNumber));
1756
+ if (typeof r.totalPages === "number" && Number.isFinite(r.totalPages)) ctx.totalPages = Math.max(1, Math.floor(r.totalPages));
1757
+ if (typeof r.pendingComments === "number" && Number.isFinite(r.pendingComments)) ctx.pendingComments = Math.max(0, Math.floor(r.pendingComments));
1758
+ if (r.newDeck === true) ctx.newDeck = true;
1759
+ const sel = r.selection;
1760
+ if (sel && typeof sel === "object") {
1761
+ const s = sel;
1762
+ if (typeof s.line === "number" && typeof s.column === "number") ctx.selection = {
1763
+ line: Math.max(1, Math.floor(s.line)),
1764
+ column: Math.max(0, Math.floor(s.column)),
1765
+ tagName: typeof s.tagName === "string" ? s.tagName.slice(0, 32) : "element",
1766
+ text: typeof s.text === "string" ? s.text.slice(0, 120) : ""
1767
+ };
1768
+ }
1769
+ return ctx;
1770
+ }
1771
+ const MODEL_RE = /^[\w.:[\]-]{1,80}$/;
1772
+ function parseModel(raw) {
1773
+ return typeof raw === "string" && MODEL_RE.test(raw) ? raw : null;
1774
+ }
1775
+ async function renamePlaceholderDeck(server, ctx, store, broadcast, turn) {
1776
+ const session = store.get(turn.sessionId);
1777
+ const slideId = session?.slideId;
1778
+ if (!session || !slideId || !UNTITLED_SLIDE_ID_RE.test(slideId)) return;
1779
+ const start = session.items.findIndex((i) => i.id === turn.userItemId);
1780
+ let marked = null;
1781
+ for (const item of session.items.slice(start + 1)) {
1782
+ if (item.kind !== "assistant") continue;
1783
+ const slug = extractDeckSlug(item.text);
1784
+ if (slug === null) continue;
1785
+ marked = slug;
1786
+ store.updateItem(session.id, item.id, "assistant", { text: stripDeckSlug(item.text) });
1787
+ }
1788
+ if (store.list().some((s) => s.slideId === slideId && s.status === "running")) return;
1789
+ const entry = resolveSlideEntry(ctx.slidesRoot, slideId);
1790
+ if (!entry) return;
1791
+ let source;
1792
+ try {
1793
+ source = await fs.readFile(entry, "utf8");
1794
+ } catch {
1795
+ return;
1796
+ }
1797
+ const meta = readMetaTitleInSource(source);
1798
+ if (meta.kind !== "found" || meta.title === "Untitled") return;
1799
+ const desired = slugifyTitle(marked ?? "") || slugifyTitle(meta.title);
1800
+ if (!desired || UNTITLED_SLIDE_ID_RE.test(desired)) return;
1801
+ if (store.list().some((s) => s.slideId === slideId && s.status === "running")) return;
1802
+ recordWrite(entry);
1803
+ const renamed = await renameSlideDir(ctx.slidesRoot, slideId, desired);
1804
+ if (!renamed.ok) return;
1805
+ recordWrite(path.join(renamed.to, "index.tsx"));
1806
+ const manifest = await readManifest(ctx.manifestPath);
1807
+ const folderId = manifest.assignments[slideId];
1808
+ if (folderId) {
1809
+ delete manifest.assignments[slideId];
1810
+ manifest.assignments[renamed.slideId] = folderId;
1811
+ await writeManifest(ctx.manifestPath, manifest);
1812
+ }
1813
+ for (const s of store.list()) if (s.slideId === slideId) store.patch(s.id, { slideId: renamed.slideId });
1814
+ invalidateSlidesModule(server);
1815
+ broadcast({
1816
+ type: "slide-renamed",
1817
+ from: slideId,
1818
+ to: renamed.slideId
1819
+ });
1820
+ }
1821
+ function parseRespond(raw) {
1822
+ if (!raw || typeof raw !== "object") return null;
1823
+ const r = raw;
1824
+ if (r.cancel === true) return { cancel: true };
1825
+ if (typeof r.allow === "boolean") return { allow: r.allow };
1826
+ if (r.answers && typeof r.answers === "object") {
1827
+ const answers = {};
1828
+ for (const [k, v] of Object.entries(r.answers)) if (typeof v === "string") answers[k] = v;
1829
+ return { answers };
1830
+ }
1831
+ return null;
1832
+ }
1833
+ function parseTurnSettings(raw) {
1834
+ const settings = {};
1835
+ const context = parseContext(raw.context);
1836
+ if (context) settings.context = context;
1837
+ if (typeof raw.autoApprove === "boolean") settings.autoApprove = raw.autoApprove;
1838
+ if ("model" in raw) settings.model = parseModel(raw.model);
1839
+ if ("effort" in raw) settings.effort = isAgentEffort(raw.effort) ? raw.effort : null;
1840
+ return settings;
1841
+ }
1842
+ const AGENT_BODY_LIMIT = 1048576;
1843
+ function registerAgentRoutes(server, ctx) {
1844
+ const providers = createProviders();
1845
+ const store = createSessionStore({ file: path.join(ctx.userCwd, "node_modules", ".open-slide", "agent-sessions.json") });
1846
+ const streams = /* @__PURE__ */ new Set();
1847
+ const broadcast = (event) => {
1848
+ const frame = `data: ${JSON.stringify(event)}\n\n`;
1849
+ for (const stream of streams) stream.write(frame);
1850
+ };
1851
+ const heartbeat = setInterval(() => {
1852
+ for (const stream of streams) stream.write(": ping\n\n");
1853
+ }, 25e3);
1854
+ heartbeat.unref();
1855
+ const runner = createAgentRunner({
1856
+ userCwd: ctx.userCwd,
1857
+ writableRoots: [ctx.slidesRoot, ctx.globalAssetsRoot],
1858
+ store,
1859
+ providers,
1860
+ onTurnEnd: (turn) => renamePlaceholderDeck(server, ctx, store, broadcast, turn)
1861
+ });
1862
+ store.subscribe(broadcast);
1863
+ server.httpServer?.on("close", () => {
1864
+ clearInterval(heartbeat);
1865
+ for (const stream of streams) stream.end();
1866
+ runner.stopAll();
1867
+ store.flush();
1868
+ });
1869
+ const installsInFlight = /* @__PURE__ */ new Map();
1870
+ server.middlewares.use("/__agent", async (req, res, next) => {
1871
+ const url = new URL(req.url ?? "/", "http://local");
1872
+ const method = req.method ?? "GET";
1873
+ const segments = url.pathname.split("/").filter(Boolean);
1874
+ const enabled = ctx.agentEnabled && isLoopbackRequest(req);
1875
+ try {
1876
+ if (method === "GET" && url.pathname === "/status") {
1877
+ res.setHeader("cache-control", "no-store");
1878
+ if (!enabled) return json(res, 200, {
1879
+ enabled: false,
1880
+ packageManager: "",
1881
+ providers: []
1882
+ });
1883
+ const pm = await detectPackageManager(ctx.userCwd);
1884
+ return json(res, 200, await agentStatus(ctx.userCwd, providers, pm));
1885
+ }
1886
+ if (!enabled) return json(res, 404, { error: "agent panel disabled" });
1887
+ if (method === "GET" && url.pathname === "/events") {
1888
+ res.writeHead(200, {
1889
+ "content-type": "text/event-stream",
1890
+ "cache-control": "no-store",
1891
+ connection: "keep-alive"
1892
+ });
1893
+ res.write(": connected\n\n");
1894
+ streams.add(res);
1895
+ const drop = () => streams.delete(res);
1896
+ res.on("close", drop);
1897
+ res.on("error", drop);
1898
+ return;
1899
+ }
1900
+ if (method === "POST" && url.pathname === "/install") {
1901
+ const guard = validateMutationRequest(req, { requireJsonBody: true });
1902
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
1903
+ const body = await readBody(req, AGENT_BODY_LIMIT);
1904
+ if (!isAgentProviderId(body.provider)) return json(res, 400, { error: "invalid provider" });
1905
+ const provider = providers[body.provider];
1906
+ const pm = await detectPackageManager(ctx.userCwd);
1907
+ const pkg = provider.sdkPackage;
1908
+ let install = installsInFlight.get(pkg);
1909
+ if (!install) {
1910
+ install = installSdk(pm, pkg, ctx.userCwd).finally(() => installsInFlight.delete(pkg));
1911
+ installsInFlight.set(pkg, install);
1912
+ }
1913
+ await install;
1914
+ return json(res, 200, { ok: true });
1915
+ }
1916
+ if (method === "GET" && url.pathname === "/sessions") {
1917
+ res.setHeader("cache-control", "no-store");
1918
+ return json(res, 200, { sessions: store.list() });
1919
+ }
1920
+ if (method === "POST" && url.pathname === "/sessions") {
1921
+ const guard = validateMutationRequest(req, { requireJsonBody: true });
1922
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
1923
+ const body = await readBody(req, AGENT_BODY_LIMIT);
1924
+ if (!isAgentProviderId(body.provider)) return json(res, 400, { error: "invalid provider" });
1925
+ const slideId = typeof body.slideId === "string" && SLIDE_ID_RE.test(body.slideId) ? body.slideId : null;
1926
+ return json(res, 200, store.create(body.provider, slideId));
1927
+ }
1928
+ if (segments[0] === "sessions" && segments[1]) {
1929
+ const id = segments[1];
1930
+ const action = segments[2];
1931
+ const session = store.get(id);
1932
+ if (!session) return json(res, 404, { error: "session not found" });
1933
+ if (method === "GET" && !action) {
1934
+ res.setHeader("cache-control", "no-store");
1935
+ return json(res, 200, session);
1936
+ }
1937
+ if (method === "DELETE" && !action) {
1938
+ const guard = validateMutationRequest(req);
1939
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
1940
+ runner.stop(id);
1941
+ store.remove(id);
1942
+ return json(res, 200, { ok: true });
1943
+ }
1944
+ if (method === "POST" && action === "messages") {
1945
+ const guard = validateMutationRequest(req, { requireJsonBody: true });
1946
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
1947
+ const body = await readBody(req, AGENT_BODY_LIMIT);
1948
+ if (typeof body.text !== "string" || !body.text.trim()) return json(res, 400, { error: "missing text" });
1949
+ if (runner.isRunning(id)) return json(res, 409, { error: "session is already running" });
1950
+ if (body.edit !== void 0 && !session.items.some((i) => i.id === body.edit && i.kind === "user")) return json(res, 400, { error: "unknown message" });
1951
+ await runner.send(id, {
1952
+ text: body.text,
1953
+ context: parseContext(body.context),
1954
+ autoApprove: body.autoApprove === true,
1955
+ model: parseModel(body.model),
1956
+ effort: isAgentEffort(body.effort) ? body.effort : null,
1957
+ edit: typeof body.edit === "string" ? body.edit : void 0
1958
+ });
1959
+ return json(res, 202, { ok: true });
1960
+ }
1961
+ if (method === "POST" && action === "stop") {
1962
+ const guard = validateMutationRequest(req);
1963
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
1964
+ return json(res, 200, { ok: runner.stop(id) });
1965
+ }
1966
+ if (method === "POST" && action === "respond") {
1967
+ const guard = validateMutationRequest(req, { requireJsonBody: true });
1968
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
1969
+ const body = await readBody(req, AGENT_BODY_LIMIT);
1970
+ const payload = parseRespond(body);
1971
+ if (typeof body.itemId !== "string" || !payload) return json(res, 400, { error: "invalid response" });
1972
+ const ok = runner.respond(id, body.itemId, payload, parseTurnSettings(body));
1973
+ return json(res, ok ? 200 : 409, ok ? { ok } : { error: "nothing pending" });
1974
+ }
1975
+ }
1976
+ next();
1977
+ } catch (err) {
1978
+ if (err instanceof BodyTooLargeError) return json(res, 413, { error: err.message });
1979
+ json(res, 500, { error: String(err.message ?? err) });
1980
+ }
1981
+ });
1982
+ }
1983
+ //#endregion
16
1984
  //#region src/editing/babel-walk.ts
17
1985
  const SKIP_KEYS = /* @__PURE__ */ new Set([
18
1986
  "loc",
@@ -1488,6 +3456,40 @@ function registerAssetRoutes(server, ctx) {
1488
3456
  });
1489
3457
  }
1490
3458
  //#endregion
3459
+ //#region src/editing/page-entries.ts
3460
+ /**
3461
+ * The deck's page array as written, one entry per page, with whitespace
3462
+ * collapsed so reformatting the file keeps every entry the same. `null` when
3463
+ * the array cannot be read one entry per page: a spread stands for any number
3464
+ * of pages, and a default export that is not an array literal hides them.
3465
+ */
3466
+ function defaultExportEntriesInSource(source) {
3467
+ let body;
3468
+ try {
3469
+ body = parse(source, {
3470
+ sourceType: "module",
3471
+ plugins: ["typescript", "jsx"],
3472
+ errorRecovery: true
3473
+ }).program.body;
3474
+ } catch {
3475
+ return null;
3476
+ }
3477
+ for (const node of body) {
3478
+ if (node.type !== "ExportDefaultDeclaration") continue;
3479
+ let inner = node.declaration;
3480
+ while (inner.type === "TSAsExpression" || inner.type === "TSSatisfiesExpression") inner = inner.expression;
3481
+ if (inner.type !== "ArrayExpression") return null;
3482
+ const entries = [];
3483
+ for (const element of inner.elements) {
3484
+ if (!element || element.type === "SpreadElement") return null;
3485
+ if (element.start == null || element.end == null) return null;
3486
+ entries.push(source.slice(element.start, element.end).replace(/\s+/g, " "));
3487
+ }
3488
+ return entries;
3489
+ }
3490
+ return null;
3491
+ }
3492
+ //#endregion
1491
3493
  //#region src/files/board-structure.ts
1492
3494
  function sanitizeStructure(parsed) {
1493
3495
  if (!parsed || typeof parsed !== "object") return null;
@@ -1535,16 +3537,87 @@ function reconcileColumns(columns, pageCount) {
1535
3537
  function boardStructurePath(userCwd, slideId) {
1536
3538
  return path.join(userCwd, ".open-slide", "board", `${slideId}.json`);
1537
3539
  }
1538
- async function readBoardStructure(file) {
3540
+ /** The whole board file. Columns, cards and links each own one field of it. */
3541
+ async function readBoardFile(file) {
1539
3542
  try {
1540
- return sanitizeStructure(JSON.parse(await fs.readFile(file, "utf8")));
3543
+ const parsed = JSON.parse(await fs.readFile(file, "utf8"));
3544
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1541
3545
  } catch {
1542
- return null;
3546
+ return {};
1543
3547
  }
1544
3548
  }
3549
+ async function readBoardStructure(file) {
3550
+ return sanitizeStructure(await readBoardFile(file));
3551
+ }
3552
+ const pending = /* @__PURE__ */ new Map();
3553
+ /**
3554
+ * Rewrites the fields `fields` names and keeps every other one. Updates to the
3555
+ * same file run one after another: the columns and the cards are saved by
3556
+ * separate requests, and two read-modify-writes racing would drop one of them.
3557
+ */
3558
+ function updateBoardFile(file, fields) {
3559
+ const run = (pending.get(file) ?? Promise.resolve()).catch(() => void 0).then(async () => {
3560
+ const current = await readBoardFile(file);
3561
+ await fs.mkdir(path.dirname(file), { recursive: true });
3562
+ await fs.writeFile(file, `${JSON.stringify({
3563
+ ...current,
3564
+ ...fields
3565
+ }, null, 2)}\n`, "utf8");
3566
+ });
3567
+ pending.set(file, run);
3568
+ run.finally(() => {
3569
+ if (pending.get(file) === run) pending.delete(file);
3570
+ }).catch(() => void 0);
3571
+ return run;
3572
+ }
1545
3573
  async function writeBoardStructure(file, structure) {
1546
- await fs.mkdir(path.dirname(file), { recursive: true });
1547
- await fs.writeFile(file, `${JSON.stringify(structure, null, 2)}\n`, "utf8");
3574
+ await updateBoardFile(file, { columns: structure.columns });
3575
+ }
3576
+ //#endregion
3577
+ //#region src/files/board-cards.ts
3578
+ const CARD_STATUSES = [
3579
+ "todo",
3580
+ "doing",
3581
+ "review",
3582
+ "done"
3583
+ ];
3584
+ const TITLE_MAX = 80;
3585
+ const MEMO_MAX = 2e3;
3586
+ function isCardStatus(value) {
3587
+ return typeof value === "string" && CARD_STATUSES.includes(value);
3588
+ }
3589
+ function cleanCard(raw) {
3590
+ if (!raw || typeof raw !== "object") return null;
3591
+ const { title, status, memo } = raw;
3592
+ const card = {};
3593
+ if (typeof title === "string" && title.trim()) card.title = title.trim().slice(0, TITLE_MAX);
3594
+ if (isCardStatus(status)) card.status = status;
3595
+ if (typeof memo === "string" && memo.trim()) card.memo = memo.trim().slice(0, MEMO_MAX);
3596
+ return Object.keys(card).length > 0 ? card : null;
3597
+ }
3598
+ function sanitizeCards(raw) {
3599
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
3600
+ const cards = {};
3601
+ for (const [key, value] of Object.entries(raw)) {
3602
+ const card = cleanCard(value);
3603
+ if (key && card) cards[key] = card;
3604
+ }
3605
+ return cards;
3606
+ }
3607
+ const sameLink = (link, a, b) => link.a === a && link.b === b || link.a === b && link.b === a;
3608
+ function linkCards(links, a, b) {
3609
+ if (!a || !b || a === b || links.some((link) => sameLink(link, a, b))) return links;
3610
+ return [...links, {
3611
+ a,
3612
+ b
3613
+ }];
3614
+ }
3615
+ function sanitizeLinks(raw) {
3616
+ if (!Array.isArray(raw)) return [];
3617
+ return raw.reduce((links, item) => {
3618
+ const { a, b } = item ?? {};
3619
+ return typeof a === "string" && typeof b === "string" ? linkCards(links, a, b) : links;
3620
+ }, []);
1548
3621
  }
1549
3622
  //#endregion
1550
3623
  //#region src/vite/routes/board.ts
@@ -1581,6 +3654,34 @@ function registerBoardRoutes(server, ctx) {
1581
3654
  await writeBoardStructure(boardStructurePath(ctx.userCwd, slideId), structure);
1582
3655
  return json(res, 200, { ok: true });
1583
3656
  }
3657
+ if (method === "GET" && url.pathname === "/cards") {
3658
+ const slideId = url.searchParams.get("slideId") ?? "";
3659
+ if (!resolveSlideEntryPath(ctx, slideId)) return json(res, 400, { error: "invalid slideId" });
3660
+ const stored = await readBoardFile(boardStructurePath(ctx.userCwd, slideId));
3661
+ return json(res, 200, {
3662
+ cards: sanitizeCards(stored.cards),
3663
+ links: sanitizeLinks(stored.links)
3664
+ });
3665
+ }
3666
+ if (method === "PUT" && url.pathname === "/cards") {
3667
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
3668
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
3669
+ const body = await readBody(req);
3670
+ const slideId = body.slideId ?? "";
3671
+ if (!resolveSlideEntryPath(ctx, slideId)) return json(res, 400, { error: "invalid slideId" });
3672
+ await updateBoardFile(boardStructurePath(ctx.userCwd, slideId), {
3673
+ cards: sanitizeCards(body.cards),
3674
+ links: sanitizeLinks(body.links)
3675
+ });
3676
+ return json(res, 200, { ok: true });
3677
+ }
3678
+ if (method === "GET" && url.pathname === "/entries") {
3679
+ const slideId = url.searchParams.get("slideId") ?? "";
3680
+ const entry = resolveSlideEntryPath(ctx, slideId);
3681
+ if (!entry) return json(res, 400, { error: "invalid slideId" });
3682
+ const source = await readSlideSource(entry);
3683
+ return json(res, 200, { entries: source ? defaultExportEntriesInSource(source) : null });
3684
+ }
1584
3685
  next();
1585
3686
  } catch (err) {
1586
3687
  json(res, 500, { error: String(err.message ?? err) });
@@ -1597,6 +3698,24 @@ function b64urlDecode(s) {
1597
3698
  const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
1598
3699
  return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString("utf8");
1599
3700
  }
3701
+ function isFiniteNumber(value) {
3702
+ return typeof value === "number" && Number.isFinite(value);
3703
+ }
3704
+ function parseCommentRect(value) {
3705
+ if (!value || typeof value !== "object") return void 0;
3706
+ const { x, y, w, h } = value;
3707
+ if (!isFiniteNumber(x) || !isFiniteNumber(y) || !isFiniteNumber(w) || !isFiniteNumber(h)) return void 0;
3708
+ if (w <= 0 || h <= 0) return void 0;
3709
+ return {
3710
+ x: Math.round(x),
3711
+ y: Math.round(y),
3712
+ w: Math.round(w),
3713
+ h: Math.round(h)
3714
+ };
3715
+ }
3716
+ function parseCommentPage(value) {
3717
+ return isFiniteNumber(value) && value >= 0 ? Math.floor(value) : void 0;
3718
+ }
1600
3719
  function parseMarkers(source) {
1601
3720
  const comments = [];
1602
3721
  const lines = source.split("\n");
@@ -1611,12 +3730,36 @@ function parseMarkers(source) {
1611
3730
  line: i + 1,
1612
3731
  ts,
1613
3732
  note: payload.note,
1614
- hint: payload.hint
3733
+ hint: payload.hint,
3734
+ rect: parseCommentRect(payload.rect),
3735
+ page: parseCommentPage(payload.page)
1615
3736
  });
1616
3737
  } catch {}
1617
3738
  }
1618
3739
  return comments;
1619
3740
  }
3741
+ function markerColumn(source, line) {
3742
+ const index = (source.split("\n")[line - 1] ?? "").search(/\{\/\*\s*@slide-comment/);
3743
+ return index === -1 ? 0 : index;
3744
+ }
3745
+ function resolveTargets(source, comments) {
3746
+ const ast = parseSource(source);
3747
+ return comments.map((comment) => {
3748
+ if (!ast) return {
3749
+ ...comment,
3750
+ target: null
3751
+ };
3752
+ const column = markerColumn(source, comment.line);
3753
+ const start = findJsxAncestors(ast, comment.line, column).find((node) => t.isJSXElement(node))?.loc?.start;
3754
+ return {
3755
+ ...comment,
3756
+ target: start ? {
3757
+ line: start.line,
3758
+ column: start.column
3759
+ } : null
3760
+ };
3761
+ });
3762
+ }
1620
3763
  function newCommentId() {
1621
3764
  return shortId("c");
1622
3765
  }
@@ -1638,11 +3781,43 @@ function lineIndent(source, lineNumber) {
1638
3781
  }
1639
3782
  function planInsertion(source, target) {
1640
3783
  if (t.isJSXElement(target) && target.openingElement.selfClosing) return null;
3784
+ const offset = (t.isJSXFragment(target) ? target.openingFragment : target.openingElement).end ?? 0;
1641
3785
  return {
1642
- offset: (t.isJSXFragment(target) ? target.openingFragment : target.openingElement).end ?? 0,
1643
- indent: `${lineIndent(source, target.loc?.start.line ?? 1)} `
3786
+ offset,
3787
+ indent: `${lineIndent(source, target.loc?.start.line ?? 1)} `,
3788
+ breakAfter: !/^[ \t]*\r?\n/.test(source.slice(offset))
1644
3789
  };
1645
3790
  }
3791
+ function buildMarker(plan, id, ts, encodedText) {
3792
+ const marker = `\n${plan.indent}{/* @slide-comment id="${id}" ts="${ts}" text="${encodedText}" */}`;
3793
+ return plan.breakAfter ? `${marker}\n${plan.indent}` : marker;
3794
+ }
3795
+ function updateMarkerNote(source, id, note) {
3796
+ const re = new RegExp(`(\\{\\/\\*\\s*@slide-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text=")([A-Za-z0-9_\\-]+={0,2})("\\s*\\*\\/\\})`);
3797
+ const m = source.match(re);
3798
+ if (!m) return null;
3799
+ let payload;
3800
+ try {
3801
+ payload = JSON.parse(b64urlDecode(m[2]));
3802
+ } catch {
3803
+ return null;
3804
+ }
3805
+ const encoded = b64urlEncode(JSON.stringify({
3806
+ ...payload,
3807
+ note
3808
+ }));
3809
+ return source.replace(re, `$1${encoded}$3`);
3810
+ }
3811
+ function removeMarker(source, id) {
3812
+ const lines = source.split("\n");
3813
+ const idRe = markerDeleteRegex(id);
3814
+ const hit = lines.findIndex((line) => idRe.test(line));
3815
+ if (hit === -1) return null;
3816
+ const stripped = lines[hit].replace(idRe, "");
3817
+ if (stripped.trim() === "") lines.splice(hit, 1);
3818
+ else lines[hit] = stripped;
3819
+ return lines.join("\n");
3820
+ }
1646
3821
  function findInsertion(source, line, column) {
1647
3822
  const ast = parseSource(source);
1648
3823
  if (!ast) return null;
@@ -1669,7 +3844,7 @@ function registerCommentRoutes(server, ctx) {
1669
3844
  if (!file) return json(res, 400, { error: "invalid slideId" });
1670
3845
  const source = await readSlideSource(file);
1671
3846
  if (source === null) return json(res, 404, { error: "slide not found" });
1672
- return json(res, 200, { comments: parseMarkers(source) });
3847
+ return json(res, 200, { comments: resolveTargets(source, parseMarkers(source)) });
1673
3848
  }
1674
3849
  if (method === "POST" && url.pathname === "/add") {
1675
3850
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
@@ -1686,11 +3861,13 @@ function registerCommentRoutes(server, ctx) {
1686
3861
  if (!plan) return json(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
1687
3862
  const id = newCommentId();
1688
3863
  const ts = (/* @__PURE__ */ new Date()).toISOString();
1689
- const payload = b64urlEncode(JSON.stringify({
3864
+ const payload = {
1690
3865
  note: body.text,
1691
- hint: body.hint
1692
- }));
1693
- const marker = `\n${plan.indent}{/* @slide-comment id="${id}" ts="${ts}" text="${payload}" */}`;
3866
+ hint: body.hint,
3867
+ rect: parseCommentRect(body.rect),
3868
+ page: parseCommentPage(body.page)
3869
+ };
3870
+ const marker = buildMarker(plan, id, ts, b64urlEncode(JSON.stringify(payload)));
1694
3871
  const next = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
1695
3872
  await fs.writeFile(file, next, "utf8");
1696
3873
  const markerLine = offsetToLine(next, plan.offset + 1);
@@ -1699,6 +3876,22 @@ function registerCommentRoutes(server, ctx) {
1699
3876
  line: markerLine
1700
3877
  });
1701
3878
  }
3879
+ if (method === "PUT" && url.pathname.startsWith("/")) {
3880
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
3881
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
3882
+ const id = url.pathname.slice(1);
3883
+ if (!/^c-[a-f0-9]+$/.test(id)) return json(res, 400, { error: "invalid id" });
3884
+ const body = await readBody(req);
3885
+ const file = resolveSlideEntryPath(ctx, body.slideId ?? "");
3886
+ if (!file) return json(res, 400, { error: "invalid slideId" });
3887
+ if (!body.text || typeof body.text !== "string") return json(res, 400, { error: "missing text" });
3888
+ const source = await readSlideSource(file);
3889
+ if (source === null) return json(res, 404, { error: "slide not found" });
3890
+ const next = updateMarkerNote(source, id, body.text);
3891
+ if (next === null) return json(res, 404, { error: "marker not found" });
3892
+ await fs.writeFile(file, next, "utf8");
3893
+ return json(res, 200, { ok: true });
3894
+ }
1702
3895
  if (method === "DELETE" && url.pathname.startsWith("/")) {
1703
3896
  const requestCheck = validateMutationRequest(req);
1704
3897
  if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
@@ -1709,12 +3902,9 @@ function registerCommentRoutes(server, ctx) {
1709
3902
  if (!file) return json(res, 400, { error: "invalid slideId" });
1710
3903
  const source = await readSlideSource(file);
1711
3904
  if (source === null) return json(res, 404, { error: "slide not found" });
1712
- const lines = source.split("\n");
1713
- const idRe = markerDeleteRegex(id);
1714
- const hit = lines.findIndex((l) => idRe.test(l));
1715
- if (hit === -1) return json(res, 404, { error: "marker not found" });
1716
- lines.splice(hit, 1);
1717
- await fs.writeFile(file, lines.join("\n"), "utf8");
3905
+ const next = removeMarker(source, id);
3906
+ if (next === null) return json(res, 404, { error: "marker not found" });
3907
+ await fs.writeFile(file, next, "utf8");
1718
3908
  return json(res, 200, { ok: true });
1719
3909
  }
1720
3910
  next();
@@ -2133,6 +4323,18 @@ function registerSlideRoutes(server, ctx) {
2133
4323
  const url = new URL(req.url ?? "/", "http://local");
2134
4324
  const method = req.method ?? "GET";
2135
4325
  try {
4326
+ if (url.pathname === "/" && method === "POST") {
4327
+ const requestCheck = validateMutationRequest(req);
4328
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
4329
+ const created = await createBlankSlideDir(ctx.slidesRoot, ctx.frameworkSpecifier);
4330
+ if (!created.ok) return json(res, created.status, { error: created.error });
4331
+ recordWrite(created.file);
4332
+ invalidateSlidesModule(server);
4333
+ return json(res, 200, {
4334
+ ok: true,
4335
+ slideId: created.slideId
4336
+ });
4337
+ }
2136
4338
  const reorderMatch = url.pathname.match(/^\/([^/]+)\/reorder$/);
2137
4339
  if (reorderMatch && method === "PUT") {
2138
4340
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
@@ -2357,6 +4559,7 @@ function apiPlugin(opts) {
2357
4559
  registerFolderRoutes(server, ctx);
2358
4560
  registerUpdateRoutes(server, ctx);
2359
4561
  registerRestartRoutes(server);
4562
+ registerAgentRoutes(server, ctx);
2360
4563
  }
2361
4564
  };
2362
4565
  }
@@ -2868,24 +5071,6 @@ function locTagsPlugin(opts) {
2868
5071
  };
2869
5072
  }
2870
5073
  //#endregion
2871
- //#region src/vite/recent-writes.ts
2872
- const recentWrites = /* @__PURE__ */ new Map();
2873
- /** Record that `file` was just written by us. */
2874
- function recordWrite(file, now = Date.now()) {
2875
- recentWrites.set(file, now);
2876
- }
2877
- /**
2878
- * True if `file` was recorded within the recent-write window. Expired entries
2879
- * are pruned so a stale path can no longer suppress a later genuine edit.
2880
- */
2881
- function hasRecentWrite(file, now = Date.now()) {
2882
- const ts = recentWrites.get(file);
2883
- if (ts == null) return false;
2884
- if (now - ts < 1500) return true;
2885
- recentWrites.delete(file);
2886
- return false;
2887
- }
2888
- //#endregion
2889
5074
  //#region src/vite/notes-plugin.ts
2890
5075
  function findNotesExport(ast) {
2891
5076
  for (const stmt of ast.program.body) {
@@ -2945,343 +5130,136 @@ function applyNotesEdit(source, index, text) {
2945
5130
  if (text === "") return {
2946
5131
  ok: true,
2947
5132
  source
2948
- };
2949
- const block = [
2950
- "",
2951
- "",
2952
- "export const notes: (string | undefined)[] = [",
2953
- ...[...Array.from({ length: index }, () => "undefined"), literal].map((s) => ` ${s},`),
2954
- "];",
2955
- ""
2956
- ].join("\n");
2957
- const offset = findInsertionOffset(ast, source);
2958
- return {
2959
- ok: true,
2960
- source: source.slice(0, offset) + block + source.slice(offset)
2961
- };
2962
- }
2963
- const elementTexts = [];
2964
- for (const el of found.elements) {
2965
- if (el === null) {
2966
- elementTexts.push("undefined");
2967
- continue;
2968
- }
2969
- if (typeof el.start !== "number" || typeof el.end !== "number") return {
2970
- ok: false,
2971
- status: 422,
2972
- error: "`notes` element missing source range"
2973
- };
2974
- elementTexts.push(source.slice(el.start, el.end));
2975
- }
2976
- while (elementTexts.length <= index) elementTexts.push("undefined");
2977
- elementTexts[index] = literal;
2978
- while (elementTexts.length > 0 && elementTexts[elementTexts.length - 1] === "undefined") elementTexts.pop();
2979
- const replacement = elementTexts.length === 0 ? "[]" : `[\n${elementTexts.map((s) => ` ${s},`).join("\n")}\n]`;
2980
- return {
2981
- ok: true,
2982
- source: source.slice(0, found.arrayStart) + replacement + source.slice(found.arrayEnd)
2983
- };
2984
- }
2985
- function notesPlugin(opts) {
2986
- const slidesRoot = path.resolve(opts.userCwd, opts.slidesDir ?? "slides");
2987
- return {
2988
- name: "lemonriver:notes",
2989
- apply: "serve",
2990
- handleHotUpdate(ctx) {
2991
- if (hasRecentWrite(ctx.file)) return [];
2992
- },
2993
- configureServer(server) {
2994
- server.middlewares.use("/__notes", async (req, res, next) => {
2995
- const url = new URL(req.url ?? "/", "http://local");
2996
- if ((req.method ?? "GET") !== "PUT" || url.pathname !== "/") return next();
2997
- const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2998
- if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2999
- try {
3000
- const body = await readBody(req);
3001
- const slideId = body.slideId ?? "";
3002
- const file = resolveSlideEntry(slidesRoot, slideId);
3003
- if (!file) return json(res, 400, { error: "invalid slideId" });
3004
- if (typeof body.index !== "number") return json(res, 400, { error: "missing index" });
3005
- if (typeof body.text !== "string") return json(res, 400, { error: "missing text" });
3006
- const source = await readSlideSource(file);
3007
- if (source === null) return json(res, 404, { error: "slide not found" });
3008
- const result = applyNotesEdit(source, body.index, body.text);
3009
- if (!result.ok) return json(res, result.status, { error: result.error });
3010
- const changed = result.source !== source;
3011
- if (changed) {
3012
- recordWrite(file);
3013
- await fs.writeFile(file, result.source, "utf8");
3014
- }
3015
- return json(res, 200, {
3016
- ok: true,
3017
- changed
3018
- });
3019
- } catch (err) {
3020
- json(res, 500, { error: String(err.message ?? err) });
3021
- }
3022
- });
3023
- }
3024
- };
3025
- }
3026
- //#endregion
3027
- //#region src/vite/open-slide-plugin.ts
3028
- const SLIDES_VMOD = "virtual:open-slide/slides";
3029
- const CONFIG_VMOD = "virtual:open-slide/config";
3030
- const FOLDERS_VMOD = "virtual:open-slide/folders";
3031
- function resolved$1(id) {
3032
- return `\0${id}`;
3033
- }
3034
- async function findSlides(userCwd, slidesDir) {
3035
- const abs = path.resolve(userCwd, slidesDir);
3036
- if (!existsSync(abs)) return [];
3037
- return (await fg("*/index.{tsx,jsx,ts,js}", {
3038
- cwd: abs,
3039
- absolute: true,
3040
- onlyFiles: true
3041
- })).sort();
3042
- }
3043
- function toId(absFile, slidesRoot) {
3044
- return path.relative(slidesRoot, absFile).split(path.sep)[0];
3045
- }
3046
- const META_THEME_RE = /(?:^|[\s,{])theme\s*:\s*['"]([^'"]+)['"]/;
3047
- const META_CREATED_AT_RE = /(?:^|[\s,{])createdAt\s*:\s*['"]([^'"]+)['"]/;
3048
- function extractMeta(src) {
3049
- const empty = {
3050
- theme: null,
3051
- createdAt: null
3052
- };
3053
- const metaStart = src.search(/export\s+const\s+meta\b/);
3054
- if (metaStart === -1) return empty;
3055
- const eqIdx = src.indexOf("=", metaStart);
3056
- if (eqIdx === -1) return empty;
3057
- const openBrace = src.indexOf("{", eqIdx);
3058
- if (openBrace === -1) return empty;
3059
- let depth = 0;
3060
- let closeBrace = -1;
3061
- for (let i = openBrace; i < src.length; i++) {
3062
- const ch = src[i];
3063
- if (ch === "{") depth++;
3064
- else if (ch === "}") {
3065
- depth--;
3066
- if (depth === 0) {
3067
- closeBrace = i;
3068
- break;
3069
- }
3070
- }
3071
- }
3072
- if (closeBrace === -1) return empty;
3073
- const body = src.slice(openBrace + 1, closeBrace);
3074
- const themeMatch = body.match(META_THEME_RE);
3075
- const createdAtMatch = body.match(META_CREATED_AT_RE);
3076
- return {
3077
- theme: themeMatch ? themeMatch[1] : null,
3078
- createdAt: createdAtMatch ? createdAtMatch[1] : null
3079
- };
3080
- }
3081
- async function readSlideMeta(abs) {
3082
- try {
3083
- return extractMeta(await fs.readFile(abs, "utf8"));
3084
- } catch {
5133
+ };
5134
+ const block = [
5135
+ "",
5136
+ "",
5137
+ "export const notes: (string | undefined)[] = [",
5138
+ ...[...Array.from({ length: index }, () => "undefined"), literal].map((s) => ` ${s},`),
5139
+ "];",
5140
+ ""
5141
+ ].join("\n");
5142
+ const offset = findInsertionOffset(ast, source);
3085
5143
  return {
3086
- theme: null,
3087
- createdAt: null
5144
+ ok: true,
5145
+ source: source.slice(0, offset) + block + source.slice(offset)
3088
5146
  };
3089
5147
  }
3090
- }
3091
- function parseCreatedAtMs(iso) {
3092
- if (!iso) return null;
3093
- const ms = Date.parse(iso);
3094
- return Number.isFinite(ms) ? ms : null;
3095
- }
3096
- const warnedInvalidSlideIds = /* @__PURE__ */ new Set();
3097
- async function generateSlidesModule(files, slidesRoot, isDev) {
3098
- const scanned = await Promise.all(files.map(async (abs) => {
3099
- const id = toId(abs, slidesRoot);
3100
- const importPath = isDev ? `@fs/${normalizePath(abs).replace(/^\/+/, "")}` : abs;
3101
- const meta = await readSlideMeta(abs);
3102
- return {
3103
- id,
3104
- importPath,
3105
- theme: meta.theme,
3106
- createdAt: parseCreatedAtMs(meta.createdAt)
5148
+ const elementTexts = [];
5149
+ for (const el of found.elements) {
5150
+ if (el === null) {
5151
+ elementTexts.push("undefined");
5152
+ continue;
5153
+ }
5154
+ if (typeof el.start !== "number" || typeof el.end !== "number") return {
5155
+ ok: false,
5156
+ status: 422,
5157
+ error: "`notes` element missing source range"
3107
5158
  };
3108
- }));
3109
- const entries = scanned.filter((e) => SLIDE_ID_RE.test(e.id));
3110
- const ignored = scanned.filter((e) => !SLIDE_ID_RE.test(e.id)).map((e) => e.id);
3111
- const ids = JSON.stringify(entries.map((e) => e.id).sort());
3112
- const themesMap = {};
3113
- const createdAtMap = {};
3114
- for (const e of entries) {
3115
- if (e.theme) themesMap[e.id] = e.theme;
3116
- if (e.createdAt !== null) createdAtMap[e.id] = e.createdAt;
5159
+ elementTexts.push(source.slice(el.start, el.end));
3117
5160
  }
3118
- const themesJson = JSON.stringify(themesMap);
3119
- const createdAtJson = JSON.stringify(createdAtMap);
3120
- const importTokens = JSON.stringify(Object.fromEntries(entries.map((e) => [e.id, 0])));
5161
+ while (elementTexts.length <= index) elementTexts.push("undefined");
5162
+ elementTexts[index] = literal;
5163
+ while (elementTexts.length > 0 && elementTexts[elementTexts.length - 1] === "undefined") elementTexts.pop();
5164
+ const replacement = elementTexts.length === 0 ? "[]" : `[\n${elementTexts.map((s) => ` ${s},`).join("\n")}\n]`;
3121
5165
  return {
3122
- code: `// virtual:open-slide/slides — generated
3123
- export const slideIds = ${ids};
3124
- export const slideThemes = ${themesJson};
3125
- export const slideCreatedAt = ${createdAtJson};
3126
- ${isDev ? `
3127
- const slideImportTokens = ${importTokens};
3128
- if (import.meta.hot) {
3129
- import.meta.hot.on('open-slide:slide-changed', (data) => {
3130
- const ids = Array.isArray(data?.slideIds) ? data.slideIds : [];
3131
- const token = Date.now();
3132
- for (const id of ids) {
3133
- if (Object.prototype.hasOwnProperty.call(slideImportTokens, id)) slideImportTokens[id] = token;
3134
- }
3135
- });
3136
- }
3137
- ` : ""}
3138
-
3139
- export async function loadSlide(id) {
3140
- switch (id) {
3141
- ${entries.map((e) => {
3142
- 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)})`;
3143
- return ` case ${JSON.stringify(e.id)}: return ${importExpr};`;
3144
- }).join("\n")}
3145
- default: throw new Error('Slide not found: ' + id);
3146
- }
3147
- }
3148
- `,
3149
- ignored
5166
+ ok: true,
5167
+ source: source.slice(0, found.arrayStart) + replacement + source.slice(found.arrayEnd)
3150
5168
  };
3151
5169
  }
3152
- function lemonRiverPlugin(opts) {
3153
- const { userCwd, config, coreVersion } = opts;
3154
- const slidesDir = config.slidesDir ?? "slides";
3155
- const slidesRoot = path.resolve(userCwd, slidesDir);
3156
- const manifestPath = foldersManifestPath(slidesRoot);
3157
- let isDev = false;
3158
- const slideIdForEntry = (p) => {
3159
- const rel = path.relative(slidesRoot, p);
3160
- if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
3161
- const parts = rel.split(path.sep);
3162
- if (parts.length !== 2) return null;
3163
- if (!/^index\.(tsx|jsx|ts|js)$/.test(parts[1])) return null;
3164
- return parts[0];
3165
- };
3166
- let slideChangeTimer = null;
3167
- const pendingSlideChanges = /* @__PURE__ */ new Set();
3168
- const queueSlideChanged = (server, id) => {
3169
- pendingSlideChanges.add(id);
3170
- if (slideChangeTimer) clearTimeout(slideChangeTimer);
3171
- slideChangeTimer = setTimeout(() => {
3172
- slideChangeTimer = null;
3173
- const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
3174
- if (mod) server.moduleGraph.invalidateModule(mod);
3175
- const slideIds = Array.from(pendingSlideChanges);
3176
- pendingSlideChanges.clear();
3177
- server.ws.send({
3178
- type: "custom",
3179
- event: "open-slide:slide-changed",
3180
- data: { slideIds }
3181
- });
3182
- }, 100);
3183
- };
5170
+ function notesPlugin(opts) {
5171
+ const slidesRoot = path.resolve(opts.userCwd, opts.slidesDir ?? "slides");
3184
5172
  return {
3185
- name: "lemonriver",
3186
- config(_c, env) {
3187
- isDev = env.command === "serve";
3188
- },
3189
- resolveId(id) {
3190
- if (id === SLIDES_VMOD) return resolved$1(SLIDES_VMOD);
3191
- if (id === CONFIG_VMOD) return resolved$1(CONFIG_VMOD);
3192
- if (id === FOLDERS_VMOD) return resolved$1(FOLDERS_VMOD);
3193
- return null;
3194
- },
3195
- async load(id) {
3196
- if (id === resolved$1(SLIDES_VMOD)) {
3197
- const { code, ignored } = await generateSlidesModule(await findSlides(userCwd, slidesDir), slidesRoot, isDev);
3198
- for (const slideId of ignored) {
3199
- if (warnedInvalidSlideIds.has(slideId)) continue;
3200
- warnedInvalidSlideIds.add(slideId);
3201
- 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.`);
3202
- }
3203
- return code;
3204
- }
3205
- if (id === resolved$1(CONFIG_VMOD)) {
3206
- const userBuild = config.build ?? {};
3207
- const buildResolved = isDev ? {
3208
- showSlideBrowser: true,
3209
- showSlideUi: true,
3210
- allowHtmlDownload: true
3211
- } : {
3212
- showSlideBrowser: userBuild.showSlideBrowser ?? true,
3213
- showSlideUi: userBuild.showSlideUi ?? true,
3214
- allowHtmlDownload: userBuild.allowHtmlDownload ?? true
3215
- };
3216
- const resolvedConfig = {
3217
- ...config,
3218
- build: buildResolved,
3219
- version: coreVersion
3220
- };
3221
- return `export default ${JSON.stringify(resolvedConfig)};\n`;
3222
- }
3223
- if (id === resolved$1(FOLDERS_VMOD)) {
3224
- const manifest = await readManifest(manifestPath);
3225
- return `export default ${JSON.stringify(manifest)};\n`;
3226
- }
3227
- return null;
3228
- },
5173
+ name: "lemonriver:notes",
5174
+ apply: "serve",
3229
5175
  handleHotUpdate(ctx) {
3230
- const slideId = slideIdForEntry(ctx.file);
3231
- if (!slideId) return;
3232
5176
  if (hasRecentWrite(ctx.file)) return [];
3233
- queueSlideChanged(ctx.server, slideId);
3234
- return [];
3235
5177
  },
3236
5178
  configureServer(server) {
3237
- const isSlideEntry = (p) => slideIdForEntry(p) !== null;
3238
- let reloadTimer = null;
3239
- const reload = () => {
3240
- if (reloadTimer) clearTimeout(reloadTimer);
3241
- reloadTimer = setTimeout(() => {
3242
- reloadTimer = null;
3243
- const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
3244
- if (mod) server.moduleGraph.invalidateModule(mod);
3245
- server.ws.send({ type: "full-reload" });
3246
- }, 150);
3247
- };
3248
- if (existsSync(slidesRoot)) server.watcher.add(slidesRoot);
3249
- server.watcher.on("add", (p) => {
3250
- if (isSlideEntry(p)) reload();
3251
- });
3252
- server.watcher.on("unlink", (p) => {
3253
- if (isSlideEntry(p)) reload();
3254
- });
3255
- let foldersTimer = null;
3256
- const invalidateFolders = () => {
3257
- if (foldersTimer) clearTimeout(foldersTimer);
3258
- foldersTimer = setTimeout(() => {
3259
- foldersTimer = null;
3260
- const mod = server.moduleGraph.getModuleById(resolved$1(FOLDERS_VMOD));
3261
- if (mod) server.moduleGraph.invalidateModule(mod);
3262
- }, 100);
3263
- };
3264
- server.watcher.add(manifestPath);
3265
- server.watcher.on("change", (p) => {
3266
- if (p === manifestPath) invalidateFolders();
5179
+ server.middlewares.use("/__notes", async (req, res, next) => {
5180
+ const url = new URL(req.url ?? "/", "http://local");
5181
+ if ((req.method ?? "GET") !== "PUT" || url.pathname !== "/") return next();
5182
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
5183
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
5184
+ try {
5185
+ const body = await readBody(req);
5186
+ const slideId = body.slideId ?? "";
5187
+ const file = resolveSlideEntry(slidesRoot, slideId);
5188
+ if (!file) return json(res, 400, { error: "invalid slideId" });
5189
+ if (typeof body.index !== "number") return json(res, 400, { error: "missing index" });
5190
+ if (typeof body.text !== "string") return json(res, 400, { error: "missing text" });
5191
+ const source = await readSlideSource(file);
5192
+ if (source === null) return json(res, 404, { error: "slide not found" });
5193
+ const result = applyNotesEdit(source, body.index, body.text);
5194
+ if (!result.ok) return json(res, result.status, { error: result.error });
5195
+ const changed = result.source !== source;
5196
+ if (changed) {
5197
+ recordWrite(file);
5198
+ await fs.writeFile(file, result.source, "utf8");
5199
+ }
5200
+ return json(res, 200, {
5201
+ ok: true,
5202
+ changed
5203
+ });
5204
+ } catch (err) {
5205
+ json(res, 500, { error: String(err.message ?? err) });
5206
+ }
3267
5207
  });
3268
- server.watcher.on("add", (p) => {
3269
- if (p === manifestPath) invalidateFolders();
5208
+ }
5209
+ };
5210
+ }
5211
+ //#endregion
5212
+ //#region src/vite/server-info-plugin.ts
5213
+ function serverInfoUrl(address, https, base) {
5214
+ let host = address.address === "::" || address.address === "0.0.0.0" ? "localhost" : address.address;
5215
+ if (host.includes(":")) host = `[${host}]`;
5216
+ const pathname = base.startsWith("/") ? base : "/";
5217
+ return `${https ? "https" : "http"}://${host}:${address.port}${pathname}`;
5218
+ }
5219
+ function serverInfoPlugin(opts) {
5220
+ const outDir = path.join(opts.userCwd, "node_modules", ".open-slide");
5221
+ const outFile = path.join(outDir, "server.json");
5222
+ return {
5223
+ name: "open-slide:server-info",
5224
+ apply: "serve",
5225
+ configureServer(server) {
5226
+ const httpServer = server.httpServer;
5227
+ if (!httpServer) return;
5228
+ let publication = Promise.resolve(null);
5229
+ httpServer.on("listening", () => {
5230
+ const address = httpServer.address();
5231
+ if (!address || typeof address === "string") return;
5232
+ const token = randomUUID();
5233
+ const body = {
5234
+ url: serverInfoUrl(address, Boolean(server.config.server.https), server.config.base),
5235
+ port: address.port,
5236
+ pid: process.pid,
5237
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5238
+ token
5239
+ };
5240
+ const tmpFile = `${outFile}.${token}.tmp`;
5241
+ publication = (async () => {
5242
+ try {
5243
+ await fs.mkdir(outDir, { recursive: true });
5244
+ await fs.writeFile(tmpFile, `${JSON.stringify(body, null, 2)}\n`, "utf8");
5245
+ await fs.rename(tmpFile, outFile);
5246
+ return token;
5247
+ } catch {
5248
+ await fs.rm(tmpFile, { force: true }).catch(() => {});
5249
+ return null;
5250
+ }
5251
+ })();
3270
5252
  });
3271
- server.watcher.on("unlink", (p) => {
3272
- if (p === manifestPath) invalidateFolders();
5253
+ httpServer.on("close", async () => {
5254
+ const token = await publication;
5255
+ if (!token) return;
5256
+ try {
5257
+ if (JSON.parse(readFileSync(outFile, "utf8")).token === token) rmSync(outFile, { force: true });
5258
+ } catch {}
3273
5259
  });
3274
5260
  }
3275
5261
  };
3276
5262
  }
3277
- async function loadUserConfig(userCwd) {
3278
- const file = CONFIG_FILES.map((name) => path.join(userCwd, name)).find((p) => existsSync(p));
3279
- if (!file) return {};
3280
- return (await loadConfigFromFile({
3281
- command: "serve",
3282
- mode: "development"
3283
- }, file, userCwd, "silent"))?.config ?? {};
3284
- }
3285
5263
  //#endregion
3286
5264
  //#region src/vite/themes-plugin.ts
3287
5265
  const THEMES_VMOD = "virtual:open-slide/themes";
@@ -3482,7 +5460,8 @@ async function createViteConfig(opts) {
3482
5460
  userCwd,
3483
5461
  slidesDir,
3484
5462
  assetsDir,
3485
- coreVersion: CORE_VERSION
5463
+ coreVersion: CORE_VERSION,
5464
+ agentEnabled: config.agent?.enabled ?? true
3486
5465
  }),
3487
5466
  notesPlugin({
3488
5467
  userCwd,
@@ -3491,7 +5470,8 @@ async function createViteConfig(opts) {
3491
5470
  currentPlugin({
3492
5471
  userCwd,
3493
5472
  slidesDir
3494
- })
5473
+ }),
5474
+ serverInfoPlugin({ userCwd })
3495
5475
  ],
3496
5476
  resolve: {
3497
5477
  alias: {