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