slides-grab 1.1.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slides-grab",
3
- "version": "1.1.6",
3
+ "version": "1.2.0",
4
4
  "description": "Agent-first presentation framework — plan, design, and visually edit HTML slides with Claude Code or Codex, then export to PDF or experimental/unstable PPTX/Figma formats",
5
5
  "license": "MIT",
6
6
  "author": "vkehfdl1",
@@ -3,7 +3,6 @@
3
3
  import { readdir, readFile, writeFile, mkdtemp, rm, mkdir } from 'node:fs/promises';
4
4
  import { watch as fsWatch } from 'node:fs';
5
5
  import { basename, dirname, join, resolve, relative, sep } from 'node:path';
6
- import { spawn } from 'node:child_process';
7
6
  import { fileURLToPath } from 'node:url';
8
7
  import { tmpdir } from 'node:os';
9
8
 
@@ -18,6 +17,10 @@ import {
18
17
  scaleSelectionToScreenshot,
19
18
  writeAnnotatedScreenshot,
20
19
  } from '../src/editor/codex-edit.js';
20
+ import {
21
+ parseEditTimeoutMs,
22
+ runEditSubprocess,
23
+ } from '../src/editor/edit-subprocess.js';
21
24
  import { buildSlideRuntimeHtml } from '../src/image-contract.js';
22
25
 
23
26
  const __filename = fileURLToPath(import.meta.url);
@@ -45,6 +48,7 @@ const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
45
48
 
46
49
  const MAX_RUNS = 200;
47
50
  const MAX_LOG_CHARS = 800_000;
51
+ const EDIT_TIMEOUT_MS = parseEditTimeoutMs();
48
52
 
