@office-kit/pptx-dev 0.7.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 +46 -17
- package/dist/cli.mjs +324 -88
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +2 -19
- package/dist/terminal-client.js +16 -16
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
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, mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { chmod, lstat, mkdir, readFile, readdir, realpath, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { watch } from "node:fs";
|
|
7
7
|
import ts from "typescript";
|
|
@@ -10,87 +10,248 @@ import { chromium } from "playwright";
|
|
|
10
10
|
import { createServer } from "node:http";
|
|
11
11
|
import MarkdownIt from "markdown-it";
|
|
12
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
|
|
13
190
|
//#region src/text-edit.ts
|
|
14
191
|
/** Literal edits are deliberately conservative: computed and ambiguous text goes to the agent. */
|
|
15
192
|
function createTextEditor(entry, preview, verify) {
|
|
16
193
|
let busy = false;
|
|
17
|
-
let undo;
|
|
18
194
|
async function restore(change) {
|
|
19
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.");
|
|
20
196
|
await writeFile(change.file, change.before);
|
|
21
197
|
}
|
|
22
|
-
return {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
visit(source);
|
|
61
|
-
}
|
|
62
|
-
if (candidates.length !== 1) throw new Error("Text is computed or has multiple source matches. Use Apply with AI.");
|
|
63
|
-
const change = candidates[0];
|
|
64
|
-
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.");
|
|
65
|
-
await writeFile(change.file, change.after);
|
|
66
|
-
try {
|
|
67
|
-
if (await verify()) throw new Error("The text change did not build.");
|
|
68
|
-
const next = preview();
|
|
69
|
-
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.");
|
|
70
|
-
if (await realpath(change.file) !== change.file || await readFile(change.file, "utf8") !== change.after) throw new Error("Source changed during editing.");
|
|
71
|
-
undo = change;
|
|
72
|
-
} catch (error) {
|
|
73
|
-
await restore(change);
|
|
74
|
-
await verify();
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
77
|
-
} finally {
|
|
78
|
-
busy = false;
|
|
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);
|
|
79
236
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
busy = true;
|
|
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);
|
|
85
241
|
try {
|
|
86
|
-
await
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
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;
|
|
91
250
|
}
|
|
251
|
+
} finally {
|
|
252
|
+
busy = false;
|
|
92
253
|
}
|
|
93
|
-
};
|
|
254
|
+
} };
|
|
94
255
|
}
|
|
95
256
|
//#endregion
|
|
96
257
|
//#region src/visual-review.ts
|
|
@@ -231,7 +392,7 @@ const page = `<!doctype html>
|
|
|
231
392
|
<link rel="stylesheet" href="/terminal.css">
|
|
232
393
|
<style>
|
|
233
394
|
${previewStyles}
|
|
234
|
-
.slide-edit-tools{display:flex;gap:6px;padding:8px 14px;flex-shrink:0}.slide-edit-tools
|
|
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}
|
|
235
396
|
|
|
236
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}
|
|
237
398
|
</style>
|
|
@@ -392,7 +553,7 @@ async function refresh(){
|
|
|
392
553
|
catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}
|
|
393
554
|
}
|
|
394
555
|
byId('toggle-chat').onclick=()=>{const hidden=document.body.classList.toggle('chat-hidden');byId('toggle-chat').setAttribute('aria-expanded',String(!hidden));resize();};
|
|
395
|
-
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();
|
|
396
557
|
<\/script><script type="module" src="/terminal.js"><\/script></html>`;
|
|
397
558
|
//#endregion
|
|
398
559
|
//#region src/agent-page.ts
|
|
@@ -476,7 +637,7 @@ Choose foreground and actual background colors together. Explicitly set readable
|
|
|
476
637
|
//#region src/terminal.ts
|
|
477
638
|
const REQUEST_LIMIT = 128e3;
|
|
478
639
|
/** A real interactive Claude session, shared across reloads of its owning tab. */
|
|
479
|
-
function createTerminal(entry, busy = () => false, base = "", verify = async () => null, review) {
|
|
640
|
+
function createTerminal(entry, busy = () => false, base = "", verify = async () => null, review, history) {
|
|
480
641
|
let repairs = 0;
|
|
481
642
|
let working = false;
|
|
482
643
|
let process;
|
|
@@ -566,6 +727,11 @@ function createTerminal(entry, busy = () => false, base = "", verify = async ()
|
|
|
566
727
|
} catch (cause) {
|
|
567
728
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
568
729
|
}
|
|
730
|
+
try {
|
|
731
|
+
await history?.end();
|
|
732
|
+
} catch (cause) {
|
|
733
|
+
error = "History: " + String(cause);
|
|
734
|
+
}
|
|
569
735
|
working = false;
|
|
570
736
|
status = error ? "Preview verification incomplete: " + error : "Preview build verified";
|
|
571
737
|
json(200, {});
|
|
@@ -573,6 +739,12 @@ function createTerminal(entry, busy = () => false, base = "", verify = async ()
|
|
|
573
739
|
emit("state", snapshot());
|
|
574
740
|
return;
|
|
575
741
|
}
|
|
742
|
+
try {
|
|
743
|
+
await history?.begin();
|
|
744
|
+
} catch (cause) {
|
|
745
|
+
json(409, { error: String(cause) });
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
576
748
|
repairs = 0;
|
|
577
749
|
working = true;
|
|
578
750
|
review?.begin();
|
|
@@ -676,11 +848,16 @@ Preview context captured with the latest terminal input: ${JSON.stringify(contex
|
|
|
676
848
|
output = (output + data).slice(-2e6);
|
|
677
849
|
emit("output", data);
|
|
678
850
|
});
|
|
679
|
-
completion = new Promise((done) => process.onExit(({ exitCode }) => {
|
|
851
|
+
completion = new Promise((done) => process.onExit(async ({ exitCode }) => {
|
|
680
852
|
process = void 0;
|
|
681
853
|
working = false;
|
|
682
854
|
stopping = false;
|
|
683
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
|
+
}
|
|
684
861
|
emit("state", snapshot());
|
|
685
862
|
done();
|
|
686
863
|
}));
|
|
@@ -694,6 +871,17 @@ Preview context captured with the latest terminal input: ${JSON.stringify(contex
|
|
|
694
871
|
})) throw new Error("Invalid inline prompt");
|
|
695
872
|
updateFocus();
|
|
696
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
|
+
}
|
|
697
885
|
process.write("\x1B[200~" + value.message + "\x1B[201~");
|
|
698
886
|
await new Promise((done) => setTimeout(done, 100));
|
|
699
887
|
if (!process || stopping) throw new Error("Claude Code stopped before submitting.");
|
|
@@ -702,8 +890,11 @@ Preview context captured with the latest terminal input: ${JSON.stringify(contex
|
|
|
702
890
|
if (!process) throw new Error("Start Claude Code first");
|
|
703
891
|
if (typeof value.data !== "string" || value.data.length > 32e3) throw new Error("Invalid terminal input");
|
|
704
892
|
updateFocus();
|
|
705
|
-
if (value.data.includes("")) working = false;
|
|
706
893
|
process.write(value.data);
|
|
894
|
+
if (value.data.includes("")) {
|
|
895
|
+
working = false;
|
|
896
|
+
await history?.end();
|
|
897
|
+
}
|
|
707
898
|
} else if (request.url === "/terminal/resize") {
|
|
708
899
|
size();
|
|
709
900
|
process?.resize(value.cols, value.rows);
|
|
@@ -737,7 +928,9 @@ const markdown = new MarkdownIt({
|
|
|
737
928
|
});
|
|
738
929
|
markdown.renderer.rules.image = (tokens, index) => markdown.utils.escapeHtml(tokens[index]?.content ?? "");
|
|
739
930
|
/** One shared conversation and one editing process per dev server. */
|
|
740
|
-
function createChat(entry, notify, busy = () => false, verify = async () => null, review, reviewImages = () => []) {
|
|
931
|
+
function createChat(entry, notify, busy = () => false, verify = async () => null, review, reviewImages = () => [], history) {
|
|
932
|
+
let starting = false;
|
|
933
|
+
let closing = false;
|
|
741
934
|
let messages = [];
|
|
742
935
|
let provider = "codex";
|
|
743
936
|
let child;
|
|
@@ -751,7 +944,7 @@ function createChat(entry, notify, busy = () => false, verify = async () => null
|
|
|
751
944
|
...message.role === "assistant" ? { html: markdown.render(message.text) } : {}
|
|
752
945
|
})),
|
|
753
946
|
provider,
|
|
754
|
-
running: !!child,
|
|
947
|
+
running: starting || !!child,
|
|
755
948
|
status
|
|
756
949
|
});
|
|
757
950
|
function stop() {
|
|
@@ -781,7 +974,7 @@ function createChat(entry, notify, busy = () => false, verify = async () => null
|
|
|
781
974
|
text,
|
|
782
975
|
context
|
|
783
976
|
};
|
|
784
|
-
const
|
|
977
|
+
const conversation = messages.slice(-12).map((message) => ({
|
|
785
978
|
...message,
|
|
786
979
|
text: message.text.slice(-12e3)
|
|
787
980
|
}));
|
|
@@ -792,7 +985,7 @@ ${authoringGuidance}
|
|
|
792
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.
|
|
793
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.
|
|
794
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.
|
|
795
|
-
Recent conversation (JSON): ${JSON.stringify(
|
|
988
|
+
Recent conversation (JSON): ${JSON.stringify(conversation)}
|
|
796
989
|
Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
797
990
|
messages.push(user);
|
|
798
991
|
const answer = {
|
|
@@ -904,6 +1097,11 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
904
1097
|
}
|
|
905
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";
|
|
906
1099
|
}
|
|
1100
|
+
try {
|
|
1101
|
+
await history?.end();
|
|
1102
|
+
} catch (cause) {
|
|
1103
|
+
status = "History: " + String(cause);
|
|
1104
|
+
}
|
|
907
1105
|
if (status !== "Done") append(status);
|
|
908
1106
|
child = void 0;
|
|
909
1107
|
notify();
|
|
@@ -945,7 +1143,7 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
945
1143
|
json(200, snapshot());
|
|
946
1144
|
return;
|
|
947
1145
|
}
|
|
948
|
-
if (child) {
|
|
1146
|
+
if (child || starting || closing) {
|
|
949
1147
|
json(409, { error: "An edit is already running" });
|
|
950
1148
|
return;
|
|
951
1149
|
}
|
|
@@ -969,7 +1167,17 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
969
1167
|
return;
|
|
970
1168
|
}
|
|
971
1169
|
const context = focus(value.slide, value.revision);
|
|
972
|
-
|
|
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
|
+
}
|
|
973
1181
|
json(202, snapshot());
|
|
974
1182
|
} catch (cause) {
|
|
975
1183
|
json(400, { error: cause instanceof Error ? cause.message : "Invalid request" });
|
|
@@ -977,8 +1185,9 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
977
1185
|
}
|
|
978
1186
|
return {
|
|
979
1187
|
handle,
|
|
980
|
-
isRunning: () => !!child,
|
|
1188
|
+
isRunning: () => starting || !!child,
|
|
981
1189
|
async close() {
|
|
1190
|
+
closing = true;
|
|
982
1191
|
stop();
|
|
983
1192
|
await completion;
|
|
984
1193
|
}
|
|
@@ -997,15 +1206,23 @@ async function serveDeck(entry, port = 4173) {
|
|
|
997
1206
|
let closed = false;
|
|
998
1207
|
const clients = /* @__PURE__ */ new Set();
|
|
999
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;
|
|
1000
1213
|
const capturedReviews = /* @__PURE__ */ new Map();
|
|
1001
1214
|
function session(id) {
|
|
1002
1215
|
let result = sessions.get(id);
|
|
1003
1216
|
if (!result) {
|
|
1004
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
|
+
};
|
|
1005
1222
|
const chat = createChat(entry, () => {
|
|
1006
1223
|
for (const client of clients) client.write("data: chat\n\n");
|
|
1007
|
-
}, () => terminal.isRunning(), verify, review, (prompt) => capturedReviews.get(prompt) ?? []);
|
|
1008
|
-
const terminal = createTerminal(entry, () => chat.isRunning(), id ? "/agents/" + id : "", verify, review);
|
|
1224
|
+
}, () => terminal.isRunning(), verify, review, (prompt) => capturedReviews.get(prompt) ?? [], turnHistory);
|
|
1225
|
+
const terminal = createTerminal(entry, () => chat.isRunning(), id ? "/agents/" + id : "", verify, review, turnHistory);
|
|
1009
1226
|
result = {
|
|
1010
1227
|
chat,
|
|
1011
1228
|
terminal,
|
|
@@ -1034,7 +1251,11 @@ async function serveDeck(entry, port = 4173) {
|
|
|
1034
1251
|
response.writeHead(404).end();
|
|
1035
1252
|
return;
|
|
1036
1253
|
}
|
|
1037
|
-
if (request.url === "/
|
|
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") {
|
|
1038
1259
|
if (request.method !== "POST" || request.headers.origin !== `http://${host}` || request.headers["content-type"] !== "application/json") {
|
|
1039
1260
|
response.writeHead(403).end();
|
|
1040
1261
|
return;
|
|
@@ -1046,10 +1267,24 @@ async function serveDeck(entry, port = 4173) {
|
|
|
1046
1267
|
body += chunk;
|
|
1047
1268
|
if (Buffer.byteLength(body) > 1e5) throw new Error("Request too large");
|
|
1048
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
|
+
}
|
|
1049
1279
|
const manualReview = createVisualReviewer(resolve(entry), () => latest?.slides ?? []);
|
|
1050
1280
|
manualReview.begin();
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
+
}
|
|
1053
1288
|
let review;
|
|
1054
1289
|
let reviewError;
|
|
1055
1290
|
try {
|
|
@@ -1200,6 +1435,7 @@ async function serveDeck(entry, port = 4173) {
|
|
|
1200
1435
|
if (started === generation) error = cause instanceof Error ? cause.stack ?? cause.message : String(cause);
|
|
1201
1436
|
} finally {
|
|
1202
1437
|
building = false;
|
|
1438
|
+
history.capture().catch(() => {});
|
|
1203
1439
|
for (const client of clients) client.write("data: updated\n\n");
|
|
1204
1440
|
if (pending && !timer) {
|
|
1205
1441
|
pending = false;
|
|
@@ -1215,7 +1451,7 @@ async function serveDeck(entry, port = 4173) {
|
|
|
1215
1451
|
"dist",
|
|
1216
1452
|
".office-kit"
|
|
1217
1453
|
].includes(part))) return;
|
|
1218
|
-
if (
|
|
1454
|
+
if (!historyFile(filename)) return;
|
|
1219
1455
|
generation++;
|
|
1220
1456
|
builder.cancel();
|
|
1221
1457
|
clearTimeout(timer);
|