@office-kit/pptx-dev 0.6.0 → 0.8.1
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/README.md +78 -2
- package/dist/cli.mjs +519 -27
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-CShdm4G1.mjs → src-CdlqA8pS.mjs} +7 -5
- package/dist/src-CdlqA8pS.mjs.map +1 -0
- package/dist/terminal-client.js +21 -17
- package/package.json +6 -6
- package/dist/src-CShdm4G1.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,13 +1,328 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as createDeckBuilder, i as initProject, n as exportDeck, r as inspectTemplate } from "./src-
|
|
2
|
+
import { a as createDeckBuilder, i as initProject, n as exportDeck, r as inspectTemplate } from "./src-CdlqA8pS.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { chmod, readFile, stat } from "node:fs/promises";
|
|
5
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
import { chmod, lstat, mkdir, readFile, readdir, realpath, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { watch } from "node:fs";
|
|
7
|
+
import ts from "typescript";
|
|
8
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
9
|
+
import { chromium } from "playwright";
|
|
7
10
|
import { createServer } from "node:http";
|
|
8
|
-
import { randomBytes } from "node:crypto";
|
|
9
11
|
import MarkdownIt from "markdown-it";
|
|
10
12
|
import { spawn } from "node:child_process";
|
|
13
|
+
//#region src/history.ts
|
|
14
|
+
const SNAPSHOT_LIMIT = 64 * 1024 * 1024;
|
|
15
|
+
const HISTORY_LIMIT = 128 * 1024 * 1024;
|
|
16
|
+
const ENTRY_LIMIT = 50;
|
|
17
|
+
const ignored = new Set([
|
|
18
|
+
"node_modules",
|
|
19
|
+
".git",
|
|
20
|
+
"dist",
|
|
21
|
+
".office-kit"
|
|
22
|
+
]);
|
|
23
|
+
const historyFile = (name) => !name.split(sep).some((part) => ignored.has(part)) && /\.([cm]?[jt]sx?|json|css|md|ya?ml|pptx|png|jpe?g|gif|bmp|tiff?|emf|wmf|svg)$/i.test(name);
|
|
24
|
+
const equal = (a, b) => a === b || !!a && !!b && a.equals(b);
|
|
25
|
+
/** Session-local project history. Overlapping agent turns form one transaction. */
|
|
26
|
+
async function createHistory(directory, notify = () => {}) {
|
|
27
|
+
const root = await realpath(directory);
|
|
28
|
+
let baseline = /* @__PURE__ */ new Map();
|
|
29
|
+
let initialized = false;
|
|
30
|
+
const past = [], future = [];
|
|
31
|
+
const active = /* @__PURE__ */ new Set();
|
|
32
|
+
let label = "";
|
|
33
|
+
let error = null;
|
|
34
|
+
let queue = Promise.resolve();
|
|
35
|
+
let restoring = false;
|
|
36
|
+
function serial(action) {
|
|
37
|
+
const result = queue.then(action);
|
|
38
|
+
queue = result.then(() => {}, () => {});
|
|
39
|
+
return result.catch((cause) => {
|
|
40
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
41
|
+
throw cause;
|
|
42
|
+
}).finally(notify);
|
|
43
|
+
}
|
|
44
|
+
async function snapshot() {
|
|
45
|
+
const files = /* @__PURE__ */ new Map();
|
|
46
|
+
let bytes = 0;
|
|
47
|
+
async function visit(dir) {
|
|
48
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
49
|
+
await Promise.all(entries.map(async (item) => {
|
|
50
|
+
if (ignored.has(item.name) || item.isSymbolicLink()) return;
|
|
51
|
+
const file = join(dir, item.name);
|
|
52
|
+
if (item.isDirectory()) return visit(file);
|
|
53
|
+
if (!item.isFile() || !historyFile(file)) return;
|
|
54
|
+
const info = await lstat(file);
|
|
55
|
+
bytes += info.size;
|
|
56
|
+
if (bytes > SNAPSHOT_LIMIT) throw new Error("Undo history exceeds 64 MiB of project source/assets. Move unused assets outside the deck folder.");
|
|
57
|
+
if (info.isSymbolicLink() || await realpath(file) !== file) throw new Error("Project paths changed while recording history. Try again.");
|
|
58
|
+
const content = await readFile(file);
|
|
59
|
+
files.set(relative(root, file), content);
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
await visit(root);
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
function record(next, description) {
|
|
66
|
+
if (!initialized) {
|
|
67
|
+
baseline = next;
|
|
68
|
+
initialized = true;
|
|
69
|
+
error = null;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const changes = [];
|
|
73
|
+
for (const file of new Set([...baseline.keys(), ...next.keys()])) {
|
|
74
|
+
const before = baseline.get(file), after = next.get(file);
|
|
75
|
+
if (!equal(before, after)) changes.push({
|
|
76
|
+
file,
|
|
77
|
+
before,
|
|
78
|
+
after
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (changes.length) {
|
|
82
|
+
past.push({
|
|
83
|
+
label: description,
|
|
84
|
+
changes
|
|
85
|
+
});
|
|
86
|
+
future.length = 0;
|
|
87
|
+
const size = () => past.reduce((sum, item) => sum + item.changes.reduce((n, change) => n + (change.before?.length ?? 0) + (change.after?.length ?? 0), 0), 0);
|
|
88
|
+
while (past.length > ENTRY_LIMIT || past.length > 1 && size() > HISTORY_LIMIT) past.shift();
|
|
89
|
+
}
|
|
90
|
+
baseline = next;
|
|
91
|
+
error = null;
|
|
92
|
+
}
|
|
93
|
+
async function contents(file) {
|
|
94
|
+
const path = join(root, file);
|
|
95
|
+
let parent = dirname(path);
|
|
96
|
+
while (true) try {
|
|
97
|
+
if (await realpath(parent) !== parent) throw new Error("Undo refused: a project directory is now a symbolic link.");
|
|
98
|
+
break;
|
|
99
|
+
} catch (cause) {
|
|
100
|
+
if (cause.code !== "ENOENT") throw cause;
|
|
101
|
+
parent = dirname(parent);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const info = await lstat(path);
|
|
105
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new Error("Undo refused: a project file changed type.");
|
|
106
|
+
return await readFile(path);
|
|
107
|
+
} catch (cause) {
|
|
108
|
+
if (cause.code === "ENOENT") return void 0;
|
|
109
|
+
throw cause;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function replace(change, expected, next) {
|
|
113
|
+
if (!equal(await contents(change.file), expected)) throw new Error(`History conflict in ${change.file}. Newer changes were preserved.`);
|
|
114
|
+
const file = join(root, change.file);
|
|
115
|
+
if (next) {
|
|
116
|
+
await mkdir(dirname(file), { recursive: true });
|
|
117
|
+
await writeFile(file, next);
|
|
118
|
+
} else await unlink(file);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
record(await snapshot(), "Initial state");
|
|
122
|
+
} catch (cause) {
|
|
123
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
state: () => ({
|
|
127
|
+
undo: past.at(-1)?.label ?? null,
|
|
128
|
+
redo: future.at(-1)?.label ?? null,
|
|
129
|
+
busy: active.size > 0 || restoring,
|
|
130
|
+
error
|
|
131
|
+
}),
|
|
132
|
+
capture: () => serial(async () => {
|
|
133
|
+
if (!active.size && !restoring) record(await snapshot(), "Source edit");
|
|
134
|
+
}),
|
|
135
|
+
begin: (id, description) => serial(async () => {
|
|
136
|
+
if (active.has(id)) return;
|
|
137
|
+
if (!active.size) {
|
|
138
|
+
record(await snapshot(), "Source edit");
|
|
139
|
+
label = description;
|
|
140
|
+
} else label = "Concurrent edits";
|
|
141
|
+
active.add(id);
|
|
142
|
+
}),
|
|
143
|
+
end: (id) => serial(async () => {
|
|
144
|
+
if (!active.delete(id) || active.size) return;
|
|
145
|
+
record(await snapshot(), label);
|
|
146
|
+
}),
|
|
147
|
+
move: (direction) => serial(async () => {
|
|
148
|
+
if (active.size) throw new Error("Wait for all agents and text edits to finish before using Undo or Redo.");
|
|
149
|
+
record(await snapshot(), "Source edit");
|
|
150
|
+
const from = direction === "undo" ? past : future;
|
|
151
|
+
const to = direction === "undo" ? future : past;
|
|
152
|
+
const item = from.at(-1);
|
|
153
|
+
if (!item) throw new Error(`Nothing to ${direction}.`);
|
|
154
|
+
const before = (c) => direction === "undo" ? c.after : c.before;
|
|
155
|
+
const after = (c) => direction === "undo" ? c.before : c.after;
|
|
156
|
+
const applied = [];
|
|
157
|
+
restoring = true;
|
|
158
|
+
try {
|
|
159
|
+
await Promise.all(item.changes.map(async (change) => {
|
|
160
|
+
if (!equal(await contents(change.file), before(change))) throw new Error(`History conflict in ${change.file}. Newer changes were preserved.`);
|
|
161
|
+
}));
|
|
162
|
+
for (const change of item.changes) {
|
|
163
|
+
await replace(change, before(change), after(change));
|
|
164
|
+
applied.push(change);
|
|
165
|
+
}
|
|
166
|
+
for (const change of item.changes) {
|
|
167
|
+
const content = after(change);
|
|
168
|
+
if (content) baseline.set(change.file, content);
|
|
169
|
+
else baseline.delete(change.file);
|
|
170
|
+
}
|
|
171
|
+
from.pop();
|
|
172
|
+
to.push(item);
|
|
173
|
+
error = null;
|
|
174
|
+
} catch (cause) {
|
|
175
|
+
const failures = [];
|
|
176
|
+
for (const change of applied.reverse()) try {
|
|
177
|
+
await replace(change, after(change), before(change));
|
|
178
|
+
} catch (rollback) {
|
|
179
|
+
failures.push(String(rollback));
|
|
180
|
+
}
|
|
181
|
+
if (failures.length) throw new Error(`${String(cause)}; rollback incomplete: ${failures.join("; ")}`);
|
|
182
|
+
throw cause;
|
|
183
|
+
} finally {
|
|
184
|
+
restoring = false;
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/text-edit.ts
|
|
191
|
+
/** Literal edits are deliberately conservative: computed and ambiguous text goes to the agent. */
|
|
192
|
+
function createTextEditor(entry, preview, verify) {
|
|
193
|
+
let busy = false;
|
|
194
|
+
async function restore(change) {
|
|
195
|
+
if (await realpath(change.file) !== change.file || await readFile(change.file, "utf8") !== change.after) throw new Error("Source changed during editing. Your newer changes were preserved.");
|
|
196
|
+
await writeFile(change.file, change.before);
|
|
197
|
+
}
|
|
198
|
+
return { async edit(value) {
|
|
199
|
+
if (busy) throw new Error("A text edit is already running.");
|
|
200
|
+
busy = true;
|
|
201
|
+
try {
|
|
202
|
+
const current = preview();
|
|
203
|
+
if (value.revision !== current.revision) throw new Error("Preview changed. Select the text again.");
|
|
204
|
+
if (!Number.isInteger(value.slide) || !current.slides[value.slide] || typeof value.before !== "string" || !value.before.trim() || value.before.length > 12e3 || typeof value.after !== "string" || value.after.length > 12e3 || value.after === value.before) throw new Error("Invalid text edit.");
|
|
205
|
+
if (current.slideTexts[value.slide].split(value.before).length !== 2) throw new Error("Text cannot be located uniquely on this slide. Use Apply with AI.");
|
|
206
|
+
const root = await realpath(dirname(entry));
|
|
207
|
+
const files = await Promise.all([...new Set(current.dependencies)].filter((file) => /\.[cm]?[jt]sx?$/.test(file)).map(async (file) => {
|
|
208
|
+
const path = await realpath(file), local = relative(root, path);
|
|
209
|
+
if (local.startsWith("..") || isAbsolute(local) || local.split(/[\\/]/).includes("node_modules")) return null;
|
|
210
|
+
return {
|
|
211
|
+
file: path,
|
|
212
|
+
before: await readFile(path, "utf8")
|
|
213
|
+
};
|
|
214
|
+
}));
|
|
215
|
+
const candidates = [];
|
|
216
|
+
for (const item of files) {
|
|
217
|
+
if (!item) continue;
|
|
218
|
+
const source = ts.createSourceFile(item.file, item.before, ts.ScriptTarget.Latest, true, /[jt]sx$/.test(extname(item.file)) ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
219
|
+
const visit = (node) => {
|
|
220
|
+
if ((ts.isJsxText(node) ? node.text.split(/\r\n|\n|\r/).map((line, index, lines) => {
|
|
221
|
+
let value = line.replaceAll(" ", " ");
|
|
222
|
+
if (index) value = value.trimStart();
|
|
223
|
+
if (index < lines.length - 1) value = value.trimEnd();
|
|
224
|
+
return value;
|
|
225
|
+
}).filter(Boolean).join(" ") : ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : void 0) === value.before) {
|
|
226
|
+
const replacement = ts.isJsxText(node) || ts.isJsxAttribute(node.parent) ? "{" + JSON.stringify(value.after) + "}" : JSON.stringify(value.after);
|
|
227
|
+
const start = ts.isJsxText(node) ? node.pos : node.getStart(source);
|
|
228
|
+
candidates.push({
|
|
229
|
+
...item,
|
|
230
|
+
after: item.before.slice(0, start) + replacement + item.before.slice(node.end)
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
ts.forEachChild(node, visit);
|
|
234
|
+
};
|
|
235
|
+
visit(source);
|
|
236
|
+
}
|
|
237
|
+
if (candidates.length !== 1) throw new Error("Text is computed or has multiple source matches. Use Apply with AI.");
|
|
238
|
+
const change = candidates[0];
|
|
239
|
+
if (preview().revision !== current.revision || await realpath(change.file) !== change.file || await readFile(change.file, "utf8") !== change.before) throw new Error("Source changed. Select the text again.");
|
|
240
|
+
await writeFile(change.file, change.after);
|
|
241
|
+
try {
|
|
242
|
+
if (await verify()) throw new Error("The text change did not build.");
|
|
243
|
+
const next = preview();
|
|
244
|
+
if (next.slides.length !== current.slides.length || next.slides.some((svg, index) => index !== value.slide && svg !== current.slides[index]) || next.slideTexts[value.slide] !== current.slideTexts[value.slide].replace(value.before, value.after)) throw new Error("This source affects other text or slides. Use Apply with AI for a local edit.");
|
|
245
|
+
if (await realpath(change.file) !== change.file || await readFile(change.file, "utf8") !== change.after) throw new Error("Source changed during editing.");
|
|
246
|
+
} catch (error) {
|
|
247
|
+
await restore(change);
|
|
248
|
+
await verify();
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
} finally {
|
|
252
|
+
busy = false;
|
|
253
|
+
}
|
|
254
|
+
} };
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/visual-review.ts
|
|
258
|
+
/** Each session compares against its own turn's preview, including offscreen slides. */
|
|
259
|
+
function createVisualReviewer(entry, slides) {
|
|
260
|
+
let before = [];
|
|
261
|
+
let rounds = 0;
|
|
262
|
+
return {
|
|
263
|
+
begin() {
|
|
264
|
+
before = slides().slice();
|
|
265
|
+
rounds = 0;
|
|
266
|
+
},
|
|
267
|
+
async next() {
|
|
268
|
+
const current = slides().slice();
|
|
269
|
+
const changed = current.flatMap((svg, index) => svg !== before[index] ? [{
|
|
270
|
+
svg,
|
|
271
|
+
index
|
|
272
|
+
}] : []);
|
|
273
|
+
if (!changed.length) return;
|
|
274
|
+
if (rounds >= 3) throw new Error("Visual review reached its correction limit. Review the latest slide manually before continuing.");
|
|
275
|
+
const round = rounds + 1;
|
|
276
|
+
let browser;
|
|
277
|
+
try {
|
|
278
|
+
browser = await chromium.launch({
|
|
279
|
+
channel: process.env.PLAYWRIGHT_CHANNEL || "chrome",
|
|
280
|
+
headless: true
|
|
281
|
+
});
|
|
282
|
+
} catch {
|
|
283
|
+
try {
|
|
284
|
+
browser = await chromium.launch({ headless: true });
|
|
285
|
+
} catch {
|
|
286
|
+
throw new Error("Visual review needs Chrome or a Playwright Chromium installation. Install Chrome and retry. The source edit was saved; visual review was not completed.");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const directory = join(dirname(entry), ".office-kit", "reviews", randomUUID());
|
|
291
|
+
await mkdir(directory, { recursive: true });
|
|
292
|
+
const page = await browser.newPage({
|
|
293
|
+
viewport: {
|
|
294
|
+
width: 1600,
|
|
295
|
+
height: 1e3
|
|
296
|
+
},
|
|
297
|
+
deviceScaleFactor: 1
|
|
298
|
+
});
|
|
299
|
+
await page.route("**/*", (route) => route.abort());
|
|
300
|
+
const images = [];
|
|
301
|
+
for (const { svg, index } of changed) {
|
|
302
|
+
await page.setContent("<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:\"><style>html,body{margin:0}svg{display:block;width:1600px;height:auto}</style>" + svg);
|
|
303
|
+
await page.evaluate(() => document.fonts.ready.then(() => void 0));
|
|
304
|
+
const file = join(directory, `slide-${index + 1}.png`);
|
|
305
|
+
await page.locator("svg").first().screenshot({
|
|
306
|
+
path: file,
|
|
307
|
+
timeout: 15e3
|
|
308
|
+
});
|
|
309
|
+
images.push(file);
|
|
310
|
+
}
|
|
311
|
+
const prompt = `Visual review after the edit (round ${round}/3). Open every attached screenshot (or read each local PNG path below) before finishing. Check clipping, overlap, alignment, spacing, legibility and foreground/background contrast. ${round === 3 ? "This is the final review: do not edit further; report any remaining defects to the user." : "Fix concrete design defects only on these affected slides;"} preserve the user's exact text and unrelated work. Do not claim visual verification unless you inspected the images. If there are no defects, finish without edits. Screenshots: ${JSON.stringify(images)}`;
|
|
312
|
+
await writeFile(join(directory, "review.txt"), prompt);
|
|
313
|
+
before = current;
|
|
314
|
+
rounds = round;
|
|
315
|
+
return {
|
|
316
|
+
prompt,
|
|
317
|
+
images
|
|
318
|
+
};
|
|
319
|
+
} finally {
|
|
320
|
+
await browser.close();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
//#endregion
|
|
11
326
|
//#region src/styles.ts
|
|
12
327
|
const previewStyles = `*{box-sizing:border-box}
|
|
13
328
|
body{margin:0;height:100dvh;overflow:hidden;background:#e9eaf4;color:#dce1f1;font:13px system-ui;display:grid;grid-template-rows:60px minmax(0,1fr) 42px}
|
|
@@ -77,6 +392,8 @@ const page = `<!doctype html>
|
|
|
77
392
|
<link rel="stylesheet" href="/terminal.css">
|
|
78
393
|
<style>
|
|
79
394
|
${previewStyles}
|
|
395
|
+
.slide-edit-tools{display:flex;gap:6px;padding:8px 14px;flex-shrink:0}.slide-edit-tools{align-items:center;flex-wrap:wrap}.slide-edit-tools span{font-size:11px;color:#625873;flex:1}.slide-edit-tools small{font-size:11px;color:#634396}.slide-hover{position:fixed;border:1px solid #b094ff;pointer-events:none;z-index:19;border-radius:2px}.slide-text-input{position:fixed;z-index:22;resize:none;background:#202438;color:#fff;border:2px solid #b094ff;border-radius:3px;padding:0;box-shadow:0 0 0 3px #b094ff30;line-height:1.15}.slide-text-input[hidden],.slide-hover[hidden]{display:none}.slide-selection{position:fixed;border:2px solid #b094ff;background:#a080ff18;pointer-events:none;z-index:20;box-shadow:0 0 0 1px #201632}.slide-edit-panel{position:fixed;z-index:21;width:min(380px,calc(100vw - 16px));padding:14px;background:#202438;border:1px solid #776591;border-radius:14px;box-shadow:0 16px 50px #0008;display:flex;flex-direction:column;gap:12px}.slide-edit-panel[hidden],.slide-selection[hidden]{display:none}.selection-heading,.selection-actions{display:flex;align-items:center;gap:8px}.selection-heading strong{flex:1}.slide-edit-panel textarea{width:100%;min-height:90px;max-height:35vh;resize:vertical;background:#141a2b;color:#f4efff;border:1px solid #514969;border-radius:8px;padding:10px;font:inherit}.selection-actions{flex-wrap:wrap}.selection-actions select{min-width:80px;flex:1}.slide-edit-panel small{color:#d1c9e3;line-height:1.5}.presenting .slide-edit-tools{display:none}
|
|
396
|
+
|
|
80
397
|
#agent-workspace{position:relative;flex:1;min-height:0;min-width:0;display:flex;overflow:auto}.agent-pane{position:absolute;padding:3px 5px;display:flex;flex-direction:column;flex:1;min-width:0;min-height:0;overflow:hidden}.agent-pane iframe{border:0;width:100%;flex:1;min-height:0;border-radius:0 0 9px 9px}.agent-tools{display:flex;gap:4px;align-items:center;flex-wrap:wrap;padding:6px;background:#20253a;border:1px solid #34334e;border-radius:9px 9px 0 0}.agent-tools span{flex:1;font-size:10px;letter-spacing:.04em;color:#b3abc9}.agent-tools svg{width:14px;height:14px}.agent-pane:focus-within .agent-tools{border-color:#8b6ed577;background:#29243e}.agent-tools button{font-size:11px;padding:5px;background:transparent;border:0;display:grid;place-items:center}.agent-divider{position:absolute;z-index:2;background:#101422;cursor:col-resize;touch-action:none}.agent-divider.vertical{cursor:row-resize}.agent-divider:hover,.agent-divider:focus-visible{background:#9b87ff}.splitting-agents iframe,.resizing-chat iframe{pointer-events:none}
|
|
81
398
|
</style>
|
|
82
399
|
<header><span class="brand"><svg viewBox="0 0 36 36" aria-hidden="true"><defs><linearGradient id="brand-gradient" x2="1" y2="1"><stop stop-color="#c39aff"/><stop offset="1" stop-color="#7165f4"/></linearGradient></defs><rect x="2" y="2" width="32" height="32" rx="10" fill="url(#brand-gradient)"/><path d="M11 10h9l6 6v10H11z" fill="none" stroke="white" stroke-width="1.8" stroke-linejoin="round"/><path d="M20 10v7h6M15 21h7M15 25h4" fill="none" stroke="white" stroke-width="1.8" stroke-linecap="round"/><path d="m28 3 1.2 3.8L33 8l-3.8 1.2L28 13l-1.2-3.8L23 8l3.8-1.2z" fill="#e7dbff"/></svg>Office <em>Kit</em></span><span class="badge">Studio</span><span id="status" role="status">Building…</span><button id="toggle-chat" aria-expanded="true" aria-controls="chat">✦ Agents</button><button id="present" disabled>Present</button><a class="download" href="/deck.pptx">Download PPTX</a></header>
|
|
@@ -87,7 +404,7 @@ ${previewStyles}
|
|
|
87
404
|
<div id="chat-resizer" role="separator" tabindex="0" aria-label="Chat width" aria-orientation="vertical" aria-controls="chat" title="Drag to resize · Double-click to reset"></div>
|
|
88
405
|
<div class="workspace-heading"><span>✦ AI WORKSPACE</span><b>LOCAL</b></div><div id="agent-workspace"></div><div id="chat-context" hidden></div></aside>
|
|
89
406
|
</div>
|
|
90
|
-
<footer><span id="count" aria-live="polite">No slides</span><span class="hint">
|
|
407
|
+
<footer><span id="count" aria-live="polite">No slides</span><span class="hint">Select an area to ask AI · Edit text directly</span><button id="prev" aria-label="Previous slide" disabled>‹</button><button id="next" aria-label="Next slide" disabled>›</button><label for="zoom">Zoom</label><select id="zoom"><option value="fit">Fit</option><option value="0.5">50%</option><option value="0.75">75%</option><option value="1">100%</option><option value="1.25">125%</option><option value="1.5">150%</option><option value="2">200%</option></select></footer>
|
|
91
408
|
<div id="presentation-controls"><button id="present-prev" aria-label="Previous slide">‹</button><span id="present-count"></span><button id="present-next" aria-label="Next slide">›</button><button id="exit-present">Exit · Esc</button></div>
|
|
92
409
|
<script>
|
|
93
410
|
let state={slides:[],error:null,aspectRatio:16/9},index=0,urls=[],presenting=false;
|
|
@@ -236,7 +553,7 @@ async function refresh(){
|
|
|
236
553
|
catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}
|
|
237
554
|
}
|
|
238
555
|
byId('toggle-chat').onclick=()=>{const hidden=document.body.classList.toggle('chat-hidden');byId('toggle-chat').setAttribute('aria-expanded',String(!hidden));resize();};
|
|
239
|
-
const events=new EventSource('/events');events.onmessage=event=>{if(event.data==='chat'){window.dispatchEvent(new Event('agent-chat'));return;}if(event.data==='ready'){delete state.revision;}void refresh();};events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();
|
|
556
|
+
const events=new EventSource('/events');events.onmessage=event=>{if(event.data==='history'){window.dispatchEvent(new Event('agent-history'));return;}if(event.data==='chat'){window.dispatchEvent(new Event('agent-chat'));return;}if(event.data==='ready'){delete state.revision;}void refresh();};events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();
|
|
240
557
|
<\/script><script type="module" src="/terminal.js"><\/script></html>`;
|
|
241
558
|
//#endregion
|
|
242
559
|
//#region src/agent-page.ts
|
|
@@ -257,6 +574,10 @@ window.addEventListener('message',event=>{
|
|
|
257
574
|
if(event.origin!==location.origin||event.source!==parent)return;
|
|
258
575
|
if(event.data?.type==='focus'){focus=event.data.focus;byId('chat-context').dataset.focus=JSON.stringify(focus);byId('chat-context').textContent=event.data.label;}
|
|
259
576
|
if(event.data?.type==='chat')void refreshChat();
|
|
577
|
+
if(event.data?.type==='inline-edit'&&byId('chat-provider').value==='codex'){
|
|
578
|
+
void (async()=>{try{if(chatBusy)throw new Error('This agent is working. Wait for it to finish.');await chatAction('/chat',{message:event.data.message,provider:'codex',...event.data.focus});parent.postMessage({type:'inline-result',id:event.data.id},location.origin);}catch(error){parent.postMessage({type:'inline-result',id:event.data.id,error:error.message},location.origin);}})();
|
|
579
|
+
}
|
|
580
|
+
|
|
260
581
|
});
|
|
261
582
|
parent.postMessage({type:'agent-ready'},location.origin);
|
|
262
583
|
let chatRequest=0,chatBusy=false,lastMessages='';
|
|
@@ -316,8 +637,9 @@ Choose foreground and actual background colors together. Explicitly set readable
|
|
|
316
637
|
//#region src/terminal.ts
|
|
317
638
|
const REQUEST_LIMIT = 128e3;
|
|
318
639
|
/** A real interactive Claude session, shared across reloads of its owning tab. */
|
|
319
|
-
function createTerminal(entry, busy = () => false, base = "", verify = async () => null) {
|
|
640
|
+
function createTerminal(entry, busy = () => false, base = "", verify = async () => null, review, history) {
|
|
320
641
|
let repairs = 0;
|
|
642
|
+
let working = false;
|
|
321
643
|
let process;
|
|
322
644
|
let owner = "";
|
|
323
645
|
let output = "";
|
|
@@ -387,13 +709,45 @@ function createTerminal(entry, busy = () => false, base = "", verify = async ()
|
|
|
387
709
|
reason: `The actual preview build failed. Repair the source before finishing (attempt ${repairs}/3). Preserve unrelated edits, inspect the installed DSL exports and local components before changing imports. Do not hide the error or remove requested content. No Raw is needed for lists or an agenda: use Text paragraphs/bullets or mapped Text rows. Build error:\n${error.slice(0, 16e3)}`
|
|
388
710
|
});
|
|
389
711
|
} else {
|
|
390
|
-
|
|
712
|
+
if (!error && review) try {
|
|
713
|
+
const visual = await review.next();
|
|
714
|
+
if (closing || stopping || !process) {
|
|
715
|
+
json(200, {});
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (visual) {
|
|
719
|
+
status = "Reviewing slide screenshots…";
|
|
720
|
+
json(200, {
|
|
721
|
+
decision: "block",
|
|
722
|
+
reason: visual.prompt
|
|
723
|
+
});
|
|
724
|
+
emit("state", snapshot());
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
} catch (cause) {
|
|
728
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
await history?.end();
|
|
732
|
+
} catch (cause) {
|
|
733
|
+
error = "History: " + String(cause);
|
|
734
|
+
}
|
|
735
|
+
working = false;
|
|
736
|
+
status = error ? "Preview verification incomplete: " + error : "Preview build verified";
|
|
391
737
|
json(200, {});
|
|
392
738
|
}
|
|
393
739
|
emit("state", snapshot());
|
|
394
740
|
return;
|
|
395
741
|
}
|
|
742
|
+
try {
|
|
743
|
+
await history?.begin();
|
|
744
|
+
} catch (cause) {
|
|
745
|
+
json(409, { error: String(cause) });
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
396
748
|
repairs = 0;
|
|
749
|
+
working = true;
|
|
750
|
+
review?.begin();
|
|
397
751
|
json(200, { hookSpecificOutput: {
|
|
398
752
|
hookEventName: "UserPromptSubmit",
|
|
399
753
|
additionalContext: `You are editing the local PowerPoint TSX project at ${resolve(entry)}.
|
|
@@ -465,7 +819,7 @@ Preview context captured with the latest terminal input: ${JSON.stringify(contex
|
|
|
465
819
|
type: "http",
|
|
466
820
|
url: `${origin}${base}/terminal/verify`,
|
|
467
821
|
headers: { Authorization: `Bearer ${token}` },
|
|
468
|
-
timeout:
|
|
822
|
+
timeout: 120
|
|
469
823
|
}] }],
|
|
470
824
|
UserPromptSubmit: [{ hooks: [{
|
|
471
825
|
type: "http",
|
|
@@ -494,19 +848,53 @@ Preview context captured with the latest terminal input: ${JSON.stringify(contex
|
|
|
494
848
|
output = (output + data).slice(-2e6);
|
|
495
849
|
emit("output", data);
|
|
496
850
|
});
|
|
497
|
-
completion = new Promise((done) => process.onExit(({ exitCode }) => {
|
|
851
|
+
completion = new Promise((done) => process.onExit(async ({ exitCode }) => {
|
|
498
852
|
process = void 0;
|
|
853
|
+
working = false;
|
|
499
854
|
stopping = false;
|
|
500
855
|
status = `Claude Code exited (${exitCode}). Start to open a new session.`;
|
|
856
|
+
try {
|
|
857
|
+
await history?.end();
|
|
858
|
+
} catch (cause) {
|
|
859
|
+
status += " History: " + String(cause);
|
|
860
|
+
}
|
|
501
861
|
emit("state", snapshot());
|
|
502
862
|
done();
|
|
503
863
|
}));
|
|
504
864
|
}
|
|
865
|
+
} else if (request.url === "/terminal/prompt") {
|
|
866
|
+
if (!process) throw new Error("Start Claude Code in the selected agent pane first.");
|
|
867
|
+
if (working) throw new Error("Claude Code is working. Wait for the current turn to finish.");
|
|
868
|
+
if (typeof value.message !== "string" || !value.message.trim() || value.message.length > 16e3 || [...value.message].some((character) => {
|
|
869
|
+
const code = character.charCodeAt(0);
|
|
870
|
+
return code < 32 && code !== 9 && code !== 10 || code === 127;
|
|
871
|
+
})) throw new Error("Invalid inline prompt");
|
|
872
|
+
updateFocus();
|
|
873
|
+
working = true;
|
|
874
|
+
try {
|
|
875
|
+
await history?.begin();
|
|
876
|
+
} catch (cause) {
|
|
877
|
+
working = false;
|
|
878
|
+
throw cause;
|
|
879
|
+
}
|
|
880
|
+
if (!process || closing || stopping) {
|
|
881
|
+
await history?.end();
|
|
882
|
+
working = false;
|
|
883
|
+
throw new Error("Claude Code stopped before submitting.");
|
|
884
|
+
}
|
|
885
|
+
process.write("\x1B[200~" + value.message + "\x1B[201~");
|
|
886
|
+
await new Promise((done) => setTimeout(done, 100));
|
|
887
|
+
if (!process || stopping) throw new Error("Claude Code stopped before submitting.");
|
|
888
|
+
process.write("\r");
|
|
505
889
|
} else if (request.url === "/terminal/input") {
|
|
506
890
|
if (!process) throw new Error("Start Claude Code first");
|
|
507
891
|
if (typeof value.data !== "string" || value.data.length > 32e3) throw new Error("Invalid terminal input");
|
|
508
892
|
updateFocus();
|
|
509
893
|
process.write(value.data);
|
|
894
|
+
if (value.data.includes("")) {
|
|
895
|
+
working = false;
|
|
896
|
+
await history?.end();
|
|
897
|
+
}
|
|
510
898
|
} else if (request.url === "/terminal/resize") {
|
|
511
899
|
size();
|
|
512
900
|
process?.resize(value.cols, value.rows);
|
|
@@ -540,7 +928,9 @@ const markdown = new MarkdownIt({
|
|
|
540
928
|
});
|
|
541
929
|
markdown.renderer.rules.image = (tokens, index) => markdown.utils.escapeHtml(tokens[index]?.content ?? "");
|
|
542
930
|
/** One shared conversation and one editing process per dev server. */
|
|
543
|
-
function createChat(entry, notify, busy = () => false, verify = async () => null) {
|
|
931
|
+
function createChat(entry, notify, busy = () => false, verify = async () => null, review, reviewImages = () => [], history) {
|
|
932
|
+
let starting = false;
|
|
933
|
+
let closing = false;
|
|
544
934
|
let messages = [];
|
|
545
935
|
let provider = "codex";
|
|
546
936
|
let child;
|
|
@@ -554,7 +944,7 @@ function createChat(entry, notify, busy = () => false, verify = async () => null
|
|
|
554
944
|
...message.role === "assistant" ? { html: markdown.render(message.text) } : {}
|
|
555
945
|
})),
|
|
556
946
|
provider,
|
|
557
|
-
running: !!child,
|
|
947
|
+
running: starting || !!child,
|
|
558
948
|
status
|
|
559
949
|
});
|
|
560
950
|
function stop() {
|
|
@@ -576,14 +966,15 @@ function createChat(entry, notify, busy = () => false, verify = async () => null
|
|
|
576
966
|
killTimer = setTimeout(() => kill("SIGKILL"), 2e3);
|
|
577
967
|
notify();
|
|
578
968
|
}
|
|
579
|
-
function start(text, selected, context, repairs = 0) {
|
|
969
|
+
function start(text, selected, context, repairs = 0, images = [], reviewing = false) {
|
|
970
|
+
if (!repairs && !reviewing) review?.begin();
|
|
580
971
|
provider = selected;
|
|
581
972
|
const user = {
|
|
582
973
|
role: "user",
|
|
583
974
|
text,
|
|
584
975
|
context
|
|
585
976
|
};
|
|
586
|
-
const
|
|
977
|
+
const conversation = messages.slice(-12).map((message) => ({
|
|
587
978
|
...message,
|
|
588
979
|
text: message.text.slice(-12e3)
|
|
589
980
|
}));
|
|
@@ -594,7 +985,7 @@ ${authoringGuidance}
|
|
|
594
985
|
The attached focus is context, NOT a restriction: use it for references such as "this slide". For other slides or deck-wide requests, find the relevant source or shared theme instead. Do not rewrite the whole deck for a local edit.
|
|
595
986
|
Slide numbers are 1-based and refer to the last successful preview at send time. Source may have changed since then; verify it before editing. Files are dependency candidates, not an exact slide-to-source mapping.
|
|
596
987
|
Respond in the user's language, briefly describing edits or answering their question. If permissions prevent an action, explain that instead of claiming success.
|
|
597
|
-
Recent conversation (JSON): ${JSON.stringify(
|
|
988
|
+
Recent conversation (JSON): ${JSON.stringify(conversation)}
|
|
598
989
|
Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
599
990
|
messages.push(user);
|
|
600
991
|
const answer = {
|
|
@@ -605,14 +996,16 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
605
996
|
if (messages.length > 100) messages = messages.slice(-100);
|
|
606
997
|
status = "Working…";
|
|
607
998
|
stopping = false;
|
|
608
|
-
|
|
999
|
+
const args = [
|
|
609
1000
|
"exec",
|
|
610
1001
|
"--json",
|
|
611
1002
|
"--sandbox",
|
|
612
1003
|
"workspace-write",
|
|
613
1004
|
"--skip-git-repo-check",
|
|
614
1005
|
"-"
|
|
615
|
-
]
|
|
1006
|
+
];
|
|
1007
|
+
for (const image of images) args.splice(args.length - 1, 0, "--image", image);
|
|
1008
|
+
child = spawn(selected, args, {
|
|
616
1009
|
cwd: dirname(resolve(entry)),
|
|
617
1010
|
stdio: [
|
|
618
1011
|
"pipe",
|
|
@@ -684,12 +1077,30 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
684
1077
|
start(`The actual preview build failed. Repair the source and preserve unrelated work (attempt ${repairs + 1}/3). Do not remove requested content or hide the error.\n${buildError.slice(0, 16e3)}`, selected, {
|
|
685
1078
|
...context,
|
|
686
1079
|
buildError
|
|
687
|
-
}, repairs + 1);
|
|
1080
|
+
}, repairs + 1, [], reviewing);
|
|
688
1081
|
await completion;
|
|
689
1082
|
done();
|
|
690
1083
|
return;
|
|
691
1084
|
}
|
|
692
|
-
|
|
1085
|
+
let reviewFailure = "";
|
|
1086
|
+
if (!stopping && !buildError && review) try {
|
|
1087
|
+
const visual = await review.next();
|
|
1088
|
+
if (!stopping && visual) {
|
|
1089
|
+
child = void 0;
|
|
1090
|
+
start(visual.prompt, selected, context, 0, visual.images, true);
|
|
1091
|
+
await completion;
|
|
1092
|
+
done();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
reviewFailure = error instanceof Error ? error.message : String(error);
|
|
1097
|
+
}
|
|
1098
|
+
status = stopping ? "Stopped. Saved edits remain in the preview." : buildError ? "Preview still has errors after 3 repair attempts.\n" + buildError.slice(0, 16e3) : reviewFailure || "Done";
|
|
1099
|
+
}
|
|
1100
|
+
try {
|
|
1101
|
+
await history?.end();
|
|
1102
|
+
} catch (cause) {
|
|
1103
|
+
status = "History: " + String(cause);
|
|
693
1104
|
}
|
|
694
1105
|
if (status !== "Done") append(status);
|
|
695
1106
|
child = void 0;
|
|
@@ -732,7 +1143,7 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
732
1143
|
json(200, snapshot());
|
|
733
1144
|
return;
|
|
734
1145
|
}
|
|
735
|
-
if (child) {
|
|
1146
|
+
if (child || starting || closing) {
|
|
736
1147
|
json(409, { error: "An edit is already running" });
|
|
737
1148
|
return;
|
|
738
1149
|
}
|
|
@@ -756,7 +1167,17 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
756
1167
|
return;
|
|
757
1168
|
}
|
|
758
1169
|
const context = focus(value.slide, value.revision);
|
|
759
|
-
|
|
1170
|
+
starting = true;
|
|
1171
|
+
try {
|
|
1172
|
+
await history?.begin();
|
|
1173
|
+
if (closing) {
|
|
1174
|
+
await history?.end();
|
|
1175
|
+
throw new Error("Agent is closing");
|
|
1176
|
+
}
|
|
1177
|
+
start(value.message.trim(), value.provider, context, 0, reviewImages(value.message.trim()));
|
|
1178
|
+
} finally {
|
|
1179
|
+
starting = false;
|
|
1180
|
+
}
|
|
760
1181
|
json(202, snapshot());
|
|
761
1182
|
} catch (cause) {
|
|
762
1183
|
json(400, { error: cause instanceof Error ? cause.message : "Invalid request" });
|
|
@@ -764,8 +1185,9 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
764
1185
|
}
|
|
765
1186
|
return {
|
|
766
1187
|
handle,
|
|
767
|
-
isRunning: () => !!child,
|
|
1188
|
+
isRunning: () => starting || !!child,
|
|
768
1189
|
async close() {
|
|
1190
|
+
closing = true;
|
|
769
1191
|
stop();
|
|
770
1192
|
await completion;
|
|
771
1193
|
}
|
|
@@ -784,13 +1206,23 @@ async function serveDeck(entry, port = 4173) {
|
|
|
784
1206
|
let closed = false;
|
|
785
1207
|
const clients = /* @__PURE__ */ new Set();
|
|
786
1208
|
const sessions = /* @__PURE__ */ new Map();
|
|
1209
|
+
const history = await createHistory(dirname(resolve(entry)), () => {
|
|
1210
|
+
for (const client of clients) client.write("data: history\n\n");
|
|
1211
|
+
});
|
|
1212
|
+
let textEdits = 0;
|
|
1213
|
+
const capturedReviews = /* @__PURE__ */ new Map();
|
|
787
1214
|
function session(id) {
|
|
788
1215
|
let result = sessions.get(id);
|
|
789
1216
|
if (!result) {
|
|
1217
|
+
const review = createVisualReviewer(resolve(entry), () => latest?.slides ?? []);
|
|
1218
|
+
const turnHistory = {
|
|
1219
|
+
begin: () => history.begin("agent:" + id, "AI edit"),
|
|
1220
|
+
end: () => history.end("agent:" + id)
|
|
1221
|
+
};
|
|
790
1222
|
const chat = createChat(entry, () => {
|
|
791
1223
|
for (const client of clients) client.write("data: chat\n\n");
|
|
792
|
-
}, () => terminal.isRunning(), verify);
|
|
793
|
-
const terminal = createTerminal(entry, () => chat.isRunning(), id ? "/agents/" + id : "", verify);
|
|
1224
|
+
}, () => terminal.isRunning(), verify, review, (prompt) => capturedReviews.get(prompt) ?? [], turnHistory);
|
|
1225
|
+
const terminal = createTerminal(entry, () => chat.isRunning(), id ? "/agents/" + id : "", verify, review, turnHistory);
|
|
794
1226
|
result = {
|
|
795
1227
|
chat,
|
|
796
1228
|
terminal,
|
|
@@ -800,6 +1232,13 @@ async function serveDeck(entry, port = 4173) {
|
|
|
800
1232
|
}
|
|
801
1233
|
return result;
|
|
802
1234
|
}
|
|
1235
|
+
const textEditor = createTextEditor(resolve(entry), () => {
|
|
1236
|
+
if (!latest || building || pending || timer || error) throw new Error("Wait for a successful preview build.");
|
|
1237
|
+
return {
|
|
1238
|
+
...latest,
|
|
1239
|
+
revision
|
|
1240
|
+
};
|
|
1241
|
+
}, verify);
|
|
803
1242
|
const server = createServer((request, response) => {
|
|
804
1243
|
const host = request.headers.host;
|
|
805
1244
|
if (host !== `127.0.0.1:${actualPort}` && host !== `localhost:${actualPort}`) {
|
|
@@ -807,11 +1246,63 @@ async function serveDeck(entry, port = 4173) {
|
|
|
807
1246
|
return;
|
|
808
1247
|
}
|
|
809
1248
|
response.setHeader("Cache-Control", "no-store");
|
|
810
|
-
const match = request.url?.match(/^\/agents\/([\w-]{1,64})(\/(?:chat(?:\/(?:stop|reset))?|terminal\/(?:start|input|resize|stop|events|context|verify)|close))?$/);
|
|
1249
|
+
const match = request.url?.match(/^\/agents\/([\w-]{1,64})(\/(?:chat(?:\/(?:stop|reset))?|terminal\/(?:start|input|prompt|resize|stop|events|context|verify)|close))?$/);
|
|
811
1250
|
if (request.url?.startsWith("/agents/") && !match) {
|
|
812
1251
|
response.writeHead(404).end();
|
|
813
1252
|
return;
|
|
814
1253
|
}
|
|
1254
|
+
if (request.url === "/history" && request.method === "GET") {
|
|
1255
|
+
response.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(history.state()));
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (request.url === "/text-edit" || request.url === "/history/undo" || request.url === "/history/redo") {
|
|
1259
|
+
if (request.method !== "POST" || request.headers.origin !== `http://${host}` || request.headers["content-type"] !== "application/json") {
|
|
1260
|
+
response.writeHead(403).end();
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
(async () => {
|
|
1264
|
+
let body = "";
|
|
1265
|
+
request.setEncoding("utf8");
|
|
1266
|
+
for await (const chunk of request) {
|
|
1267
|
+
body += chunk;
|
|
1268
|
+
if (Buffer.byteLength(body) > 1e5) throw new Error("Request too large");
|
|
1269
|
+
}
|
|
1270
|
+
if (request.url !== "/text-edit") {
|
|
1271
|
+
await history.move(request.url === "/history/undo" ? "undo" : "redo");
|
|
1272
|
+
const buildError = await verify();
|
|
1273
|
+
response.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
|
|
1274
|
+
...history.state(),
|
|
1275
|
+
buildError
|
|
1276
|
+
}));
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const manualReview = createVisualReviewer(resolve(entry), () => latest?.slides ?? []);
|
|
1280
|
+
manualReview.begin();
|
|
1281
|
+
const editId = "text:" + ++textEdits;
|
|
1282
|
+
await history.begin(editId, "Text edit");
|
|
1283
|
+
try {
|
|
1284
|
+
await textEditor.edit(JSON.parse(body));
|
|
1285
|
+
} finally {
|
|
1286
|
+
await history.end(editId);
|
|
1287
|
+
}
|
|
1288
|
+
let review;
|
|
1289
|
+
let reviewError;
|
|
1290
|
+
try {
|
|
1291
|
+
review = await manualReview.next();
|
|
1292
|
+
if (review) {
|
|
1293
|
+
capturedReviews.set(review.prompt, review.images);
|
|
1294
|
+
if (capturedReviews.size > 64) capturedReviews.delete(capturedReviews.keys().next().value);
|
|
1295
|
+
}
|
|
1296
|
+
} catch (cause) {
|
|
1297
|
+
reviewError = cause instanceof Error ? cause.message : String(cause);
|
|
1298
|
+
}
|
|
1299
|
+
response.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
|
|
1300
|
+
review,
|
|
1301
|
+
reviewError
|
|
1302
|
+
}));
|
|
1303
|
+
})().catch((cause) => response.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({ error: cause instanceof Error ? cause.message : String(cause) })));
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
815
1306
|
const id = match?.[1] ?? "";
|
|
816
1307
|
if (match) {
|
|
817
1308
|
if (request.headers["sec-fetch-site"] === "cross-site" || request.headers.origin && request.headers.origin !== `http://${host}`) {
|
|
@@ -944,6 +1435,7 @@ async function serveDeck(entry, port = 4173) {
|
|
|
944
1435
|
if (started === generation) error = cause instanceof Error ? cause.stack ?? cause.message : String(cause);
|
|
945
1436
|
} finally {
|
|
946
1437
|
building = false;
|
|
1438
|
+
history.capture().catch(() => {});
|
|
947
1439
|
for (const client of clients) client.write("data: updated\n\n");
|
|
948
1440
|
if (pending && !timer) {
|
|
949
1441
|
pending = false;
|
|
@@ -959,7 +1451,7 @@ async function serveDeck(entry, port = 4173) {
|
|
|
959
1451
|
"dist",
|
|
960
1452
|
".office-kit"
|
|
961
1453
|
].includes(part))) return;
|
|
962
|
-
if (
|
|
1454
|
+
if (!historyFile(filename)) return;
|
|
963
1455
|
generation++;
|
|
964
1456
|
builder.cancel();
|
|
965
1457
|
clearTimeout(timer);
|
|
@@ -1015,7 +1507,7 @@ try {
|
|
|
1015
1507
|
if (args.length && (args.length !== 2 || !option || args[0] !== option || !args[1])) throw new Error(`Unexpected arguments: ${args.join(" ")}\n${usage}`);
|
|
1016
1508
|
if (command === "init") {
|
|
1017
1509
|
const directory = await initProject(entry);
|
|
1018
|
-
console.log(`Created ${directory}\
|
|
1510
|
+
console.log(`Created ${directory}\nInside that directory, run npm install once.\nStart development: npm run dev (or npx office-pptx dev deck.tsx).`);
|
|
1019
1511
|
} else if (command === "inspect") console.log(JSON.stringify(await inspectTemplate(entry), null, 2));
|
|
1020
1512
|
else if (command === "build") {
|
|
1021
1513
|
const output = resolve(args[1] ?? "deck.pptx");
|