49
53
  function printUsage() {
50
54
  process.stdout.write(`Usage: slides-grab edit [options]\n\n`);
@@ -233,37 +237,24 @@ function randomRunId() {
233
237
  return `run-${ts}-${rand}`;
234
238
  }
235
239
 
240
+ function mirrorRunLog(onLog) {
241
+ return (stream, chunk) => {
242
+ onLog(stream, chunk);
243
+ process[stream].write(chunk);
244
+ };
245
+ }
246
+
236
247
  function spawnCodexEdit({ prompt, imagePath, model, cwd, onLog }) {
237
248
  const codexBin = process.env.PPT_AGENT_CODEX_BIN || 'codex';
238
249
  const args = buildCodexExecArgs({ prompt, imagePath, model });
239
-
240
- return new Promise((resolvePromise, rejectPromise) => {
241
- const child = spawn(codexBin, args, { cwd, stdio: 'pipe' });
242
-
243
- let stdout = '';
244
- let stderr = '';
245
-
246
- child.stdout.on('data', (chunk) => {
247
- const text = chunk.toString();
248
- stdout += text;
249
- onLog('stdout', text);
250
- process.stdout.write(text);
251
- });
252
-
253
- child.stderr.on('data', (chunk) => {
254
- const text = chunk.toString();
255
- stderr += text;
256
- onLog('stderr', text);
257
- process.stderr.write(text);
258
- });
259
-
260
- child.on('close', (code) => {
261
- resolvePromise({ code: code ?? 1, stdout, stderr });
262
- });
263
-
264
- child.on('error', (error) => {
265
- rejectPromise(error);
266
- });
250
+ return runEditSubprocess({
251
+ bin: codexBin,
252
+ args,
253
+ cwd,
254
+ stdio: 'pipe',
255
+ timeoutMs: EDIT_TIMEOUT_MS,
256
+ engineLabel: 'Codex',
257
+ onLog: mirrorRunLog(onLog),
267
258
  });
268
259
  }
269
260
 
@@ -275,37 +266,15 @@ function spawnClaudeEdit({ prompt, imagePath, model, cwd, onLog }) {
275
266
  const env = { ...process.env };
276
267
  delete env.CLAUDECODE;
277
268
 
278
- return new Promise((resolvePromise, rejectPromise) => {
279
- const child = spawn(claudeBin, args, {
280
- cwd,
281
- stdio: ['ignore', 'pipe', 'pipe'],
282
- env,
283
- });
284
-
285
- let stdout = '';
286
- let stderr = '';
287
-
288
- child.stdout.on('data', (chunk) => {
289
- const text = chunk.toString();
290
- stdout += text;
291
- onLog('stdout', text);
292
- process.stdout.write(text);
293
- });
294
-
295
- child.stderr.on('data', (chunk) => {
296
- const text = chunk.toString();
297
- stderr += text;
298
- onLog('stderr', text);
299
- process.stderr.write(text);
300
- });
301
-
302
- child.on('close', (code) => {
303
- resolvePromise({ code: code ?? 1, stdout, stderr });
304
- });
305
-
306
- child.on('error', (error) => {
307
- rejectPromise(error);
308
- });
269
+ return runEditSubprocess({
270
+ bin: claudeBin,
271
+ args,
272
+ cwd,
273
+ env,
274
+ stdio: ['ignore', 'pipe', 'pipe'],
275
+ timeoutMs: EDIT_TIMEOUT_MS,
276
+ engineLabel: 'Claude',
277
+ onLog: mirrorRunLog(onLog),
309
278
  });
310
279
  }
311
280
 
@@ -674,7 +643,7 @@ async function startServer(opts) {
674
643
  const success = result.code === 0;
675
644
  const message = success
676
645
  ? `${engineLabel} edit completed.`
677
- : `${engineLabel} exited with code ${result.code}.`;
646
+ : (result.timeoutMessage || `${engineLabel} exited with code ${result.code}.`);
678
647
 
679
648
  runStore.finishRun(runId, {
680
649
  status: success ? 'success' : 'failed',
@@ -23,21 +23,27 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
23
23
 
24
24
  ## Workflow
25
25
  1. Read approved `slide-outline.md`.
26
- 2. Generate slide HTML files with 2-digit numbering in selected `--slides-dir`.
27
- 3. If the deck needs a complex diagram (architecture, workflows, relationship maps, multi-node concepts), create the diagram in `tldraw`, export it with `slides-grab tldraw`, and treat the result as a local slide asset under `<slides-dir>/assets/`.
28
- 4. Run `slides-grab validate --slides-dir <path>` after generation or edits.
29
- 5. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.
30
- 6. Run `slides-grab build-viewer --slides-dir <path>` only after validation passes.
31
- 7. Iterate on user feedback by editing only requested slide files, then re-run validation and rebuild the viewer.
32
- 8. Keep revising until user approves conversion stage.
26
+ 2. Before generating slides, write a quick **visual thesis** (mood/material/energy), a **content plan** (opener → support/proof → detail/story → close/CTA), and the core design tokens (background, surface, text, muted, accent + display/headline/body/caption roles).
27
+ 3. Generate slide HTML files with 2-digit numbering in selected `--slides-dir`.
28
+ 4. If the deck needs a complex diagram (architecture, workflows, relationship maps, multi-node concepts), create the diagram in `tldraw`, export it with `slides-grab tldraw`, and treat the result as a local slide asset under `<slides-dir>/assets/`.
29
+ 5. Run `slides-grab validate --slides-dir <path>` after generation or edits.
30
+ 6. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.
31
+ 7. Run `slides-grab build-viewer --slides-dir <path>` only after validation passes.
32
+ 8. Run the slide litmus check from `references/beautiful-slide-defaults.md` before presenting the deck for review.
33
+ 9. Iterate on user feedback by editing only requested slide files, then re-run validation and rebuild the viewer.
34
+ 10. Keep revising until user approves conversion stage.
33
35
 
34
36
  ## Rules
35
37
  - Keep slide size 720pt x 405pt.
36
38
  - Keep semantic text tags (`p`, `h1-h6`, `ul`, `ol`, `li`).
37
39
  - Put local images under `<slides-dir>/assets/` and reference them as `./assets/<file>`.
38
40
  - Allow `data:` URLs when the slide must be fully self-contained.
39
- - Treat remote `https://` images as best-effort only, and never use absolute filesystem paths.
41
+ - Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.
40
42
  - Prefer `<img>` for slide imagery and `data-image-placeholder` when no final asset exists.
43
+ - Default to one job per slide, one dominant visual anchor, and copy that scans in seconds.
44
+ - Treat opening slides and section dividers like posters, not dashboards.
45
+ - Default to cardless layouts; only add a card when it improves structure or comprehension.
46
+ - Use whitespace, alignment, scale, cropping, and contrast before adding decorative chrome.
41
47
  - Prefer `tldraw` for complex diagrams instead of recreating dense node/edge diagrams directly in HTML/CSS.
42
48
  - Use `slides-grab tldraw` plus `templates/diagram-tldraw.html` when that gives a cleaner, more export-friendly result.
43
49
  - Do not present slides for review until `slides-grab validate --slides-dir <path>` passes.
@@ -48,4 +54,5 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
48
54
  For full constraints and style system, follow:
49
55
  - `references/design-rules.md`
50
56
  - `references/detailed-design-rules.md`
57
+ - `references/beautiful-slide-defaults.md` — slide-specific art direction defaults adapted from OpenAI's frontend design guidance
51
58
  - `references/design-system-full.md` — archived full design system, templates, and advanced pattern guidance
@@ -0,0 +1,45 @@
1
+ # Beautiful Slide Defaults
2
+
3
+ Slide-specific art direction guidance adapted from OpenAI's frontend design guidance for GPT-5.4. Use it to make HTML slides feel deliberate, premium, and instantly scannable without breaking `slides-grab`'s export constraints.
4
+
5
+ ## Working Model
6
+
7
+ Before building the deck, write two things:
8
+
9
+ - **visual thesis** — one sentence describing the mood, material, energy, and imagery treatment
10
+ - **content plan** — opener → support/proof → detail/story → close/CTA or decision
11
+
12
+ If the style direction is still open, gather visual references or a mood board first. Define the core tokens early: `background`, `surface`, `primary text`, `muted text`, `accent`, plus typography roles for `display`, `headline`, `body`, and `caption`.
13
+
14
+ ## Beautiful Defaults for Slides
15
+
16
+ - Start with composition, not components.
17
+ - Treat the opening slide like a poster and make the title or brand the loudest text.
18
+ - Give each slide one job, one primary takeaway, and one dominant visual anchor.
19
+ - Keep copy short enough to scan in seconds.
20
+ - Use whitespace, alignment, scale, cropping, and contrast before adding chrome.
21
+ - Limit the system by default: two typefaces max and one accent color.
22
+ - Default to cardless layouts. Prefer sections, grids, media blocks, dividers, and strong negative space.
23
+ - Use real imagery, product views, diagrams, or data as the main visual idea. Decorative gradients and abstract filler do not count.
24
+ - Keep the first slide free of secondary clutter such as stat strips, metadata piles, or multiple competing callouts unless the brief explicitly demands them.
25
+
26
+ ## Narrative Sequence for Decks
27
+
28
+ Use a narrative rhythm that feels intentional:
29
+
30
+ 1. **Opener** — identity, premise, or promise
31
+ 2. **Support / proof** — key evidence, context, or concrete value
32
+ 3. **Detail / story** — workflow, mechanism, or deeper explanation
33
+ 4. **Close / CTA** — decision, recommendation, next step, or final message
34
+
35
+ Section dividers should reset the visual tempo. Alternate dense proof slides with simpler image-led or statement-led slides so the deck keeps breathing.
36
+
37
+ ## Review Litmus
38
+
39
+ Before showing the deck, ask:
40
+
41
+ - Can the audience grasp the main point of each slide in 3–5 seconds?
42
+ - Does each slide have one dominant idea instead of multiple competing blocks?
43
+ - Is there one real visual anchor, not just decoration?
44
+ - Would this still feel premium without shadows, cards, or extra chrome?
45
+ - Can any line of copy, badge, or callout be removed without losing meaning?
@@ -18,8 +18,10 @@ These are the packaged design rules for installable `slides-grab` skills.
18
18
  ## Asset rules
19
19
  - Store deck-local assets in `<slides-dir>/assets/`
20
20
  - Reference deck-local assets as `./assets/<file>`
21
+ - If an image comes from the web, download it into `<slides-dir>/assets/` before referencing it
21
22
  - Use `tldraw`-generated local assets for complex diagrams when possible
22
23
  - Allow `data:` URLs only when the slide must be fully self-contained
24
+ - Do not leave remote `http(s)://` image URLs in saved slide HTML
23
25
  - Never use absolute filesystem paths
24
26
 
25
27
  ## Package-published theme references
@@ -461,11 +461,13 @@ Store the image at `<slides-dir>/assets/team-photo.png`.
461
461
  <img src="data:image/svg+xml;base64,..." alt="Illustration" style="width: 220pt; height: 140pt; object-fit: cover;">
462
462
  ```
463
463
 
464
- #### Remote URL (Best-Effort Only)
464
+ #### Remote URL Source (Download Before Saving)
465
465
  ```html
466
466
  <img src="https://images.example.com/hero.png" alt="Hero image" style="width: 220pt; height: 140pt; object-fit: cover;">
467
467
  ```
468
468
 
469
+ If the image source starts on the web, download it into `<slides-dir>/assets/` and change the saved slide HTML to `./assets/<file>`.
470
+
469
471
  #### Placeholder (Image Stand-In)
470
472
  ```html
471
473
  <div data-image-placeholder style="width: 220pt; height: 140pt; border: 1px dashed #c7c7c7; background: #f3f4f6; display: flex; align-items: center; justify-content: center;">
@@ -478,7 +480,7 @@ Rules:
478
480
  - Use `./assets/<file>` as the default image contract for slide HTML.
479
481
  - Keep slide assets in `<slides-dir>/assets/`.
480
482
  - `data:` URLs are allowed for fully self-contained slides.
481
- - Remote `https://` URLs are allowed but non-deterministic and should be treated as fallback only.
483
+ - Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.
482
484
  - Do not use absolute filesystem paths in slide HTML.
483
485
  - Do not use non-body `background-image` for content imagery; use `<img>` instead.
484
486
  - Use `data-image-placeholder` to reserve space when no image is available yet.
@@ -573,6 +575,6 @@ This skill is **Stage 2**. It works from the `slide-outline.md` approved by the
573
575
 
574
576
  1. **CSS gradients**: Not supported in PowerPoint conversion — replace with background images
575
577
  2. **Webfonts**: Always include the Pretendard CDN link
576
- 3. **Image paths**: Use `./assets/<file>` from each `slide-XX.html`; avoid absolute filesystem paths
578
+ 3. **Image paths**: Use `./assets/<file>` from each `slide-XX.html`; avoid absolute filesystem paths and do not leave remote `http(s)://` image URLs in saved slide HTML
577
579
  4. **Colors**: Always include `#` prefix in CSS
578
580
  5. **Text rules**: Never place text directly in div/span
@@ -11,7 +11,7 @@
11
11
  - Keep slide assets in `<slides-dir>/assets/`.
12
12
  - Use `tldraw`-generated assets for complex diagrams whenever possible.
13
13
  - `data:` URLs are allowed for fully self-contained slides.
14
- - Remote `https://` URLs are allowed but non-deterministic and fallback only.
14
+ - Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.
15
15
  - Do not use absolute filesystem paths in slide HTML.
16
16
  - Do not use non-body `background-image` for content imagery; use `<img>` instead.
17
17
  - Use `data-image-placeholder` to reserve space when no image is available yet.
@@ -9,13 +9,52 @@ export const SLIDE_SIZE = { width: 960, height: 540 };
9
9
 
10
10
  const PPT_DESIGN_SKILL_PATH = join(getPackageRoot(), 'skills', 'slides-grab-design', 'SKILL.md');
11
11
  const DETAILED_DESIGN_SKILL_PATH = join(getPackageRoot(), 'skills', 'slides-grab-design', 'references', 'detailed-design-rules.md');
12
+ const BEAUTIFUL_SLIDE_DEFAULTS_PATH = join(getPackageRoot(), 'skills', 'slides-grab-design', 'references', 'beautiful-slide-defaults.md');
13
+ const EDITOR_PPT_DESIGN_SECTION_HEADINGS = [
14
+ '## Workflow',
15
+ '## Rules',
16
+ ];
12
17
  const DETAILED_DESIGN_SECTION_HEADINGS = [
13
18
  '## Base Settings',
14
- '### 4. Image Usage Rules (Local Asset / Data URL / Remote URL / Placeholder)',
15
19
  '## Text Usage Rules',
16
20
  '## Workflow (Stage 2: Design + Human Review)',
17
21
  '## Important Notes',
18
22
  ];
23
+ const BEAUTIFUL_SLIDE_DEFAULTS_SECTION_HEADINGS = [
24
+ '## Working Model',
25
+ '## Beautiful Defaults for Slides',
26
+ '## Narrative Sequence for Decks',
27
+ '## Review Litmus',
28
+ ];
29
+ const EDITOR_PPT_DESIGN_DUPLICATE_PATTERNS = [
30
+ /visual thesis/i,
31
+ /content plan/i,
32
+ /dominant visual anchor/i,
33
+ /cardless layouts/i,
34
+ /whitespace, alignment, scale, cropping, and contrast/i,
35
+ /opening slides and section dividers like posters/i,
36
+ ];
37
+ const EDITOR_PPT_DESIGN_SKILL_FALLBACK = [
38
+ '## Workflow',
39
+ '1. Read approved `slide-outline.md` or the existing slide before editing.',
40
+ '2. Run `slides-grab validate --slides-dir <path>` after generation or edits.',
41
+ '3. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.',
42
+ '4. Run `slides-grab build-viewer --slides-dir <path>` only after validation passes.',
43
+ '5. Run the slide litmus check from `references/beautiful-slide-defaults.md` before presenting the deck for review.',
44
+ '6. Iterate on user feedback by editing only requested slide files, then re-run validation and rebuild the viewer.',
45
+ '7. Keep revising until user approves conversion stage.',
46
+ '',
47
+ '## Rules',
48
+ '- Keep slide size 720pt x 405pt.',
49
+ '- Keep semantic text tags (`p`, `h1-h6`, `ul`, `ol`, `li`).',
50
+ '- Put local images under `<slides-dir>/assets/` and reference them as `./assets/<file>`.',
51
+ '- Allow `data:` URLs when the slide must be fully self-contained.',
52
+ '- Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.',
53
+ '- Prefer `<img>` for slide imagery and `data-image-placeholder` when no final asset exists.',
54
+ '- Do not present slides for review until `slides-grab validate --slides-dir <path>` passes.',
55
+ '- Do not start conversion before approval.',
56
+ '- Use the packaged CLI and bundled references only; do not depend on unpublished agent-specific files.',
57
+ ].join('\n');
19
58
  const DETAILED_DESIGN_SKILL_FALLBACK = [
20
59
  '## Base Settings',
21
60
  '',
@@ -29,7 +68,7 @@ const DETAILED_DESIGN_SKILL_FALLBACK = [
29
68
  '- Use ./assets/<file> as the default image contract for slide HTML.',
30
69
  '- Keep slide assets in <slides-dir>/assets/.',
31
70
  '- data: URLs are allowed for fully self-contained slides.',
32
- '- Remote https:// URLs are allowed but non-deterministic and fallback only.',
71
+ '- Do not leave remote http(s):// image URLs in saved slide HTML; download source images into <slides-dir>/assets/ and reference them as ./assets/<file>.',
33
72
  '- Do not use absolute filesystem paths in slide HTML.',
34
73
  '- Do not use non-body background-image for content imagery; use <img> instead.',
35
74
  '- Use data-image-placeholder to reserve space when no image is available yet.',
@@ -51,9 +90,38 @@ const DETAILED_DESIGN_SKILL_FALLBACK = [
51
90
  '- Always include # prefix in CSS colors.',
52
91
  '- Never place text directly in div/span.',
53
92
  ].join('\n');
93
+ const BEAUTIFUL_SLIDE_DEFAULTS_FALLBACK = [
94
+ '## Working Model',
95
+ '',
96
+ 'Before building the deck, write two things:',
97
+ '- **visual thesis** — one sentence describing the mood, material, energy, and imagery treatment.',
98
+ '- **content plan** — opener → support/proof → detail/story → close/CTA or decision.',
99
+ '- Define design tokens early: background, surface, primary text, muted text, accent, plus display/headline/body/caption roles.',
100
+ '',
101
+ '## Beautiful Defaults for Slides',
102
+ '- Start with composition, not components.',
103
+ '- Treat the opening slide like a poster and make the title or brand the loudest text.',
104
+ '- Give each slide one job, one primary takeaway, and one dominant visual anchor.',
105
+ '- Keep copy short enough to scan in seconds.',
106
+ '- Use whitespace, alignment, scale, cropping, and contrast before adding chrome.',
107
+ '- Limit the system by default: two typefaces max and one accent color.',
108
+ '- Default to cardless layouts unless a card improves structure or understanding.',
109
+ '',
110
+ '## Narrative Sequence for Decks',
111
+ '- Opener → support/proof → detail/story → close/CTA or decision.',
112
+ '- Section dividers should reset the visual tempo.',
113
+ '',
114
+ '## Review Litmus',
115
+ '- Can the audience grasp the main point of each slide in 3–5 seconds?',
116
+ '- Does the slide have one dominant idea instead of competing blocks?',
117
+ '- Is there one real visual anchor, not just decoration?',
118
+ '- Would this still feel premium without shadows, cards, or extra chrome?',
119
+ ].join('\n');
54
120
 
55
121
  let cachedPptDesignSkillPrompt = null;
56
- let cachedDetailedDesignSkillPrompt = null;
122
+ let cachedEditorPptDesignSkillPrompt = null;
123
+ let cachedStructuralDesignSkillPrompt = null;
124
+ let cachedSlideArtDirectionPrompt = null;
57
125
 
58
126
  function clamp(value, min, max) {
59
127
  return Math.min(max, Math.max(min, value));
@@ -142,6 +210,25 @@ export function getPptDesignSkillPrompt() {
142
210
  return cachedPptDesignSkillPrompt;
143
211
  }
144
212
 
213
+ function getEditorPptDesignSkillPrompt() {
214
+ if (cachedEditorPptDesignSkillPrompt !== null) {
215
+ return cachedEditorPptDesignSkillPrompt;
216
+ }
217
+
218
+ const prompt = loadMarkdownSections(
219
+ PPT_DESIGN_SKILL_PATH,
220
+ EDITOR_PPT_DESIGN_SECTION_HEADINGS,
221
+ EDITOR_PPT_DESIGN_SKILL_FALLBACK,
222
+ );
223
+
224
+ cachedEditorPptDesignSkillPrompt = pruneDuplicateLines(
225
+ prompt,
226
+ EDITOR_PPT_DESIGN_DUPLICATE_PATTERNS,
227
+ );
228
+
229
+ return cachedEditorPptDesignSkillPrompt;
230
+ }
231
+
145
232
  function extractMarkdownSection(markdown, heading) {
146
233
  const lines = markdown.split('\n');
147
234
  const startIndex = lines.findIndex((line) => line.trim() === heading.trim());
@@ -168,25 +255,74 @@ function extractMarkdownSection(markdown, heading) {
168
255
  return extracted.join('\n').trim();
169
256
  }
170
257
 
171
- export function getDetailedDesignSkillPrompt() {
172
- if (cachedDetailedDesignSkillPrompt !== null) {
173
- return cachedDetailedDesignSkillPrompt;
258
+ function pruneDuplicateLines(markdown, patterns) {
259
+ const lines = markdown.split('\n');
260
+ const filtered = [];
261
+
262
+ for (const line of lines) {
263
+ if (patterns.some((pattern) => pattern.test(line))) {
264
+ continue;
265
+ }
266
+
267
+ const previousLine = filtered.at(-1) ?? '';
268
+ if (line.trim() === '' && previousLine.trim() === '') {
269
+ continue;
270
+ }
271
+
272
+ filtered.push(line);
174
273
  }
175
274
 
275
+ return filtered.join('\n').trim();
276
+ }
277
+
278
+ function loadMarkdownSections(markdownPath, headings, fallback) {
176
279
  try {
177
- const markdown = readFileSync(DETAILED_DESIGN_SKILL_PATH, 'utf8');
178
- const sections = DETAILED_DESIGN_SECTION_HEADINGS
280
+ const markdown = readFileSync(markdownPath, 'utf8');
281
+ const sections = headings
179
282
  .map((heading) => extractMarkdownSection(markdown, heading))
180
283
  .filter(Boolean);
181
284
 
182
- cachedDetailedDesignSkillPrompt = sections.length > 0
285
+ return sections.length > 0
183
286
  ? sections.join('\n\n')
184
- : DETAILED_DESIGN_SKILL_FALLBACK;
287
+ : fallback;
185
288
  } catch {
186
- cachedDetailedDesignSkillPrompt = DETAILED_DESIGN_SKILL_FALLBACK;
289
+ return fallback;
187
290
  }
291
+ }
188
292
 
189
- return cachedDetailedDesignSkillPrompt;
293
+ function getStructuralDesignSkillPrompt() {
294
+ if (cachedStructuralDesignSkillPrompt !== null) {
295
+ return cachedStructuralDesignSkillPrompt;
296
+ }
297
+
298
+ cachedStructuralDesignSkillPrompt = loadMarkdownSections(
299
+ DETAILED_DESIGN_SKILL_PATH,
300
+ DETAILED_DESIGN_SECTION_HEADINGS,
301
+ DETAILED_DESIGN_SKILL_FALLBACK,
302
+ );
303
+
304
+ return cachedStructuralDesignSkillPrompt;
305
+ }
306
+
307
+ function getSlideArtDirectionPrompt() {
308
+ if (cachedSlideArtDirectionPrompt !== null) {
309
+ return cachedSlideArtDirectionPrompt;
310
+ }
311
+
312
+ cachedSlideArtDirectionPrompt = loadMarkdownSections(
313
+ BEAUTIFUL_SLIDE_DEFAULTS_PATH,
314
+ BEAUTIFUL_SLIDE_DEFAULTS_SECTION_HEADINGS,
315
+ BEAUTIFUL_SLIDE_DEFAULTS_FALLBACK,
316
+ );
317
+
318
+ return cachedSlideArtDirectionPrompt;
319
+ }
320
+
321
+ export function getDetailedDesignSkillPrompt() {
322
+ return [
323
+ getStructuralDesignSkillPrompt(),
324
+ getSlideArtDirectionPrompt(),
325
+ ].filter(Boolean).join('\n\n');
190
326
  }
191
327
 
192
328
  export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selections = [] }) {
@@ -215,7 +351,7 @@ export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selecti
215
351
  ];
216
352
  });
217
353
 
218
- const pptDesignSkillPrompt = getPptDesignSkillPrompt();
354
+ const pptDesignSkillPrompt = getEditorPptDesignSkillPrompt();
219
355
  const skillLines = pptDesignSkillPrompt
220
356
  ? [
221
357
  'Project skill guidance (follow strictly):',
@@ -224,7 +360,7 @@ export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selecti
224
360
  '',
225
361
  ]
226
362
  : [];
227
- const detailedDesignSkillPrompt = getDetailedDesignSkillPrompt();
363
+ const detailedDesignSkillPrompt = getStructuralDesignSkillPrompt();
228
364
  const detailedSkillLines = detailedDesignSkillPrompt
229
365
  ? [
230
366
  'Detailed design/export guardrails (selected from the full design system):',
@@ -233,23 +369,37 @@ export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selecti
233
369
  '',
234
370
  ]
235
371
  : [];
372
+ const slideArtDirectionPrompt = getSlideArtDirectionPrompt();
373
+ const slideArtDirectionLines = slideArtDirectionPrompt
374
+ ? [
375
+ 'Slide art direction defaults (packaged guidance for beautiful HTML slides):',
376
+ `Primary source: ${BEAUTIFUL_SLIDE_DEFAULTS_PATH}`,
377
+ slideArtDirectionPrompt,
378
+ '',
379
+ ]
380
+ : [];
236
381
 
237
382
  return [
238
383
  `Edit ${normalizedSlidePath} only.`,
239
384
  '',
240
385
  ...skillLines,
241
386
  ...detailedSkillLines,
387
+ ...slideArtDirectionLines,
242
388
  'User edit request:',
243
389
  sanitizedPrompt,
244
390
  '',
245
391
  'Selected regions on slide (960x540 coordinate space):',
246
392
  ...selectionLines,
247
393
  'Rules:',
248
- '- Modify only the requested slide file.',
394
+ '- Edit only the requested slide HTML file among slide-*.html files.',
395
+ '- Do not modify any other slide HTML files unless explicitly requested.',
249
396
  '- Keep existing structure/content unless the request requires a change.',
250
397
  '- Keep slide dimensions at 720pt x 405pt.',
251
398
  '- Keep text in semantic tags (<p>, <h1>-<h6>, <ul>, <ol>, <li>).',
399
+ '- You may add or update supporting files required for the requested slide, including local assets under <slides-dir>/assets/ and tldraw source/export files used to generate those assets.',
400
+ '- If you create or update a supporting asset, store it under <slides-dir>/assets/ and reference it from the requested slide as ./assets/<file>.',
252
401
  '- Keep local assets under ./assets/ and preserve portable relative paths.',
402
+ '- Do not modify unrelated assets, shared resources, or generated files that are not required for the requested slide.',
253
403
  '- Do not persist runtime-only editor/viewer injections such as <base>, debug scripts, or viewer wrapper markup into the slide file.',
254
404
  '- Return after applying the change.',
255
405
  ].join('\n');
@@ -0,0 +1,97 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ export const DEFAULT_EDIT_TIMEOUT_MS = 10 * 60 * 1000;
4
+ export const EDIT_TIMEOUT_EXIT_CODE = 124;
5
+ export const EDIT_TIMEOUT_ENV_VAR = 'PPT_AGENT_EDIT_TIMEOUT_MS';
6
+ export const EDIT_TIMEOUT_KILL_SIGNAL = 'SIGTERM';
7
+ export const EDIT_TIMEOUT_FORCE_KILL_AFTER_MS = 5_000;
8
+
9
+ export function parseEditTimeoutMs(rawValue = process.env[EDIT_TIMEOUT_ENV_VAR]) {
10
+ if (rawValue == null || rawValue === '') {
11
+ return DEFAULT_EDIT_TIMEOUT_MS;
12
+ }
13
+
14
+ const timeoutMs = Number(rawValue);
15
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
16
+ return DEFAULT_EDIT_TIMEOUT_MS;
17
+ }
18
+
19
+ return Math.floor(timeoutMs);
20
+ }
21
+
22
+ export function buildEditTimeoutMessage({ engineLabel = 'Editor process', timeoutMs }) {
23
+ return `${engineLabel} edit timed out after ${timeoutMs}ms and was terminated.`;
24
+ }
25
+
26
+ export function runEditSubprocess({
27
+ bin,
28
+ args,
29
+ cwd,
30
+ env,
31
+ stdio = ['ignore', 'pipe', 'pipe'],
32
+ timeoutMs = DEFAULT_EDIT_TIMEOUT_MS,
33
+ engineLabel,
34
+ onLog = () => {},
35
+ spawnImpl = spawn,
36
+ }) {
37
+ return new Promise((resolvePromise, rejectPromise) => {
38
+ const child = spawnImpl(bin, args, { cwd, env, stdio });
39
+
40
+ let stdout = '';
41
+ let stderr = '';
42
+ let timedOut = false;
43
+ let settled = false;
44
+ let forceKillTimer = null;
45
+
46
+ const timeoutMessage = buildEditTimeoutMessage({ engineLabel, timeoutMs });
47
+
48
+ const timeoutTimer = setTimeout(() => {
49
+ timedOut = true;
50
+ const messageLine = `${timeoutMessage}\n`;
51
+ stderr += messageLine;
52
+ onLog('stderr', messageLine);
53
+ child.kill(EDIT_TIMEOUT_KILL_SIGNAL);
54
+ forceKillTimer = setTimeout(() => {
55
+ child.kill('SIGKILL');
56
+ }, EDIT_TIMEOUT_FORCE_KILL_AFTER_MS);
57
+ forceKillTimer.unref?.();
58
+ }, timeoutMs);
59
+ timeoutTimer.unref?.();
60
+
61
+ child.stdout?.on('data', (chunk) => {
62
+ const text = chunk.toString();
63
+ stdout += text;
64
+ onLog('stdout', text);
65
+ });
66
+
67
+ child.stderr?.on('data', (chunk) => {
68
+ const text = chunk.toString();
69
+ stderr += text;
70
+ onLog('stderr', text);
71
+ });
72
+
73
+ child.on('close', (code, signal) => {
74
+ if (settled) return;
75
+ settled = true;
76
+ clearTimeout(timeoutTimer);
77
+ clearTimeout(forceKillTimer);
78
+ resolvePromise({
79
+ code: timedOut ? EDIT_TIMEOUT_EXIT_CODE : (code ?? 1),
80
+ stdout,
81
+ stderr,
82
+ signal: timedOut ? (signal || EDIT_TIMEOUT_KILL_SIGNAL) : signal,
83
+ timedOut,
84
+ timeoutMs: timedOut ? timeoutMs : null,
85
+ timeoutMessage: timedOut ? timeoutMessage : null,
86
+ });
87
+ });
88
+
89
+ child.on('error', (error) => {
90
+ if (settled) return;
91
+ settled = true;
92
+ clearTimeout(timeoutTimer);
93
+ clearTimeout(forceKillTimer);
94
+ rejectPromise(error);
95
+ });
96
+ });
97
+ }
@@ -66,9 +66,9 @@ export function buildImageContractReport({ slideFile, sources = [] }) {
66
66
 
67
67
  if (classification.kind === 'remote-url') {
68
68
  issues.push({
69
- severity: 'warning',
69
+ severity: 'critical',
70
70
  code: 'remote-image-url',
71
- message: 'Remote image URL is best-effort only and may break deterministic exports.',
71
+ message: 'Remote image URLs are unsupported in saved slide HTML. Download the image into ./assets/<file> instead.',
72
72
  slide: slideFile,
73
73
  ...entry,
74
74
  });
@@ -77,9 +77,9 @@ export function buildImageContractReport({ slideFile, sources = [] }) {
77
77
 
78
78
  if (classification.kind === 'remote-url-insecure') {
79
79
  issues.push({
80
- severity: 'warning',
80
+ severity: 'critical',
81
81
  code: 'remote-image-url-insecure',
82
- message: 'Insecure http:// image URL is discouraged. Prefer ./assets/<file> or data: URLs.',
82
+ message: 'Remote http:// image URLs are unsupported in saved slide HTML. Download the image into ./assets/<file> instead.',
83
83
  slide: slideFile,
84
84
  ...entry,
85
85
  });
@@ -183,11 +183,11 @@ export function buildSlideRuntimeHtml(html, { baseHref, slideFile }) {
183
183
  const src = (image.getAttribute('src') || '').trim();
184
184
  if (!src || src.startsWith('data:')) continue;
185
185
  if (src.startsWith('https://')) {
186
- warn('remote image is best-effort only', { src });
186
+ fail('remote image URL is unsupported in saved slides; download it into ./assets/<file>', { src });
187
187
  continue;
188
188
  }
189
189
  if (src.startsWith('http://')) {
190
- warn('insecure remote image is discouraged', { src });
190
+ fail('remote http:// image URL is unsupported in saved slides; download it into ./assets/<file>', { src });
191
191
  continue;
192
192
  }
193
193
  if (absolutePathRe.test(src) || src.startsWith('/')) {
@@ -622,6 +622,10 @@ const EXPORT_BLOCKING_IMAGE_CONTRACT_CODES = new Set([
622
622
  'absolute-filesystem-image-path',
623
623
  'missing-local-asset',
624
624
  'missing-local-background-asset',
625
+ 'remote-background-image-url',
626
+ 'remote-background-image-url-insecure',
627
+ 'remote-image-url',
628
+ 'remote-image-url-insecure',
625
629
  'root-relative-image-path',
626
630
  'unsupported-background-image',
627
631
  ]);