@office-kit/pptx-dev 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -2
- package/dist/cli.mjs +275 -19
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +19 -2
- 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/README.md
CHANGED
|
@@ -16,11 +16,24 @@ npm install
|
|
|
16
16
|
npm run dev
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
For a single-command first launch (macOS/Linux):
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npx --yes @office-kit/pptx-dev@latest init my-slides && cd my-slides && npm install && npm run dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The built-in `init` is the starter generator: no separate `degit` checkout is
|
|
26
|
+
needed, and it selects compatible package versions. For an existing project,
|
|
27
|
+
start development with `npm run dev` or `npx office-pptx dev deck.tsx`.
|
|
28
|
+
If you prefer the bare `office-pptx dev deck.tsx` command, install the CLI once
|
|
29
|
+
with `npm install --global @office-kit/pptx-dev@latest`. The local npm script
|
|
30
|
+
continues to use the project's installed version.
|
|
31
|
+
|
|
19
32
|
Open the local URL printed by the server. Save a slide file, `theme.ts` or `deck.tsx` to rebuild. The viewer
|
|
20
33
|
has a vertical thumbnail strip, a large slide canvas and an AI chat panel on the right. Click a thumbnail or use
|
|
21
34
|
arrow keys, Page Up/Down, Home/End to navigate. Fit/zoom and Present (Escape to
|
|
22
|
-
exit) are viewing controls
|
|
23
|
-
|
|
35
|
+
exit) are viewing controls. Select an area for an AI instruction or use **Edit text**
|
|
36
|
+
to save a literal directly. All edits are persisted in TSX.
|
|
24
37
|
Keep the server running throughout the edit/review loop. Saving updates only the
|
|
25
38
|
changed thumbnails and slide view, preserving zoom, scroll position and presentation
|
|
26
39
|
mode. The previous slide stays visible until its replacement is ready. Rapid edits
|
|
@@ -140,6 +153,40 @@ References: [Claude Code commands](https://code.claude.com/docs/en/commands),
|
|
|
140
153
|
[Claude Code hooks](https://code.claude.com/docs/en/hooks), and
|
|
141
154
|
[Codex non-interactive mode](https://developers.openai.com/codex/noninteractive).
|
|
142
155
|
|
|
156
|
+
## Edit from the slide
|
|
157
|
+
|
|
158
|
+
Use **Select area** and drag a rectangle, then enter an instruction such as
|
|
159
|
+
“Move this down a little.” Choose an agent pane and click **Apply with AI**.
|
|
160
|
+
The request includes the slide, preview revision, relative bounds and intersecting
|
|
161
|
+
text. Start Claude Code in that pane first; Codex starts when you send.
|
|
162
|
+
Selection is cleared when the preview or selected slide changes. Shift+Enter
|
|
163
|
+
inserts a newline; Cmd/Ctrl+Enter sends the request.
|
|
164
|
+
|
|
165
|
+
Use **Edit text**, click a paragraph, and **Save text** to update its TSX source
|
|
166
|
+
without waiting for an agent. Direct saves require a unique source literal within
|
|
167
|
+
the entry directory and verify that other slides did not change. Computed text,
|
|
168
|
+
shared values and ambiguous matches use **Apply with AI** instead. **Undo text**
|
|
169
|
+
restores the last direct edit only if its file has not changed since. **Undo with
|
|
170
|
+
AI** asks the agent to reverse its previous selection edit while preserving newer
|
|
171
|
+
work; it is not a source snapshot restore.
|
|
172
|
+
|
|
173
|
+
After each successful agent edit or direct text save/undo, the dev server captures
|
|
174
|
+
all changed slides, including slides offscreen. Codex receives PNG attachments;
|
|
175
|
+
Claude Code receives local PNG paths through its Stop hook and is instructed to
|
|
176
|
+
read them. The agent checks overlap, clipping, alignment, spacing and contrast,
|
|
177
|
+
and can make up to two correction passes followed by a final inspection. This is
|
|
178
|
+
AI-assisted review, not a guarantee of pixel-perfect rendering. The preview updates
|
|
179
|
+
as soon as the build finishes, independently of screenshot review.
|
|
180
|
+
|
|
181
|
+
Screenshot capture uses an installed Chrome, or falls back to Playwright Chromium.
|
|
182
|
+
Set `PLAYWRIGHT_CHANNEL` to use another installed Chromium channel. If neither is
|
|
183
|
+
available, install Chrome and restart the dev server. Capture failures are shown
|
|
184
|
+
explicitly; saved edits remain available. If the chosen agent is busy or not started,
|
|
185
|
+
use **Retry visual review** after making it available. Screenshots and review prompts
|
|
186
|
+
are stored locally under `.office-kit/reviews/`; new projects ignore this directory.
|
|
187
|
+
Add `.office-kit/` to `.gitignore` in existing projects. Images are provided to the
|
|
188
|
+
selected AI agent using its existing login and provider settings.
|
|
189
|
+
|
|
143
190
|
## Edit only what changed
|
|
144
191
|
|
|
145
192
|
New projects separate slide order, content and shared styling:
|
package/dist/cli.mjs
CHANGED
|
@@ -1,13 +1,167 @@
|
|
|
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, mkdir, readFile, realpath, stat, 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/text-edit.ts
|
|
14
|
+
/** Literal edits are deliberately conservative: computed and ambiguous text goes to the agent. */
|
|
15
|
+
function createTextEditor(entry, preview, verify) {
|
|
16
|
+
let busy = false;
|
|
17
|
+
let undo;
|
|
18
|
+
async function restore(change) {
|
|
19
|
+
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
|
+
await writeFile(change.file, change.before);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
async edit(value) {
|
|
24
|
+
if (busy) throw new Error("A text edit is already running.");
|
|
25
|
+
busy = true;
|
|
26
|
+
try {
|
|
27
|
+
const current = preview();
|
|
28
|
+
if (value.revision !== current.revision) throw new Error("Preview changed. Select the text again.");
|
|
29
|
+
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.");
|
|
30
|
+
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.");
|
|
31
|
+
const root = await realpath(dirname(entry));
|
|
32
|
+
const files = await Promise.all([...new Set(current.dependencies)].filter((file) => /\.[cm]?[jt]sx?$/.test(file)).map(async (file) => {
|
|
33
|
+
const path = await realpath(file), local = relative(root, path);
|
|
34
|
+
if (local.startsWith("..") || isAbsolute(local) || local.split(/[\\/]/).includes("node_modules")) return null;
|
|
35
|
+
return {
|
|
36
|
+
file: path,
|
|
37
|
+
before: await readFile(path, "utf8")
|
|
38
|
+
};
|
|
39
|
+
}));
|
|
40
|
+
const candidates = [];
|
|
41
|
+
for (const item of files) {
|
|
42
|
+
if (!item) continue;
|
|
43
|
+
const source = ts.createSourceFile(item.file, item.before, ts.ScriptTarget.Latest, true, /[jt]sx$/.test(extname(item.file)) ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
44
|
+
const visit = (node) => {
|
|
45
|
+
if ((ts.isJsxText(node) ? node.text.split(/\r\n|\n|\r/).map((line, index, lines) => {
|
|
46
|
+
let value = line.replaceAll(" ", " ");
|
|
47
|
+
if (index) value = value.trimStart();
|
|
48
|
+
if (index < lines.length - 1) value = value.trimEnd();
|
|
49
|
+
return value;
|
|
50
|
+
}).filter(Boolean).join(" ") : ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : void 0) === value.before) {
|
|
51
|
+
const replacement = ts.isJsxText(node) || ts.isJsxAttribute(node.parent) ? "{" + JSON.stringify(value.after) + "}" : JSON.stringify(value.after);
|
|
52
|
+
const start = ts.isJsxText(node) ? node.pos : node.getStart(source);
|
|
53
|
+
candidates.push({
|
|
54
|
+
...item,
|
|
55
|
+
after: item.before.slice(0, start) + replacement + item.before.slice(node.end)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
ts.forEachChild(node, visit);
|
|
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;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
async undo() {
|
|
82
|
+
if (busy) throw new Error("A text edit is already running.");
|
|
83
|
+
if (!undo) throw new Error("No direct text edit to undo.");
|
|
84
|
+
busy = true;
|
|
85
|
+
try {
|
|
86
|
+
await restore(undo);
|
|
87
|
+
undo = void 0;
|
|
88
|
+
if (await verify()) throw new Error("Text restored, but the project has build errors.");
|
|
89
|
+
} finally {
|
|
90
|
+
busy = false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/visual-review.ts
|
|
97
|
+
/** Each session compares against its own turn's preview, including offscreen slides. */
|
|
98
|
+
function createVisualReviewer(entry, slides) {
|
|
99
|
+
let before = [];
|
|
100
|
+
let rounds = 0;
|
|
101
|
+
return {
|
|
102
|
+
begin() {
|
|
103
|
+
before = slides().slice();
|
|
104
|
+
rounds = 0;
|
|
105
|
+
},
|
|
106
|
+
async next() {
|
|
107
|
+
const current = slides().slice();
|
|
108
|
+
const changed = current.flatMap((svg, index) => svg !== before[index] ? [{
|
|
109
|
+
svg,
|
|
110
|
+
index
|
|
111
|
+
}] : []);
|
|
112
|
+
if (!changed.length) return;
|
|
113
|
+
if (rounds >= 3) throw new Error("Visual review reached its correction limit. Review the latest slide manually before continuing.");
|
|
114
|
+
const round = rounds + 1;
|
|
115
|
+
let browser;
|
|
116
|
+
try {
|
|
117
|
+
browser = await chromium.launch({
|
|
118
|
+
channel: process.env.PLAYWRIGHT_CHANNEL || "chrome",
|
|
119
|
+
headless: true
|
|
120
|
+
});
|
|
121
|
+
} catch {
|
|
122
|
+
try {
|
|
123
|
+
browser = await chromium.launch({ headless: true });
|
|
124
|
+
} catch {
|
|
125
|
+
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.");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const directory = join(dirname(entry), ".office-kit", "reviews", randomUUID());
|
|
130
|
+
await mkdir(directory, { recursive: true });
|
|
131
|
+
const page = await browser.newPage({
|
|
132
|
+
viewport: {
|
|
133
|
+
width: 1600,
|
|
134
|
+
height: 1e3
|
|
135
|
+
},
|
|
136
|
+
deviceScaleFactor: 1
|
|
137
|
+
});
|
|
138
|
+
await page.route("**/*", (route) => route.abort());
|
|
139
|
+
const images = [];
|
|
140
|
+
for (const { svg, index } of changed) {
|
|
141
|
+
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);
|
|
142
|
+
await page.evaluate(() => document.fonts.ready.then(() => void 0));
|
|
143
|
+
const file = join(directory, `slide-${index + 1}.png`);
|
|
144
|
+
await page.locator("svg").first().screenshot({
|
|
145
|
+
path: file,
|
|
146
|
+
timeout: 15e3
|
|
147
|
+
});
|
|
148
|
+
images.push(file);
|
|
149
|
+
}
|
|
150
|
+
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)}`;
|
|
151
|
+
await writeFile(join(directory, "review.txt"), prompt);
|
|
152
|
+
before = current;
|
|
153
|
+
rounds = round;
|
|
154
|
+
return {
|
|
155
|
+
prompt,
|
|
156
|
+
images
|
|
157
|
+
};
|
|
158
|
+
} finally {
|
|
159
|
+
await browser.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
11
165
|
//#region src/styles.ts
|
|
12
166
|
const previewStyles = `*{box-sizing:border-box}
|
|
13
167
|
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 +231,8 @@ const page = `<!doctype html>
|
|
|
77
231
|
<link rel="stylesheet" href="/terminal.css">
|
|
78
232
|
<style>
|
|
79
233
|
${previewStyles}
|
|
234
|
+
.slide-edit-tools{display:flex;gap:6px;padding:8px 14px;flex-shrink:0}.slide-edit-tools button[aria-pressed=true]{background:#6850b8;border-color:#b8a1ff}.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
|
+
|
|
80
236
|
#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
237
|
</style>
|
|
82
238
|
<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 +243,7 @@ ${previewStyles}
|
|
|
87
243
|
<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
244
|
<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
245
|
</div>
|
|
90
|
-
<footer><span id="count" aria-live="polite">No slides</span><span class="hint">
|
|
246
|
+
<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
247
|
<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
248
|
<script>
|
|
93
249
|
let state={slides:[],error:null,aspectRatio:16/9},index=0,urls=[],presenting=false;
|
|
@@ -257,6 +413,10 @@ window.addEventListener('message',event=>{
|
|
|
257
413
|
if(event.origin!==location.origin||event.source!==parent)return;
|
|
258
414
|
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
415
|
if(event.data?.type==='chat')void refreshChat();
|
|
416
|
+
if(event.data?.type==='inline-edit'&&byId('chat-provider').value==='codex'){
|
|
417
|
+
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);}})();
|
|
418
|
+
}
|
|
419
|
+
|
|
260
420
|
});
|
|
261
421
|
parent.postMessage({type:'agent-ready'},location.origin);
|
|
262
422
|
let chatRequest=0,chatBusy=false,lastMessages='';
|
|
@@ -316,8 +476,9 @@ Choose foreground and actual background colors together. Explicitly set readable
|
|
|
316
476
|
//#region src/terminal.ts
|
|
317
477
|
const REQUEST_LIMIT = 128e3;
|
|
318
478
|
/** A real interactive Claude session, shared across reloads of its owning tab. */
|
|
319
|
-
function createTerminal(entry, busy = () => false, base = "", verify = async () => null) {
|
|
479
|
+
function createTerminal(entry, busy = () => false, base = "", verify = async () => null, review) {
|
|
320
480
|
let repairs = 0;
|
|
481
|
+
let working = false;
|
|
321
482
|
let process;
|
|
322
483
|
let owner = "";
|
|
323
484
|
let output = "";
|
|
@@ -387,13 +548,34 @@ function createTerminal(entry, busy = () => false, base = "", verify = async ()
|
|
|
387
548
|
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
549
|
});
|
|
389
550
|
} else {
|
|
390
|
-
|
|
551
|
+
if (!error && review) try {
|
|
552
|
+
const visual = await review.next();
|
|
553
|
+
if (closing || stopping || !process) {
|
|
554
|
+
json(200, {});
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (visual) {
|
|
558
|
+
status = "Reviewing slide screenshots…";
|
|
559
|
+
json(200, {
|
|
560
|
+
decision: "block",
|
|
561
|
+
reason: visual.prompt
|
|
562
|
+
});
|
|
563
|
+
emit("state", snapshot());
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
} catch (cause) {
|
|
567
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
568
|
+
}
|
|
569
|
+
working = false;
|
|
570
|
+
status = error ? "Preview verification incomplete: " + error : "Preview build verified";
|
|
391
571
|
json(200, {});
|
|
392
572
|
}
|
|
393
573
|
emit("state", snapshot());
|
|
394
574
|
return;
|
|
395
575
|
}
|
|
396
576
|
repairs = 0;
|
|
577
|
+
working = true;
|
|
578
|
+
review?.begin();
|
|
397
579
|
json(200, { hookSpecificOutput: {
|
|
398
580
|
hookEventName: "UserPromptSubmit",
|
|
399
581
|
additionalContext: `You are editing the local PowerPoint TSX project at ${resolve(entry)}.
|
|
@@ -465,7 +647,7 @@ Preview context captured with the latest terminal input: ${JSON.stringify(contex
|
|
|
465
647
|
type: "http",
|
|
466
648
|
url: `${origin}${base}/terminal/verify`,
|
|
467
649
|
headers: { Authorization: `Bearer ${token}` },
|
|
468
|
-
timeout:
|
|
650
|
+
timeout: 120
|
|
469
651
|
}] }],
|
|
470
652
|
UserPromptSubmit: [{ hooks: [{
|
|
471
653
|
type: "http",
|
|
@@ -496,16 +678,31 @@ Preview context captured with the latest terminal input: ${JSON.stringify(contex
|
|
|
496
678
|
});
|
|
497
679
|
completion = new Promise((done) => process.onExit(({ exitCode }) => {
|
|
498
680
|
process = void 0;
|
|
681
|
+
working = false;
|
|
499
682
|
stopping = false;
|
|
500
683
|
status = `Claude Code exited (${exitCode}). Start to open a new session.`;
|
|
501
684
|
emit("state", snapshot());
|
|
502
685
|
done();
|
|
503
686
|
}));
|
|
504
687
|
}
|
|
688
|
+
} else if (request.url === "/terminal/prompt") {
|
|
689
|
+
if (!process) throw new Error("Start Claude Code in the selected agent pane first.");
|
|
690
|
+
if (working) throw new Error("Claude Code is working. Wait for the current turn to finish.");
|
|
691
|
+
if (typeof value.message !== "string" || !value.message.trim() || value.message.length > 16e3 || [...value.message].some((character) => {
|
|
692
|
+
const code = character.charCodeAt(0);
|
|
693
|
+
return code < 32 && code !== 9 && code !== 10 || code === 127;
|
|
694
|
+
})) throw new Error("Invalid inline prompt");
|
|
695
|
+
updateFocus();
|
|
696
|
+
working = true;
|
|
697
|
+
process.write("\x1B[200~" + value.message + "\x1B[201~");
|
|
698
|
+
await new Promise((done) => setTimeout(done, 100));
|
|
699
|
+
if (!process || stopping) throw new Error("Claude Code stopped before submitting.");
|
|
700
|
+
process.write("\r");
|
|
505
701
|
} else if (request.url === "/terminal/input") {
|
|
506
702
|
if (!process) throw new Error("Start Claude Code first");
|
|
507
703
|
if (typeof value.data !== "string" || value.data.length > 32e3) throw new Error("Invalid terminal input");
|
|
508
704
|
updateFocus();
|
|
705
|
+
if (value.data.includes("")) working = false;
|
|
509
706
|
process.write(value.data);
|
|
510
707
|
} else if (request.url === "/terminal/resize") {
|
|
511
708
|
size();
|
|
@@ -540,7 +737,7 @@ const markdown = new MarkdownIt({
|
|
|
540
737
|
});
|
|
541
738
|
markdown.renderer.rules.image = (tokens, index) => markdown.utils.escapeHtml(tokens[index]?.content ?? "");
|
|
542
739
|
/** One shared conversation and one editing process per dev server. */
|
|
543
|
-
function createChat(entry, notify, busy = () => false, verify = async () => null) {
|
|
740
|
+
function createChat(entry, notify, busy = () => false, verify = async () => null, review, reviewImages = () => []) {
|
|
544
741
|
let messages = [];
|
|
545
742
|
let provider = "codex";
|
|
546
743
|
let child;
|
|
@@ -576,7 +773,8 @@ function createChat(entry, notify, busy = () => false, verify = async () => null
|
|
|
576
773
|
killTimer = setTimeout(() => kill("SIGKILL"), 2e3);
|
|
577
774
|
notify();
|
|
578
775
|
}
|
|
579
|
-
function start(text, selected, context, repairs = 0) {
|
|
776
|
+
function start(text, selected, context, repairs = 0, images = [], reviewing = false) {
|
|
777
|
+
if (!repairs && !reviewing) review?.begin();
|
|
580
778
|
provider = selected;
|
|
581
779
|
const user = {
|
|
582
780
|
role: "user",
|
|
@@ -605,14 +803,16 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
605
803
|
if (messages.length > 100) messages = messages.slice(-100);
|
|
606
804
|
status = "Working…";
|
|
607
805
|
stopping = false;
|
|
608
|
-
|
|
806
|
+
const args = [
|
|
609
807
|
"exec",
|
|
610
808
|
"--json",
|
|
611
809
|
"--sandbox",
|
|
612
810
|
"workspace-write",
|
|
613
811
|
"--skip-git-repo-check",
|
|
614
812
|
"-"
|
|
615
|
-
]
|
|
813
|
+
];
|
|
814
|
+
for (const image of images) args.splice(args.length - 1, 0, "--image", image);
|
|
815
|
+
child = spawn(selected, args, {
|
|
616
816
|
cwd: dirname(resolve(entry)),
|
|
617
817
|
stdio: [
|
|
618
818
|
"pipe",
|
|
@@ -684,12 +884,25 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
684
884
|
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
885
|
...context,
|
|
686
886
|
buildError
|
|
687
|
-
}, repairs + 1);
|
|
887
|
+
}, repairs + 1, [], reviewing);
|
|
688
888
|
await completion;
|
|
689
889
|
done();
|
|
690
890
|
return;
|
|
691
891
|
}
|
|
692
|
-
|
|
892
|
+
let reviewFailure = "";
|
|
893
|
+
if (!stopping && !buildError && review) try {
|
|
894
|
+
const visual = await review.next();
|
|
895
|
+
if (!stopping && visual) {
|
|
896
|
+
child = void 0;
|
|
897
|
+
start(visual.prompt, selected, context, 0, visual.images, true);
|
|
898
|
+
await completion;
|
|
899
|
+
done();
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
} catch (error) {
|
|
903
|
+
reviewFailure = error instanceof Error ? error.message : String(error);
|
|
904
|
+
}
|
|
905
|
+
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";
|
|
693
906
|
}
|
|
694
907
|
if (status !== "Done") append(status);
|
|
695
908
|
child = void 0;
|
|
@@ -756,7 +969,7 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
756
969
|
return;
|
|
757
970
|
}
|
|
758
971
|
const context = focus(value.slide, value.revision);
|
|
759
|
-
start(value.message.trim(), value.provider, context);
|
|
972
|
+
start(value.message.trim(), value.provider, context, 0, reviewImages(value.message.trim()));
|
|
760
973
|
json(202, snapshot());
|
|
761
974
|
} catch (cause) {
|
|
762
975
|
json(400, { error: cause instanceof Error ? cause.message : "Invalid request" });
|
|
@@ -784,13 +997,15 @@ async function serveDeck(entry, port = 4173) {
|
|
|
784
997
|
let closed = false;
|
|
785
998
|
const clients = /* @__PURE__ */ new Set();
|
|
786
999
|
const sessions = /* @__PURE__ */ new Map();
|
|
1000
|
+
const capturedReviews = /* @__PURE__ */ new Map();
|
|
787
1001
|
function session(id) {
|
|
788
1002
|
let result = sessions.get(id);
|
|
789
1003
|
if (!result) {
|
|
1004
|
+
const review = createVisualReviewer(resolve(entry), () => latest?.slides ?? []);
|
|
790
1005
|
const chat = createChat(entry, () => {
|
|
791
1006
|
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);
|
|
1007
|
+
}, () => terminal.isRunning(), verify, review, (prompt) => capturedReviews.get(prompt) ?? []);
|
|
1008
|
+
const terminal = createTerminal(entry, () => chat.isRunning(), id ? "/agents/" + id : "", verify, review);
|
|
794
1009
|
result = {
|
|
795
1010
|
chat,
|
|
796
1011
|
terminal,
|
|
@@ -800,6 +1015,13 @@ async function serveDeck(entry, port = 4173) {
|
|
|
800
1015
|
}
|
|
801
1016
|
return result;
|
|
802
1017
|
}
|
|
1018
|
+
const textEditor = createTextEditor(resolve(entry), () => {
|
|
1019
|
+
if (!latest || building || pending || timer || error) throw new Error("Wait for a successful preview build.");
|
|
1020
|
+
return {
|
|
1021
|
+
...latest,
|
|
1022
|
+
revision
|
|
1023
|
+
};
|
|
1024
|
+
}, verify);
|
|
803
1025
|
const server = createServer((request, response) => {
|
|
804
1026
|
const host = request.headers.host;
|
|
805
1027
|
if (host !== `127.0.0.1:${actualPort}` && host !== `localhost:${actualPort}`) {
|
|
@@ -807,11 +1029,45 @@ async function serveDeck(entry, port = 4173) {
|
|
|
807
1029
|
return;
|
|
808
1030
|
}
|
|
809
1031
|
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))?$/);
|
|
1032
|
+
const match = request.url?.match(/^\/agents\/([\w-]{1,64})(\/(?:chat(?:\/(?:stop|reset))?|terminal\/(?:start|input|prompt|resize|stop|events|context|verify)|close))?$/);
|
|
811
1033
|
if (request.url?.startsWith("/agents/") && !match) {
|
|
812
1034
|
response.writeHead(404).end();
|
|
813
1035
|
return;
|
|
814
1036
|
}
|
|
1037
|
+
if (request.url === "/text-edit" || request.url === "/text-edit/undo") {
|
|
1038
|
+
if (request.method !== "POST" || request.headers.origin !== `http://${host}` || request.headers["content-type"] !== "application/json") {
|
|
1039
|
+
response.writeHead(403).end();
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
(async () => {
|
|
1043
|
+
let body = "";
|
|
1044
|
+
request.setEncoding("utf8");
|
|
1045
|
+
for await (const chunk of request) {
|
|
1046
|
+
body += chunk;
|
|
1047
|
+
if (Buffer.byteLength(body) > 1e5) throw new Error("Request too large");
|
|
1048
|
+
}
|
|
1049
|
+
const manualReview = createVisualReviewer(resolve(entry), () => latest?.slides ?? []);
|
|
1050
|
+
manualReview.begin();
|
|
1051
|
+
if (request.url === "/text-edit/undo") await textEditor.undo();
|
|
1052
|
+
else await textEditor.edit(JSON.parse(body));
|
|
1053
|
+
let review;
|
|
1054
|
+
let reviewError;
|
|
1055
|
+
try {
|
|
1056
|
+
review = await manualReview.next();
|
|
1057
|
+
if (review) {
|
|
1058
|
+
capturedReviews.set(review.prompt, review.images);
|
|
1059
|
+
if (capturedReviews.size > 64) capturedReviews.delete(capturedReviews.keys().next().value);
|
|
1060
|
+
}
|
|
1061
|
+
} catch (cause) {
|
|
1062
|
+
reviewError = cause instanceof Error ? cause.message : String(cause);
|
|
1063
|
+
}
|
|
1064
|
+
response.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
|
|
1065
|
+
review,
|
|
1066
|
+
reviewError
|
|
1067
|
+
}));
|
|
1068
|
+
})().catch((cause) => response.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({ error: cause instanceof Error ? cause.message : String(cause) })));
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
815
1071
|
const id = match?.[1] ?? "";
|
|
816
1072
|
if (match) {
|
|
817
1073
|
if (request.headers["sec-fetch-site"] === "cross-site" || request.headers.origin && request.headers.origin !== `http://${host}`) {
|
|
@@ -1015,7 +1271,7 @@ try {
|
|
|
1015
1271
|
if (args.length && (args.length !== 2 || !option || args[0] !== option || !args[1])) throw new Error(`Unexpected arguments: ${args.join(" ")}\n${usage}`);
|
|
1016
1272
|
if (command === "init") {
|
|
1017
1273
|
const directory = await initProject(entry);
|
|
1018
|
-
console.log(`Created ${directory}\
|
|
1274
|
+
console.log(`Created ${directory}\nInside that directory, run npm install once.\nStart development: npm run dev (or npx office-pptx dev deck.tsx).`);
|
|
1019
1275
|
} else if (command === "inspect") console.log(JSON.stringify(await inspectTemplate(entry), null, 2));
|
|
1020
1276
|
else if (command === "build") {
|
|
1021
1277
|
const output = resolve(args[1] ?? "deck.pptx");
|