@office-kit/pptx-dev 0.2.3 → 0.3.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 CHANGED
@@ -17,7 +17,7 @@ npm run dev
17
17
  ```
18
18
 
19
19
  Open the local URL printed by the server. Save a slide file, `theme.ts` or `deck.tsx` to rebuild. The viewer
20
- has a vertical thumbnail strip and a large slide canvas. Click a thumbnail or use
20
+ has a vertical thumbnail strip, a large slide canvas and an AI chat panel on the right. Click a thumbnail or use
21
21
  arrow keys, Page Up/Down, Home/End to navigate. Fit/zoom and Present (Escape to
22
22
  exit) are viewing controls; the canvas has no editing, dragging or resize handles.
23
23
  Changes are made only in TSX, including when an AI agent edits the presentation.
@@ -43,7 +43,50 @@ The generated `CLAUDE.md` explains authoring conventions. VSCode picks up the
43
43
  included `tsconfig.json` for completion and diagnostics. Start the **Preview
44
44
  presentation** task, then use **Simple Browser: Show** with the printed URL and
45
45
  move it to a side editor group. No custom editor extension is needed for this
46
- workflow. Preview-to-source selection is not implemented.
46
+ workflow. Clicking a slide supplies chat context; opening its exact TSX location is not implemented.
47
+
48
+ ## Chat in the preview
49
+
50
+ Choose **Claude Code** or **Codex** in the right panel and describe the change.
51
+ Install the selected CLI (`claude` or `codex`) on your PATH and sign in through
52
+ that CLI once before using chat. The dev server runs it from the deck entry's
53
+ directory, using its existing account and model configuration. No API key is
54
+ stored in the browser. Requests use the selected provider and its normal usage
55
+ limits. Sending a message authorizes edits to the local project.
56
+
57
+ Each request includes the focused slide's **1-based number, text, slide count,
58
+ preview revision, deck entry and source dependency paths**, plus any build error.
59
+ The focus is captured at send time, so navigating during a response does not
60
+ change it. A stale preview is rejected with a retry message. Dependency paths are
61
+ source candidates, not an exact slide-to-file mapping; the agent locates the
62
+ relevant TSX using the entry, slide order and text. This also works for single-file
63
+ decks, generated slide lists and imported presentations.
64
+
65
+ “This slide” directs a focused patch; requests mentioning other slides or the
66
+ whole deck can edit other files or shared styling. Saving uses the same automatic
67
+ preview rebuild as manual editing. Both CLIs run non-interactively:
68
+
69
+ - Claude Code uses print mode with JSON events and `acceptEdits`, exposing only
70
+ `Read`, `Edit`, `Write`, `Glob` and `Grep`. Shell execution is not exposed.
71
+ - Codex uses `exec --json` with the `workspace-write` sandbox. Sandbox bypass is
72
+ never enabled. Operations requiring interactive approval are not supported.
73
+
74
+ Progress, replies and failures appear in the panel. **Stop** terminates the running
75
+ agent; edits already saved remain. Only one edit runs at a time across browser
76
+ tabs. **New chat** clears conversational context without reverting files. Recent
77
+ 12 messages (bounded text) are passed to each new CLI invocation; these are new
78
+ CLI sessions, not a continuation of an existing terminal conversation. The server
79
+ keeps up to 100 messages in memory, shared by its tabs, until it exits. Reloading
80
+ restores that conversation. Restarting the server clears it.
81
+
82
+ Use **Chat** in the header to hide/show the panel, and Ctrl/Cmd+Enter to send.
83
+ On narrow screens the panel moves below the preview. Presentation mode hides it.
84
+ Chat requests require same-origin JSON as well as the server's loopback Host
85
+ check. Authentication and CLI installation still happen in a terminal; normal
86
+ slide editing and review can then stay in the preview.
87
+
88
+ Protocol references: [Codex non-interactive mode](https://developers.openai.com/codex/noninteractive)
89
+ and [Claude Code programmatic use](https://code.claude.com/docs/en/headless).
47
90
 
48
91
  ## Edit only what changed
49
92
 
@@ -86,7 +129,7 @@ preview server binds to `127.0.0.1` and rejects unexpected Host headers.
86
129
 
87
130
  Programmatic exports: `buildDeck`, `exportDeck`, `initProject`, `inspectTemplate`.
88
131
  `buildDeck` returns PPTX bytes, SVG slides, aspect ratio, module dependencies and
89
- core validation diagnostics. Browser APIs and DOM globals are not available in
132
+ slide text and core validation diagnostics. Browser APIs and DOM globals are not available in
90
133
  TSX evaluation.
91
134
 
92
135
  ## Developing in this monorepo
package/dist/cli.mjs CHANGED
@@ -3,6 +3,7 @@ import { a as createDeckBuilder, i as initProject, n as exportDeck, r as inspect
3
3
  import { dirname, resolve, sep } from "node:path";
4
4
  import { watch } from "node:fs";
5
5
  import { createServer } from "node:http";
6
+ import { spawn } from "node:child_process";
6
7
  //#region src/page.ts
7
8
  const page = `<!doctype html>
8
9
  <html lang="en">
@@ -17,7 +18,7 @@ button:hover:not(:disabled),.download:hover{background:#f0f3f9}button:disabled{o
17
18
  header{display:flex;align-items:center;gap:16px;padding:0 20px;background:#fff;border-bottom:1px solid #d4d9e2;min-width:0}
18
19
  .brand{font-weight:700;font-size:17px;white-space:nowrap}.badge{font-size:11px;color:#616b7c;background:#f1f3f7;border-radius:4px;padding:3px 6px}
19
20
  #status{flex:1;color:#667085;min-width:0}#present{background:#293c73;color:#fff;border-color:#293c73}
20
- .workspace{display:grid;grid-template-columns:224px minmax(0,1fr);min-height:0}
21
+ .workspace{display:grid;grid-template-columns:190px minmax(0,1fr) 340px;min-height:0}
21
22
  .filmstrip{background:#f7f8fa;border-right:1px solid #d4d9e2;overflow:auto;overscroll-behavior:contain;padding:16px 12px}
22
23
  .filmstrip h2{margin:0 0 12px 26px;text-transform:uppercase;letter-spacing:.1em;font-size:10px;color:#7c8596;font-weight:600}
23
24
  #thumbnails{display:flex;flex-direction:column;gap:12px;margin:0;padding:0;list-style:none}
@@ -30,32 +31,52 @@ main{min-width:0;min-height:0;display:flex;flex-direction:column}
30
31
  #error[hidden]{display:none}
31
32
  #stage{flex:1;min-height:0;overflow:auto;display:flex;padding:32px;overscroll-behavior:contain}
32
33
  #slide{position:relative;flex:none;margin:auto;background:white;box-shadow:0 3px 24px #19212d20;overflow:hidden}
33
- #slide iframe{position:absolute;inset:0;display:block;width:100%;height:100%;border:0;pointer-events:none}
34
+ #slide{user-select:text;-webkit-user-select:text}
34
35
  #empty{margin:auto;color:#737d8e}
35
36
  footer{display:flex;align-items:center;gap:14px;padding:0 16px;background:#fff;border-top:1px solid #d4d9e2;font-size:12px}
36
37
  #count{min-width:90px}footer .hint{flex:1;color:#737d8e}footer button{padding:3px 10px}footer select{padding:3px 8px}
37
38
  #presentation-controls{display:none}
38
39
  body.presenting{grid-template-rows:minmax(0,1fr);background:#111}
39
- .presenting header,.presenting footer,.presenting .filmstrip{display:none}
40
+ .presenting header,.presenting footer,.presenting .filmstrip,.presenting #chat{display:none}
40
41
  .presenting .workspace{grid-template-columns:minmax(0,1fr)}.presenting #stage{padding:0;background:#111}.presenting #slide{box-shadow:none}
41
42
  .presenting #presentation-controls{display:flex;position:fixed;bottom:16px;left:50%;transform:translateX(-50%);align-items:center;gap:12px;background:#202735e8;color:white;padding:6px;border-radius:8px;opacity:0;transition:opacity .15s}
42
43
  .presenting #presentation-controls:hover,.presenting #presentation-controls:focus-within{opacity:1}
43
44
  #presentation-controls button{background:transparent;color:white;border-color:#5d6575}
44
45
  @media(max-width:700px){.workspace{grid-template-columns:140px minmax(0,1fr)}.filmstrip{padding:12px 5px}header{padding:0 12px;gap:10px}.badge,footer .hint{display:none}#stage{padding:16px}footer{gap:8px}#status{font-size:11px}.download{padding:7px 8px}}
46
+ #chat{min-height:0;display:flex;flex-direction:column;background:#fff;border-left:1px solid #d4d9e2}
47
+ .chat-heading{display:flex;align-items:center;gap:8px;padding:16px;border-bottom:1px solid #e6e9ef}.chat-heading strong{flex:1}.chat-heading button{padding:5px 8px}
48
+ #messages{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:16px;overscroll-behavior:contain}
49
+ .chat-message{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.6}.chat-message.user{background:#eef2ff;border-radius:8px;padding:10px 12px}.chat-message small{display:block;color:#697386;font-size:11px;margin-bottom:4px}.chat-intro{color:#737d8e;line-height:1.7}
50
+ #chat-form{padding:14px;border-top:1px solid #e6e9ef}#chat-context{font-size:11px;color:#4967dd;margin-bottom:8px}#chat-input{font:inherit;resize:vertical;min-height:90px;max-height:200px;width:100%;padding:10px;border:1px solid #d4d9e2;border-radius:8px} .chat-actions{display:flex;gap:8px;margin-top:8px;align-items:center}.chat-actions select{min-width:0;flex:1}#chat-send{background:#293c73;color:white}#chat-status{padding:0 14px 12px;color:#667085;font-size:11px;white-space:pre-wrap;max-height:90px;overflow:auto}
51
+ body.chat-hidden .workspace{grid-template-columns:190px minmax(0,1fr)}body.chat-hidden #chat{display:none}
52
+ body.presenting .workspace{grid-template-columns:minmax(0,1fr)}
53
+ @media(max-width:1000px){.workspace{grid-template-columns:120px minmax(0,1fr) 300px}.filmstrip{padding:12px 5px}#stage{padding:16px}}
54
+ @media(max-width:700px){.workspace{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(160px,1fr) minmax(200px,1fr)}.filmstrip{display:none}#chat{border-top:1px solid #d4d9e2}.chat-heading{padding:8px 12px}#chat-form{padding:8px 12px}#chat-input{min-height:50px}body.chat-hidden .workspace,.presenting .workspace{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}.brand{font-size:14px}header{gap:6px}.download{font-size:11px}}
45
55
  @media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
46
56
  </style>
47
- <header><span class="brand">Office Kit</span><span class="badge">Preview</span><span id="status" role="status">Building…</span><button id="present" disabled>Present</button><a class="download" href="/deck.pptx">Download PPTX</a></header>
57
+ <header><span class="brand">Office Kit</span><span class="badge">Preview</span><span id="status" role="status">Building…</span><button id="toggle-chat" aria-expanded="true" aria-controls="chat">Chat</button><button id="present" disabled>Present</button><a class="download" href="/deck.pptx">Download PPTX</a></header>
48
58
  <div class="workspace">
49
59
  <nav class="filmstrip" aria-label="Slides"><h2>Slides</h2><ol id="thumbnails"></ol></nav>
50
60
  <main aria-label="Slide viewer"><pre id="error" role="alert" hidden></pre><div id="stage" tabindex="-1"><div id="empty">Waiting for slides…</div><div id="slide" hidden></div></div></main>
61
+ <aside id="chat" aria-label="Slide chat">
62
+ <div class="chat-heading"><strong>Edit with AI</strong><button id="chat-reset">New chat</button></div>
63
+ <div id="messages" role="log" aria-label="Conversation"><p class="chat-intro">Describe what you want to change. The current slide is included automatically. You can also ask about other slides or the whole deck.<br><br>Uses your locally installed Claude Code or Codex. Sign in through its CLI first. Sending a message allows it to edit this project.</p></div>
64
+ <form id="chat-form"><div id="chat-context">No slide selected</div><label for="chat-input" class="chat-intro">What would you like to change?</label><textarea id="chat-input" maxlength="16000" placeholder="Make this headline shorter…" required></textarea><div class="chat-actions"><select id="chat-provider" aria-label="AI provider"><option value="claude">Claude Code</option><option value="codex">Codex</option></select><button id="chat-stop" type="button" hidden>Stop</button><button id="chat-send" type="submit">Send</button></div></form>
65
+ <div id="chat-status" role="status">Ready</div>
66
+ </aside>
51
67
  </div>
52
- <footer><span id="count" aria-live="polite">No slides</span><span class="hint">Changes appear automatically · View only</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>
68
+ <footer><span id="count" aria-live="polite">No slides</span><span class="hint">Changes appear automatically · Text can be selected and copied</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>
53
69
  <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>
54
70
  <script>
55
71
  let state={slides:[],error:null,aspectRatio:16/9},index=0,urls=[],presenting=false;
56
- let displayedSvg, pendingFrame;
72
+ let displayedSvg;
57
73
  const byId=id=>document.getElementById(id);
58
74
  const stage=byId('stage'),slide=byId('slide'),thumbnails=byId('thumbnails');
75
+ // The slide lives in a shadow root rather than a sandboxed iframe so its text
76
+ // (XHTML inside <foreignObject>) can be selected and copied while the deck's
77
+ // SVG stays out of the viewer's own DOM and CSS. Keyboard events still reach the
78
+ // document, so arrow-key navigation keeps working after clicking into a slide.
79
+ const canvas=slide.attachShadow({mode:'open'});
59
80
  function resize(){
60
81
  if(!state.slides.length)return;
61
82
  const style=getComputedStyle(stage);
@@ -69,6 +90,7 @@ function resize(){
69
90
  function selectSlide(next,focusThumbnail=false,reveal=true){
70
91
  index=Math.max(0,Math.min(next,state.slides.length-1));
71
92
  const count=state.slides.length?'Slide '+(index+1)+' of '+state.slides.length:'No slides';
93
+ byId('chat-context').textContent=state.slides.length?'Context: '+count:'Context: whole project';
72
94
  byId('count').textContent=count;byId('present-count').textContent=count;
73
95
  for(const id of ['prev','present-prev'])byId(id).disabled=index===0;
74
96
  for(const id of ['next','present-next'])byId(id).disabled=index>=state.slides.length-1;
@@ -78,21 +100,9 @@ function selectSlide(next,focusThumbnail=false,reveal=true){
78
100
  const svg=state.slides[index];
79
101
  if(svg!==displayedSvg){
80
102
  displayedSvg=svg;
81
- if(pendingFrame){pendingFrame.remove();pendingFrame=null;}
82
- if(svg){
83
- const frame=document.createElement('iframe');frame.sandbox='';frame.tabIndex=-1;
84
- frame.title='Slide '+(index+1);frame.style.visibility='hidden';
85
- pendingFrame=frame;
86
- frame.onload=()=>{
87
- if(pendingFrame!==frame)return;
88
- for(const child of Array.from(slide.children))if(child!==frame)child.remove();
89
- frame.style.visibility='visible';pendingFrame=null;
90
- };
91
- frame.srcdoc='<style>html,body{margin:0;width:100%;height:100%;overflow:hidden}svg{display:block;width:100%;height:100%}</style>'+svg;
92
- slide.append(frame);
93
- }else slide.replaceChildren();
103
+ canvas.innerHTML=svg?'<style>svg{display:block;width:100%;height:100%}</style>'+svg:'';
94
104
  }
95
- for(const frame of slide.children)frame.title='Slide '+(index+1);
105
+ slide.setAttribute('aria-label','Slide '+(index+1));
96
106
  for(const [position,item] of Array.from(thumbnails.children).entries()){
97
107
  const button=item.firstElementChild,selected=position===index;
98
108
  button.setAttribute('aria-current',String(selected));button.tabIndex=selected?0:-1;
@@ -151,7 +161,7 @@ document.addEventListener('fullscreenchange',()=>{if(!document.fullscreenElement
151
161
  for(const id of ['prev','present-prev'])byId(id).onclick=()=>selectSlide(index-1);
152
162
  for(const id of ['next','present-next'])byId(id).onclick=()=>selectSlide(index+1);
153
163
  byId('zoom').onchange=resize;
154
- stage.onclick=()=>{if(presenting)selectSlide(index+1);};
164
+ stage.onclick=()=>{if(presenting&&!getSelection().toString())selectSlide(index+1);};
155
165
  document.addEventListener('keydown',event=>{
156
166
  if(event.key==='Escape'&&presenting){event.preventDefault();void exitPresentation();return;}
157
167
  if(event.altKey||event.ctrlKey||event.metaKey||event.target.closest('select,input,textarea,[contenteditable]'))return;
@@ -171,9 +181,279 @@ async function refresh(){
171
181
  try{const response=await fetch('/state'+(state.revision===undefined?'':'?since='+state.revision));if(!response.ok)throw new Error('Preview unavailable');const updated=await response.json();if(id===refreshId){if(updated.changes){updated.slides=state.slides.slice(0,updated.count);updated.slides.length=updated.count;for(const [position,svg] of Object.entries(updated.changes))updated.slides[Number(position)]=svg;}update(updated);}}
172
182
  catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}
173
183
  }
174
- const events=new EventSource('/events');events.onmessage=event=>{if(event.data==='ready')delete state.revision;void refresh();};events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();
184
+ let chatRequest=0,chatBusy=false,lastMessages='';
185
+ function renderChat(chat){
186
+ chatBusy=chat.running;
187
+ byId('chat-send').disabled=chat.running;
188
+ byId('chat-reset').disabled=chat.running;
189
+ byId('chat-provider').disabled=chat.running;
190
+ byId('chat-stop').hidden=!chat.running;
191
+ byId('chat-status').textContent=chat.status;
192
+ if(chat.messages.length)byId('chat-provider').value=chat.provider;
193
+ const serialized=JSON.stringify(chat.messages);
194
+ if(serialized===lastMessages)return;
195
+ lastMessages=serialized;
196
+ const log=byId('messages'),atBottom=log.scrollHeight-log.scrollTop-log.clientHeight<60;
197
+ log.replaceChildren();
198
+ if(!chat.messages.length){const intro=document.createElement('p');intro.className='chat-intro';intro.textContent='Ask for a focused slide edit or a change across the deck. Uses your locally installed CLI and its login. Sending allows edits to this project.';log.append(intro);}
199
+ for(const message of chat.messages){
200
+ const item=document.createElement('div'),label=document.createElement('small'),body=document.createElement('div');
201
+ item.className='chat-message '+message.role;
202
+ label.textContent=message.role==='user'?'You · '+(message.context?.slide?'Slide '+message.context.slide:'Whole project'):'Assistant';
203
+ body.textContent=message.text||(chat.running?'Working…':'No response');item.append(label,body);log.append(item);
204
+ }
205
+ if(atBottom)log.scrollTop=log.scrollHeight;
206
+ }
207
+ async function refreshChat(){
208
+ const id=++chatRequest;
209
+ try{const response=await fetch('/chat');if(!response.ok)throw new Error('Chat unavailable');const chat=await response.json();if(id===chatRequest)renderChat(chat);}
210
+ catch(error){if(id===chatRequest)byId('chat-status').textContent=error.message;}
211
+ }
212
+ async function chatAction(path,payload){
213
+ ++chatRequest;
214
+ const response=await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
215
+ const result=await response.json();if(!response.ok)throw new Error(result.error||'Chat request failed');
216
+ await refreshChat();
217
+ }
218
+ byId('chat-form').onsubmit=async event=>{
219
+ event.preventDefault();if(chatBusy)return;
220
+ const input=byId('chat-input'),message=input.value.trim();if(!message)return;
221
+ chatBusy=true;byId('chat-send').disabled=true;
222
+ try{await chatAction('/chat',{message,provider:byId('chat-provider').value,slide:state.slides.length?index:null,revision:state.revision??0});if(input.value.trim()===message)input.value='';}
223
+ catch(error){byId('chat-status').textContent=error.message;chatBusy=false;byId('chat-send').disabled=false;}
224
+ };
225
+ byId('chat-input').onkeydown=event=>{if(event.key==='Enter'&&(event.metaKey||event.ctrlKey)&&!event.isComposing){event.preventDefault();byId('chat-form').requestSubmit();}};
226
+ for(const action of ['stop','reset'])byId('chat-'+action).onclick=()=>chatAction('/chat/'+action,{}).catch(error=>{byId('chat-status').textContent=error.message;});
227
+ byId('toggle-chat').onclick=()=>{const hidden=document.body.classList.toggle('chat-hidden');byId('toggle-chat').setAttribute('aria-expanded',String(!hidden));resize();};
228
+ void refreshChat();
229
+ const events=new EventSource('/events');events.onmessage=event=>{if(event.data==='chat'){void refreshChat();return;}if(event.data==='ready'){delete state.revision;void refreshChat();}void refresh();};events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();
175
230
  <\/script></html>`;
176
231
  //#endregion
232
+ //#region src/chat.ts
233
+ /** One shared conversation and one editing process per dev server. */
234
+ function createChat(entry, notify) {
235
+ let messages = [];
236
+ let provider = "claude";
237
+ let child;
238
+ let status = "Ready";
239
+ let stopping = false;
240
+ let killTimer;
241
+ let completion = Promise.resolve();
242
+ const snapshot = () => ({
243
+ messages,
244
+ provider,
245
+ running: !!child,
246
+ status
247
+ });
248
+ function stop() {
249
+ if (!child || stopping) return;
250
+ stopping = true;
251
+ status = "Stopping…";
252
+ const processToStop = child;
253
+ const kill = (signal) => {
254
+ try {
255
+ if (process.platform !== "win32" && processToStop.pid) process.kill(-processToStop.pid, signal);
256
+ else processToStop.kill(signal);
257
+ } catch {}
258
+ };
259
+ kill("SIGTERM");
260
+ killTimer = setTimeout(() => kill("SIGKILL"), 2e3);
261
+ notify();
262
+ }
263
+ function start(text, selected, context) {
264
+ provider = selected;
265
+ const user = {
266
+ role: "user",
267
+ text,
268
+ context
269
+ };
270
+ const history = messages.slice(-12).map((message) => ({
271
+ ...message,
272
+ text: message.text.slice(-12e3)
273
+ }));
274
+ const prompt = `You are editing a local PowerPoint TSX project in ${dirname(resolve(entry))}.
275
+ Read the project's authoring instructions. Make focused patches to the TSX source, preserving unrelated work.
276
+ The preview rebuilds automatically on save; do not start another dev server.
277
+ 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.
278
+ 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.
279
+ Respond in the user's language, briefly describing edits or answering their question. If permissions prevent an action, explain that instead of claiming success.
280
+ Recent conversation (JSON): ${JSON.stringify(history)}
281
+ Current request and preview context (JSON): ${JSON.stringify(user)}`;
282
+ messages.push(user);
283
+ const answer = {
284
+ role: "assistant",
285
+ text: ""
286
+ };
287
+ messages.push(answer);
288
+ if (messages.length > 100) messages = messages.slice(-100);
289
+ status = "Working…";
290
+ stopping = false;
291
+ child = spawn(selected, selected === "codex" ? [
292
+ "exec",
293
+ "--json",
294
+ "--sandbox",
295
+ "workspace-write",
296
+ "--skip-git-repo-check",
297
+ "-"
298
+ ] : [
299
+ "--print",
300
+ "--verbose",
301
+ "--output-format",
302
+ "stream-json",
303
+ "--permission-mode",
304
+ "acceptEdits",
305
+ "--tools",
306
+ "Read,Edit,Write,Glob,Grep"
307
+ ], {
308
+ cwd: dirname(resolve(entry)),
309
+ stdio: [
310
+ "pipe",
311
+ "pipe",
312
+ "pipe"
313
+ ],
314
+ detached: process.platform !== "win32",
315
+ shell: false
316
+ });
317
+ const running = child;
318
+ let buffer = "", stderr = "", failure = "", sawResult = false;
319
+ function append(value) {
320
+ answer.text = (answer.text + (answer.text ? "\n\n" : "") + value).slice(-1e5);
321
+ notify();
322
+ }
323
+ function consume(line) {
324
+ if (!line.trim()) return;
325
+ try {
326
+ const event = JSON.parse(line);
327
+ if (selected === "codex") {
328
+ if (event.type === "item.completed" && event.item?.type === "agent_message") append(event.item.text ?? "");
329
+ if (event.type === "item.started") {
330
+ status = "Editing / inspecting…";
331
+ notify();
332
+ }
333
+ if (event.type === "turn.completed") sawResult = true;
334
+ if (event.type === "turn.failed" || event.type === "error") failure = event.error?.message ?? event.message ?? "Agent failed";
335
+ } else {
336
+ if (event.type === "assistant") for (const block of event.message?.content ?? []) {
337
+ if (block.type === "text") append(block.text);
338
+ if (block.type === "tool_use") {
339
+ status = "Using " + block.name + "…";
340
+ notify();
341
+ }
342
+ }
343
+ if (event.type === "result") {
344
+ sawResult = true;
345
+ if (event.is_error) failure = event.result || event.errors?.join("\n") || "Agent failed";
346
+ else if (!answer.text && event.result) append(event.result);
347
+ if (event.permission_denials?.length) failure = "Some actions were denied by CLI permissions. " + (event.result ?? "");
348
+ }
349
+ }
350
+ } catch {
351
+ failure = "Unexpected CLI output. Update your CLI and try again.";
352
+ }
353
+ }
354
+ running.stdout.setEncoding("utf8");
355
+ running.stdout.on("data", (chunk) => {
356
+ buffer += chunk;
357
+ let end;
358
+ while ((end = buffer.indexOf("\n")) !== -1) {
359
+ consume(buffer.slice(0, end));
360
+ buffer = buffer.slice(end + 1);
361
+ }
362
+ if (buffer.length > 2e6) {
363
+ failure = "CLI output exceeded the limit.";
364
+ stop();
365
+ }
366
+ });
367
+ running.stderr.setEncoding("utf8");
368
+ running.stderr.on("data", (chunk) => {
369
+ stderr = (stderr + chunk).slice(-8e3);
370
+ });
371
+ running.stdin.on("error", () => {});
372
+ running.stdin.end(prompt);
373
+ completion = new Promise((done) => {
374
+ running.on("error", (error) => {
375
+ failure = `Could not run ${selected}. Install it and log in from your terminal first. ${error.message}`;
376
+ });
377
+ running.on("close", (code) => {
378
+ if (buffer.trim()) consume(buffer);
379
+ clearTimeout(killTimer);
380
+ status = stopping ? "Stopped. Saved edits remain in the preview." : failure || (code !== 0 ? stderr || `${selected} exited with code ${code}` : !sawResult ? "CLI exited without a completed result." : "Done");
381
+ if (status !== "Done") append(status);
382
+ child = void 0;
383
+ notify();
384
+ done();
385
+ });
386
+ });
387
+ notify();
388
+ }
389
+ async function handle(request, response, focus) {
390
+ const json = (code, value) => {
391
+ response.writeHead(code, { "Content-Type": "application/json" });
392
+ response.end(JSON.stringify(value));
393
+ };
394
+ if (request.method === "GET" && request.url === "/chat") {
395
+ json(200, snapshot());
396
+ return;
397
+ }
398
+ if (request.method !== "POST") {
399
+ json(405, { error: "Method not allowed" });
400
+ return;
401
+ }
402
+ if (request.headers.origin !== `http://${request.headers.host}` || request.headers["content-type"]?.split(";")[0] !== "application/json") {
403
+ json(403, { error: "Same-origin JSON required" });
404
+ return;
405
+ }
406
+ try {
407
+ let body = "";
408
+ request.setEncoding("utf8");
409
+ for await (const chunk of request) {
410
+ body += chunk;
411
+ if (Buffer.byteLength(body) > 32e3) {
412
+ json(413, { error: "Message too large" });
413
+ return;
414
+ }
415
+ }
416
+ const value = JSON.parse(body);
417
+ if (request.url === "/chat/stop") {
418
+ stop();
419
+ json(200, snapshot());
420
+ return;
421
+ }
422
+ if (child) {
423
+ json(409, { error: "An edit is already running" });
424
+ return;
425
+ }
426
+ if (request.url === "/chat/reset") {
427
+ messages = [];
428
+ status = "Ready";
429
+ notify();
430
+ json(200, snapshot());
431
+ return;
432
+ }
433
+ if (request.url !== "/chat") {
434
+ json(404, { error: "Not found" });
435
+ return;
436
+ }
437
+ if (typeof value.message !== "string" || !value.message.trim() || value.message.length > 16e3 || !["claude", "codex"].includes(value.provider) || !(value.slide === null || Number.isInteger(value.slide) && value.slide >= 0) || !Number.isInteger(value.revision)) {
438
+ json(400, { error: "Invalid chat request" });
439
+ return;
440
+ }
441
+ const context = focus(value.slide, value.revision);
442
+ start(value.message.trim(), value.provider, context);
443
+ json(202, snapshot());
444
+ } catch (cause) {
445
+ json(400, { error: cause instanceof Error ? cause.message : "Invalid request" });
446
+ }
447
+ }
448
+ return {
449
+ handle,
450
+ async close() {
451
+ stop();
452
+ await completion;
453
+ }
454
+ };
455
+ }
456
+ //#endregion
177
457
  //#region src/server.ts
178
458
  async function serveDeck(entry, port = 4173) {
179
459
  let latest;
@@ -185,6 +465,9 @@ async function serveDeck(entry, port = 4173) {
185
465
  let pending = false;
186
466
  let closed = false;
187
467
  const clients = /* @__PURE__ */ new Set();
468
+ const chat = createChat(entry, () => {
469
+ for (const client of clients) client.write("data: chat\n\n");
470
+ });
188
471
  const server = createServer((request, response) => {
189
472
  const host = request.headers.host;
190
473
  if (host !== `127.0.0.1:${actualPort}` && host !== `localhost:${actualPort}`) {
@@ -192,7 +475,21 @@ async function serveDeck(entry, port = 4173) {
192
475
  return;
193
476
  }
194
477
  response.setHeader("Cache-Control", "no-store");
195
- if (request.url === "/events") {
478
+ if (request.url === "/chat" || request.url?.startsWith("/chat/")) chat.handle(request, response, (index, viewedRevision) => {
479
+ if (viewedRevision !== revision) throw new Error("Preview changed. Review the current slide and send again.");
480
+ if (index !== null && (!latest || index >= latest.slides.length)) throw new Error("Slide no longer exists.");
481
+ return {
482
+ slide: index === null ? null : index + 1,
483
+ count: latest?.slides.length ?? 0,
484
+ revision,
485
+ text: index === null ? "" : (latest?.slideTexts[index] ?? "").slice(0, 12e3),
486
+ entry: resolve(entry),
487
+ files: latest?.dependencies ?? [resolve(entry)],
488
+ buildError: error
489
+ };
490
+ });
491
+ else if (request.method !== "GET") response.writeHead(405).end();
492
+ else if (request.url === "/events") {
196
493
  response.writeHead(200, { "Content-Type": "text/event-stream" });
197
494
  response.write("data: ready\n\n");
198
495
  clients.add(response);
@@ -296,6 +593,7 @@ async function serveDeck(entry, port = 4173) {
296
593
  closed = true;
297
594
  clearTimeout(timer);
298
595
  watcher.close();
596
+ await chat.close();
299
597
  await builder.close();
300
598
  for (const client of clients) client.end();
301
599
  await new Promise((done, reject) => server.close((cause) => cause ? reject(cause) : done()));
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":[],"sources":["../src/page.ts","../src/server.ts","../src/cli.ts"],"sourcesContent":["export const page = `<!doctype html>\n<html lang=\"en\">\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\">\n<title>Office Kit — PowerPoint preview</title>\n<style>\n*{box-sizing:border-box}\nbody{margin:0;height:100dvh;overflow:hidden;background:#e9ecf1;color:#202735;font:13px system-ui;display:grid;grid-template-rows:60px minmax(0,1fr) 42px}\nbutton,a,select{font:inherit;color:inherit}button,select,.download{border:1px solid #d4d9e2;border-radius:6px;background:#fff;padding:7px 12px;text-decoration:none;cursor:pointer}\nbutton:hover:not(:disabled),.download:hover{background:#f0f3f9}button:disabled{opacity:.4;cursor:default}\n:focus-visible{outline:2px solid #4967dd;outline-offset:3px}\nheader{display:flex;align-items:center;gap:16px;padding:0 20px;background:#fff;border-bottom:1px solid #d4d9e2;min-width:0}\n.brand{font-weight:700;font-size:17px;white-space:nowrap}.badge{font-size:11px;color:#616b7c;background:#f1f3f7;border-radius:4px;padding:3px 6px}\n#status{flex:1;color:#667085;min-width:0}#present{background:#293c73;color:#fff;border-color:#293c73}\n.workspace{display:grid;grid-template-columns:224px minmax(0,1fr);min-height:0}\n.filmstrip{background:#f7f8fa;border-right:1px solid #d4d9e2;overflow:auto;overscroll-behavior:contain;padding:16px 12px}\n.filmstrip h2{margin:0 0 12px 26px;text-transform:uppercase;letter-spacing:.1em;font-size:10px;color:#7c8596;font-weight:600}\n#thumbnails{display:flex;flex-direction:column;gap:12px;margin:0;padding:0;list-style:none}\n.thumbnail{display:flex;align-items:flex-start;gap:8px;width:100%;padding:4px 3px;border:0;background:transparent;border-radius:5px;text-align:left}\n.slide-number{width:18px;flex:none;text-align:right;font-size:11px;color:#737d8e;padding-top:5px}\n.thumbnail img{display:block;min-width:0;width:calc(100% - 26px);background:white;box-shadow:0 1px 4px #19212d18;border:2px solid transparent;border-radius:3px;aspect-ratio:var(--slide-ratio,16/9);object-fit:contain}\n.thumbnail[aria-current=\"true\"]{background:#e7edff}.thumbnail[aria-current=\"true\"] img{border-color:#4967dd}.thumbnail[aria-current=\"true\"] .slide-number{color:#3654c1;font-weight:700}\nmain{min-width:0;min-height:0;display:flex;flex-direction:column}\n#error{flex:none;max-height:30%;overflow:auto;background:#fff0ef;color:#922e25;margin:0;padding:16px 20px;white-space:pre-wrap;border-bottom:1px solid #e8bbb7}\n#error[hidden]{display:none}\n#stage{flex:1;min-height:0;overflow:auto;display:flex;padding:32px;overscroll-behavior:contain}\n#slide{position:relative;flex:none;margin:auto;background:white;box-shadow:0 3px 24px #19212d20;overflow:hidden}\n#slide iframe{position:absolute;inset:0;display:block;width:100%;height:100%;border:0;pointer-events:none}\n#empty{margin:auto;color:#737d8e}\nfooter{display:flex;align-items:center;gap:14px;padding:0 16px;background:#fff;border-top:1px solid #d4d9e2;font-size:12px}\n#count{min-width:90px}footer .hint{flex:1;color:#737d8e}footer button{padding:3px 10px}footer select{padding:3px 8px}\n#presentation-controls{display:none}\nbody.presenting{grid-template-rows:minmax(0,1fr);background:#111}\n.presenting header,.presenting footer,.presenting .filmstrip{display:none}\n.presenting .workspace{grid-template-columns:minmax(0,1fr)}.presenting #stage{padding:0;background:#111}.presenting #slide{box-shadow:none}\n.presenting #presentation-controls{display:flex;position:fixed;bottom:16px;left:50%;transform:translateX(-50%);align-items:center;gap:12px;background:#202735e8;color:white;padding:6px;border-radius:8px;opacity:0;transition:opacity .15s}\n.presenting #presentation-controls:hover,.presenting #presentation-controls:focus-within{opacity:1}\n#presentation-controls button{background:transparent;color:white;border-color:#5d6575}\n@media(max-width:700px){.workspace{grid-template-columns:140px minmax(0,1fr)}.filmstrip{padding:12px 5px}header{padding:0 12px;gap:10px}.badge,footer .hint{display:none}#stage{padding:16px}footer{gap:8px}#status{font-size:11px}.download{padding:7px 8px}}\n@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}\n</style>\n<header><span class=\"brand\">Office Kit</span><span class=\"badge\">Preview</span><span id=\"status\" role=\"status\">Building…</span><button id=\"present\" disabled>Present</button><a class=\"download\" href=\"/deck.pptx\">Download PPTX</a></header>\n<div class=\"workspace\">\n<nav class=\"filmstrip\" aria-label=\"Slides\"><h2>Slides</h2><ol id=\"thumbnails\"></ol></nav>\n<main aria-label=\"Slide viewer\"><pre id=\"error\" role=\"alert\" hidden></pre><div id=\"stage\" tabindex=\"-1\"><div id=\"empty\">Waiting for slides…</div><div id=\"slide\" hidden></div></div></main>\n</div>\n<footer><span id=\"count\" aria-live=\"polite\">No slides</span><span class=\"hint\">Changes appear automatically · View only</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>\n<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>\n<script>\nlet state={slides:[],error:null,aspectRatio:16/9},index=0,urls=[],presenting=false;\nlet displayedSvg, pendingFrame;\nconst byId=id=>document.getElementById(id);\nconst stage=byId('stage'),slide=byId('slide'),thumbnails=byId('thumbnails');\nfunction resize(){\n if(!state.slides.length)return;\n const style=getComputedStyle(stage);\n const width=Math.max(1,stage.clientWidth-parseFloat(style.paddingLeft)-parseFloat(style.paddingRight));\n const height=Math.max(1,stage.clientHeight-parseFloat(style.paddingTop)-parseFloat(style.paddingBottom));\n const ratio=state.aspectRatio;\n const zoom=byId('zoom').value;\n const slideWidth=presenting||zoom==='fit'?Math.min(width,height*ratio):1280*Number(zoom);\n slide.style.width=slideWidth+'px';slide.style.height=slideWidth/ratio+'px';\n}\nfunction selectSlide(next,focusThumbnail=false,reveal=true){\n index=Math.max(0,Math.min(next,state.slides.length-1));\n const count=state.slides.length?'Slide '+(index+1)+' of '+state.slides.length:'No slides';\n byId('count').textContent=count;byId('present-count').textContent=count;\n for(const id of ['prev','present-prev'])byId(id).disabled=index===0;\n for(const id of ['next','present-next'])byId(id).disabled=index>=state.slides.length-1;\n byId('present').disabled=!state.slides.length;\n byId('zoom').disabled=!state.slides.length;\n slide.hidden=!state.slides.length;byId('empty').hidden=!!state.slides.length;\n const svg=state.slides[index];\n if(svg!==displayedSvg){\n displayedSvg=svg;\n if(pendingFrame){pendingFrame.remove();pendingFrame=null;}\n if(svg){\n const frame=document.createElement('iframe');frame.sandbox='';frame.tabIndex=-1;\n frame.title='Slide '+(index+1);frame.style.visibility='hidden';\n pendingFrame=frame;\n frame.onload=()=>{\n if(pendingFrame!==frame)return;\n for(const child of Array.from(slide.children))if(child!==frame)child.remove();\n frame.style.visibility='visible';pendingFrame=null;\n };\n frame.srcdoc='<style>html,body{margin:0;width:100%;height:100%;overflow:hidden}svg{display:block;width:100%;height:100%}</style>'+svg;\n slide.append(frame);\n }else slide.replaceChildren();\n }\n for(const frame of slide.children)frame.title='Slide '+(index+1);\n for(const [position,item] of Array.from(thumbnails.children).entries()){\n const button=item.firstElementChild,selected=position===index;\n button.setAttribute('aria-current',String(selected));button.tabIndex=selected?0:-1;\n if(selected){if(reveal)button.scrollIntoView({block:'nearest'});if(focusThumbnail)button.focus({preventScroll:true});}\n }\n resize();\n}\nfunction update(updated){\n const focusedThumbnail=thumbnails.contains(document.activeElement);\n const previous=state;\n state=updated;\n byId('status').textContent=state.error?'Build failed · showing last successful output':state.building?'Updating…':state.slides.length+' slides · Live';\n byId('error').textContent=state.error||'';byId('error').hidden=!state.error;\n document.documentElement.style.setProperty('--slide-ratio',String(state.aspectRatio));\n for(let i=state.slides.length;i<urls.length;i++){\n URL.revokeObjectURL(urls[i]);thumbnails.lastElementChild.remove();\n }\n urls.length=state.slides.length;\n state.slides.forEach((svg,i)=>{\n if(svg===previous.slides[i])return;\n const oldUrl=urls[i];\n urls[i]=URL.createObjectURL(new Blob([svg],{type:'image/svg+xml'}));\n let item=thumbnails.children[i];\n if(!item){\n item=document.createElement('li');\n const button=document.createElement('button'),number=document.createElement('span'),image=document.createElement('img');\n button.className='thumbnail';button.setAttribute('aria-label','Slide '+(i+1));button.onclick=()=>selectSlide(i,true);\n number.className='slide-number';number.textContent=String(i+1);\n image.alt='';image.draggable=false;image.loading='lazy';\n button.append(number,image);item.append(button);thumbnails.append(item);\n }\n item.querySelector('img').src=urls[i];\n if(oldUrl)URL.revokeObjectURL(oldUrl);\n });\n selectSlide(index,focusedThumbnail,false);\n if(!state.slides.length&&presenting)void exitPresentation();\n}\nfunction setPresenting(value){\n presenting=value;document.body.classList.toggle('presenting',value);resize();\n if(value)stage.focus();else{\n byId('present').focus();\n thumbnails.children[index]?.firstElementChild.scrollIntoView({block:'nearest'});\n }\n}\nasync function exitPresentation(){\n setPresenting(false);\n if(document.fullscreenElement)await document.exitFullscreen();\n}\nbyId('present').onclick=async()=>{\n setPresenting(true);\n try{await document.documentElement.requestFullscreen();}\n catch{byId('exit-present').textContent='Exit view · Esc';}\n};\nbyId('exit-present').onclick=exitPresentation;\ndocument.addEventListener('fullscreenchange',()=>{if(!document.fullscreenElement&&presenting)setPresenting(false);});\nfor(const id of ['prev','present-prev'])byId(id).onclick=()=>selectSlide(index-1);\nfor(const id of ['next','present-next'])byId(id).onclick=()=>selectSlide(index+1);\nbyId('zoom').onchange=resize;\nstage.onclick=()=>{if(presenting)selectSlide(index+1);};\ndocument.addEventListener('keydown',event=>{\n if(event.key==='Escape'&&presenting){event.preventDefault();void exitPresentation();return;}\n if(event.altKey||event.ctrlKey||event.metaKey||event.target.closest('select,input,textarea,[contenteditable]'))return;\n let next=index;\n if(['ArrowLeft','ArrowUp','PageUp'].includes(event.key))next--;\n else if(['ArrowRight','ArrowDown','PageDown'].includes(event.key))next++;\n else if(event.key==='Home')next=0;\n else if(event.key==='End')next=state.slides.length-1;\n else if(event.key===' '&&presenting&&!event.target.closest('button,a'))next+=event.shiftKey?-1:1;\n else return;\n event.preventDefault();selectSlide(next,thumbnails.contains(document.activeElement));\n});\nnew ResizeObserver(resize).observe(stage);\nlet refreshId=0;\nasync function refresh(){\n const id=++refreshId;\n try{const response=await fetch('/state'+(state.revision===undefined?'':'?since='+state.revision));if(!response.ok)throw new Error('Preview unavailable');const updated=await response.json();if(id===refreshId){if(updated.changes){updated.slides=state.slides.slice(0,updated.count);updated.slides.length=updated.count;for(const [position,svg] of Object.entries(updated.changes))updated.slides[Number(position)]=svg;}update(updated);}}\n catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}\n}\nconst events=new EventSource('/events');events.onmessage=event=>{if(event.data==='ready')delete state.revision;void refresh();};events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();\n</script></html>`;\n","import { createServer, type ServerResponse } from 'node:http';\nimport { watch } from 'node:fs';\nimport { dirname, resolve, sep } from 'node:path';\nimport type { BuildResult } from './build.ts';\nimport { createDeckBuilder } from './build-runner.ts';\nimport { page } from './page.ts';\n\nexport async function serveDeck(entry: string, port = 4173) {\n let latest: BuildResult | undefined;\n let error: string | null = null;\n let building = false;\n let revision = 0;\n let generation = 0;\n let patch: Record<number, string> = {};\n let pending = false;\n let closed = false;\n const clients = new Set<ServerResponse>();\n const server = createServer((request, response) => {\n const host = request.headers.host;\n if (host !== `127.0.0.1:${actualPort}` && host !== `localhost:${actualPort}`) {\n response.writeHead(403).end();\n return;\n }\n response.setHeader('Cache-Control', 'no-store');\n if (request.url === '/events') {\n response.writeHead(200, { 'Content-Type': 'text/event-stream' });\n response.write('data: ready\\n\\n');\n clients.add(response);\n request.on('close', () => clients.delete(response));\n } else if (request.url === '/state' || request.url?.startsWith('/state?')) {\n const since = new URL(request.url, 'http://localhost').searchParams.get('since');\n const incremental =\n since !== null && (since === String(revision) || since === String(revision - 1));\n response.writeHead(200, { 'Content-Type': 'application/json' });\n response.end(\n JSON.stringify({\n revision,\n building,\n ...(incremental\n ? {\n changes: since === String(revision) ? {} : patch,\n count: latest?.slides.length ?? 0,\n }\n : { slides: latest?.slides ?? [] }),\n aspectRatio: latest?.aspectRatio ?? 16 / 9,\n error,\n diagnostics: latest?.diagnostics ?? [],\n }),\n );\n } else if (request.url === '/deck.pptx' && latest) {\n response.writeHead(200, {\n 'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n 'Content-Disposition': 'attachment; filename=\"deck.pptx\"',\n });\n response.end(latest.bytes);\n } else if (request.url === '/') {\n response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n response.end(page);\n } else {\n response.writeHead(404).end();\n }\n });\n async function rebuild() {\n if (closed) return;\n if (building) {\n pending = true;\n return;\n }\n building = true;\n const started = generation;\n for (const client of clients) client.write('data: building\\n\\n');\n try {\n const result = await builder.build();\n if (started === generation && !closed) {\n patch = {};\n result.slides.forEach((svg, index) => {\n if (svg !== latest?.slides[index]) patch[index] = svg;\n });\n latest = result;\n revision++;\n error = null;\n }\n } catch (cause) {\n if (started === generation)\n error = cause instanceof Error ? (cause.stack ?? cause.message) : String(cause);\n } finally {\n building = false;\n for (const client of clients) client.write('data: updated\\n\\n');\n if (pending && !timer) {\n pending = false;\n void rebuild();\n }\n }\n }\n let timer: ReturnType<typeof setTimeout> | undefined;\n const root = dirname(resolve(entry));\n const watcher = watch(root, { recursive: true }, (_, filename) => {\n if (\n !filename ||\n filename\n .split(sep)\n .some((part) => ['node_modules', '.git', 'dist', '.office-kit'].includes(part))\n )\n return;\n if (!/\\.([cm]?[jt]sx?|json|pptx|png|jpe?g|gif|bmp|tiff?|emf|wmf|svg)$/i.test(filename)) return;\n generation++;\n void builder.cancel();\n clearTimeout(timer);\n timer = setTimeout(() => {\n timer = undefined;\n pending = false;\n void rebuild();\n }, 30);\n });\n const builder = createDeckBuilder(entry);\n let actualPort = port;\n try {\n await new Promise<void>((resolveListen, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => {\n server.off('error', reject);\n resolveListen();\n });\n });\n } catch (cause) {\n watcher.close();\n await builder.close();\n throw cause;\n }\n const address = server.address();\n if (address && typeof address !== 'string') actualPort = address.port;\n await rebuild();\n return {\n url: `http://127.0.0.1:${actualPort}`,\n async close() {\n closed = true;\n clearTimeout(timer);\n watcher.close();\n await builder.close();\n for (const client of clients) client.end();\n await new Promise<void>((done, reject) =>\n server.close((cause) => (cause ? reject(cause) : done())),\n );\n },\n };\n}\n","#!/usr/bin/env node\nimport { resolve } from 'node:path';\nimport { exportDeck } from './index.ts';\nimport { serveDeck } from './server.ts';\nimport { initProject } from './init.ts';\nimport { inspectTemplate } from './inspect.ts';\n\nconst usage = `Usage: office-pptx init <new-directory>\n office-pptx dev <deck.tsx> [--port 4173]\n office-pptx build <deck.tsx> [--out deck.pptx]\n office-pptx inspect <template.pptx>`;\nconst [command, entry, ...args] = process.argv.slice(2);\ntry {\n if (command === '--help' || command === '-h') {\n console.log(usage);\n } else {\n if (!entry) throw new Error(usage);\n const option = command === 'build' ? '--out' : command === 'dev' ? '--port' : undefined;\n if (args.length && (args.length !== 2 || !option || args[0] !== option || !args[1])) {\n throw new Error(`Unexpected arguments: ${args.join(' ')}\\n${usage}`);\n }\n if (command === 'init') {\n const directory = await initProject(entry);\n console.log(`Created ${directory}\\nRun npm install, then npm run dev inside that directory.`);\n } else if (command === 'inspect') {\n console.log(JSON.stringify(await inspectTemplate(entry), null, 2));\n } else if (command === 'build') {\n const output = resolve(args[1] ?? 'deck.pptx');\n const result = await exportDeck(entry, output);\n console.log(`Wrote ${output} (${result.slides.length} slides)`);\n } else if (command === 'dev') {\n const port = args[1] === undefined ? 4173 : Number(args[1]);\n if (!Number.isInteger(port) || port < 0 || port > 65535)\n throw new Error('Port must be an integer between 0 and 65535.');\n const server = await serveDeck(entry, port);\n console.log(`Preview: ${server.url}`);\n const stop = () => {\n void server.close().then(() => process.exit(0));\n };\n process.once('SIGINT', stop);\n process.once('SIGTERM', stop);\n } else {\n throw new Error(usage);\n }\n }\n} catch (cause) {\n console.error(cause instanceof Error ? cause.message : cause);\n process.exitCode = 1;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/page.ts","../src/chat.ts","../src/server.ts","../src/cli.ts"],"sourcesContent":["export const page = `<!doctype html>\n<html lang=\"en\">\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\">\n<title>Office Kit — PowerPoint preview</title>\n<style>\n*{box-sizing:border-box}\nbody{margin:0;height:100dvh;overflow:hidden;background:#e9ecf1;color:#202735;font:13px system-ui;display:grid;grid-template-rows:60px minmax(0,1fr) 42px}\nbutton,a,select{font:inherit;color:inherit}button,select,.download{border:1px solid #d4d9e2;border-radius:6px;background:#fff;padding:7px 12px;text-decoration:none;cursor:pointer}\nbutton:hover:not(:disabled),.download:hover{background:#f0f3f9}button:disabled{opacity:.4;cursor:default}\n:focus-visible{outline:2px solid #4967dd;outline-offset:3px}\nheader{display:flex;align-items:center;gap:16px;padding:0 20px;background:#fff;border-bottom:1px solid #d4d9e2;min-width:0}\n.brand{font-weight:700;font-size:17px;white-space:nowrap}.badge{font-size:11px;color:#616b7c;background:#f1f3f7;border-radius:4px;padding:3px 6px}\n#status{flex:1;color:#667085;min-width:0}#present{background:#293c73;color:#fff;border-color:#293c73}\n.workspace{display:grid;grid-template-columns:190px minmax(0,1fr) 340px;min-height:0}\n.filmstrip{background:#f7f8fa;border-right:1px solid #d4d9e2;overflow:auto;overscroll-behavior:contain;padding:16px 12px}\n.filmstrip h2{margin:0 0 12px 26px;text-transform:uppercase;letter-spacing:.1em;font-size:10px;color:#7c8596;font-weight:600}\n#thumbnails{display:flex;flex-direction:column;gap:12px;margin:0;padding:0;list-style:none}\n.thumbnail{display:flex;align-items:flex-start;gap:8px;width:100%;padding:4px 3px;border:0;background:transparent;border-radius:5px;text-align:left}\n.slide-number{width:18px;flex:none;text-align:right;font-size:11px;color:#737d8e;padding-top:5px}\n.thumbnail img{display:block;min-width:0;width:calc(100% - 26px);background:white;box-shadow:0 1px 4px #19212d18;border:2px solid transparent;border-radius:3px;aspect-ratio:var(--slide-ratio,16/9);object-fit:contain}\n.thumbnail[aria-current=\"true\"]{background:#e7edff}.thumbnail[aria-current=\"true\"] img{border-color:#4967dd}.thumbnail[aria-current=\"true\"] .slide-number{color:#3654c1;font-weight:700}\nmain{min-width:0;min-height:0;display:flex;flex-direction:column}\n#error{flex:none;max-height:30%;overflow:auto;background:#fff0ef;color:#922e25;margin:0;padding:16px 20px;white-space:pre-wrap;border-bottom:1px solid #e8bbb7}\n#error[hidden]{display:none}\n#stage{flex:1;min-height:0;overflow:auto;display:flex;padding:32px;overscroll-behavior:contain}\n#slide{position:relative;flex:none;margin:auto;background:white;box-shadow:0 3px 24px #19212d20;overflow:hidden}\n#slide{user-select:text;-webkit-user-select:text}\n#empty{margin:auto;color:#737d8e}\nfooter{display:flex;align-items:center;gap:14px;padding:0 16px;background:#fff;border-top:1px solid #d4d9e2;font-size:12px}\n#count{min-width:90px}footer .hint{flex:1;color:#737d8e}footer button{padding:3px 10px}footer select{padding:3px 8px}\n#presentation-controls{display:none}\nbody.presenting{grid-template-rows:minmax(0,1fr);background:#111}\n.presenting header,.presenting footer,.presenting .filmstrip,.presenting #chat{display:none}\n.presenting .workspace{grid-template-columns:minmax(0,1fr)}.presenting #stage{padding:0;background:#111}.presenting #slide{box-shadow:none}\n.presenting #presentation-controls{display:flex;position:fixed;bottom:16px;left:50%;transform:translateX(-50%);align-items:center;gap:12px;background:#202735e8;color:white;padding:6px;border-radius:8px;opacity:0;transition:opacity .15s}\n.presenting #presentation-controls:hover,.presenting #presentation-controls:focus-within{opacity:1}\n#presentation-controls button{background:transparent;color:white;border-color:#5d6575}\n@media(max-width:700px){.workspace{grid-template-columns:140px minmax(0,1fr)}.filmstrip{padding:12px 5px}header{padding:0 12px;gap:10px}.badge,footer .hint{display:none}#stage{padding:16px}footer{gap:8px}#status{font-size:11px}.download{padding:7px 8px}}\n#chat{min-height:0;display:flex;flex-direction:column;background:#fff;border-left:1px solid #d4d9e2}\n.chat-heading{display:flex;align-items:center;gap:8px;padding:16px;border-bottom:1px solid #e6e9ef}.chat-heading strong{flex:1}.chat-heading button{padding:5px 8px}\n#messages{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:16px;overscroll-behavior:contain}\n.chat-message{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.6}.chat-message.user{background:#eef2ff;border-radius:8px;padding:10px 12px}.chat-message small{display:block;color:#697386;font-size:11px;margin-bottom:4px}.chat-intro{color:#737d8e;line-height:1.7}\n#chat-form{padding:14px;border-top:1px solid #e6e9ef}#chat-context{font-size:11px;color:#4967dd;margin-bottom:8px}#chat-input{font:inherit;resize:vertical;min-height:90px;max-height:200px;width:100%;padding:10px;border:1px solid #d4d9e2;border-radius:8px} .chat-actions{display:flex;gap:8px;margin-top:8px;align-items:center}.chat-actions select{min-width:0;flex:1}#chat-send{background:#293c73;color:white}#chat-status{padding:0 14px 12px;color:#667085;font-size:11px;white-space:pre-wrap;max-height:90px;overflow:auto}\nbody.chat-hidden .workspace{grid-template-columns:190px minmax(0,1fr)}body.chat-hidden #chat{display:none}\nbody.presenting .workspace{grid-template-columns:minmax(0,1fr)}\n@media(max-width:1000px){.workspace{grid-template-columns:120px minmax(0,1fr) 300px}.filmstrip{padding:12px 5px}#stage{padding:16px}}\n@media(max-width:700px){.workspace{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(160px,1fr) minmax(200px,1fr)}.filmstrip{display:none}#chat{border-top:1px solid #d4d9e2}.chat-heading{padding:8px 12px}#chat-form{padding:8px 12px}#chat-input{min-height:50px}body.chat-hidden .workspace,.presenting .workspace{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}.brand{font-size:14px}header{gap:6px}.download{font-size:11px}}\n@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}\n</style>\n<header><span class=\"brand\">Office Kit</span><span class=\"badge\">Preview</span><span id=\"status\" role=\"status\">Building…</span><button id=\"toggle-chat\" aria-expanded=\"true\" aria-controls=\"chat\">Chat</button><button id=\"present\" disabled>Present</button><a class=\"download\" href=\"/deck.pptx\">Download PPTX</a></header>\n<div class=\"workspace\">\n<nav class=\"filmstrip\" aria-label=\"Slides\"><h2>Slides</h2><ol id=\"thumbnails\"></ol></nav>\n<main aria-label=\"Slide viewer\"><pre id=\"error\" role=\"alert\" hidden></pre><div id=\"stage\" tabindex=\"-1\"><div id=\"empty\">Waiting for slides…</div><div id=\"slide\" hidden></div></div></main>\n<aside id=\"chat\" aria-label=\"Slide chat\">\n<div class=\"chat-heading\"><strong>Edit with AI</strong><button id=\"chat-reset\">New chat</button></div>\n<div id=\"messages\" role=\"log\" aria-label=\"Conversation\"><p class=\"chat-intro\">Describe what you want to change. The current slide is included automatically. You can also ask about other slides or the whole deck.<br><br>Uses your locally installed Claude Code or Codex. Sign in through its CLI first. Sending a message allows it to edit this project.</p></div>\n<form id=\"chat-form\"><div id=\"chat-context\">No slide selected</div><label for=\"chat-input\" class=\"chat-intro\">What would you like to change?</label><textarea id=\"chat-input\" maxlength=\"16000\" placeholder=\"Make this headline shorter…\" required></textarea><div class=\"chat-actions\"><select id=\"chat-provider\" aria-label=\"AI provider\"><option value=\"claude\">Claude Code</option><option value=\"codex\">Codex</option></select><button id=\"chat-stop\" type=\"button\" hidden>Stop</button><button id=\"chat-send\" type=\"submit\">Send</button></div></form>\n<div id=\"chat-status\" role=\"status\">Ready</div>\n</aside>\n</div>\n<footer><span id=\"count\" aria-live=\"polite\">No slides</span><span class=\"hint\">Changes appear automatically · Text can be selected and copied</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>\n<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>\n<script>\nlet state={slides:[],error:null,aspectRatio:16/9},index=0,urls=[],presenting=false;\nlet displayedSvg;\nconst byId=id=>document.getElementById(id);\nconst stage=byId('stage'),slide=byId('slide'),thumbnails=byId('thumbnails');\n// The slide lives in a shadow root rather than a sandboxed iframe so its text\n// (XHTML inside <foreignObject>) can be selected and copied while the deck's\n// SVG stays out of the viewer's own DOM and CSS. Keyboard events still reach the\n// document, so arrow-key navigation keeps working after clicking into a slide.\nconst canvas=slide.attachShadow({mode:'open'});\nfunction resize(){\n if(!state.slides.length)return;\n const style=getComputedStyle(stage);\n const width=Math.max(1,stage.clientWidth-parseFloat(style.paddingLeft)-parseFloat(style.paddingRight));\n const height=Math.max(1,stage.clientHeight-parseFloat(style.paddingTop)-parseFloat(style.paddingBottom));\n const ratio=state.aspectRatio;\n const zoom=byId('zoom').value;\n const slideWidth=presenting||zoom==='fit'?Math.min(width,height*ratio):1280*Number(zoom);\n slide.style.width=slideWidth+'px';slide.style.height=slideWidth/ratio+'px';\n}\nfunction selectSlide(next,focusThumbnail=false,reveal=true){\n index=Math.max(0,Math.min(next,state.slides.length-1));\n const count=state.slides.length?'Slide '+(index+1)+' of '+state.slides.length:'No slides';\n byId('chat-context').textContent=state.slides.length?'Context: '+count:'Context: whole project';\n byId('count').textContent=count;byId('present-count').textContent=count;\n for(const id of ['prev','present-prev'])byId(id).disabled=index===0;\n for(const id of ['next','present-next'])byId(id).disabled=index>=state.slides.length-1;\n byId('present').disabled=!state.slides.length;\n byId('zoom').disabled=!state.slides.length;\n slide.hidden=!state.slides.length;byId('empty').hidden=!!state.slides.length;\n const svg=state.slides[index];\n if(svg!==displayedSvg){\n displayedSvg=svg;\n canvas.innerHTML=svg?'<style>svg{display:block;width:100%;height:100%}</style>'+svg:'';\n }\n slide.setAttribute('aria-label','Slide '+(index+1));\n for(const [position,item] of Array.from(thumbnails.children).entries()){\n const button=item.firstElementChild,selected=position===index;\n button.setAttribute('aria-current',String(selected));button.tabIndex=selected?0:-1;\n if(selected){if(reveal)button.scrollIntoView({block:'nearest'});if(focusThumbnail)button.focus({preventScroll:true});}\n }\n resize();\n}\nfunction update(updated){\n const focusedThumbnail=thumbnails.contains(document.activeElement);\n const previous=state;\n state=updated;\n byId('status').textContent=state.error?'Build failed · showing last successful output':state.building?'Updating…':state.slides.length+' slides · Live';\n byId('error').textContent=state.error||'';byId('error').hidden=!state.error;\n document.documentElement.style.setProperty('--slide-ratio',String(state.aspectRatio));\n for(let i=state.slides.length;i<urls.length;i++){\n URL.revokeObjectURL(urls[i]);thumbnails.lastElementChild.remove();\n }\n urls.length=state.slides.length;\n state.slides.forEach((svg,i)=>{\n if(svg===previous.slides[i])return;\n const oldUrl=urls[i];\n urls[i]=URL.createObjectURL(new Blob([svg],{type:'image/svg+xml'}));\n let item=thumbnails.children[i];\n if(!item){\n item=document.createElement('li');\n const button=document.createElement('button'),number=document.createElement('span'),image=document.createElement('img');\n button.className='thumbnail';button.setAttribute('aria-label','Slide '+(i+1));button.onclick=()=>selectSlide(i,true);\n number.className='slide-number';number.textContent=String(i+1);\n image.alt='';image.draggable=false;image.loading='lazy';\n button.append(number,image);item.append(button);thumbnails.append(item);\n }\n item.querySelector('img').src=urls[i];\n if(oldUrl)URL.revokeObjectURL(oldUrl);\n });\n selectSlide(index,focusedThumbnail,false);\n if(!state.slides.length&&presenting)void exitPresentation();\n}\nfunction setPresenting(value){\n presenting=value;document.body.classList.toggle('presenting',value);resize();\n if(value)stage.focus();else{\n byId('present').focus();\n thumbnails.children[index]?.firstElementChild.scrollIntoView({block:'nearest'});\n }\n}\nasync function exitPresentation(){\n setPresenting(false);\n if(document.fullscreenElement)await document.exitFullscreen();\n}\nbyId('present').onclick=async()=>{\n setPresenting(true);\n try{await document.documentElement.requestFullscreen();}\n catch{byId('exit-present').textContent='Exit view · Esc';}\n};\nbyId('exit-present').onclick=exitPresentation;\ndocument.addEventListener('fullscreenchange',()=>{if(!document.fullscreenElement&&presenting)setPresenting(false);});\nfor(const id of ['prev','present-prev'])byId(id).onclick=()=>selectSlide(index-1);\nfor(const id of ['next','present-next'])byId(id).onclick=()=>selectSlide(index+1);\nbyId('zoom').onchange=resize;\nstage.onclick=()=>{if(presenting&&!getSelection().toString())selectSlide(index+1);};\ndocument.addEventListener('keydown',event=>{\n if(event.key==='Escape'&&presenting){event.preventDefault();void exitPresentation();return;}\n if(event.altKey||event.ctrlKey||event.metaKey||event.target.closest('select,input,textarea,[contenteditable]'))return;\n let next=index;\n if(['ArrowLeft','ArrowUp','PageUp'].includes(event.key))next--;\n else if(['ArrowRight','ArrowDown','PageDown'].includes(event.key))next++;\n else if(event.key==='Home')next=0;\n else if(event.key==='End')next=state.slides.length-1;\n else if(event.key===' '&&presenting&&!event.target.closest('button,a'))next+=event.shiftKey?-1:1;\n else return;\n event.preventDefault();selectSlide(next,thumbnails.contains(document.activeElement));\n});\nnew ResizeObserver(resize).observe(stage);\nlet refreshId=0;\nasync function refresh(){\n const id=++refreshId;\n try{const response=await fetch('/state'+(state.revision===undefined?'':'?since='+state.revision));if(!response.ok)throw new Error('Preview unavailable');const updated=await response.json();if(id===refreshId){if(updated.changes){updated.slides=state.slides.slice(0,updated.count);updated.slides.length=updated.count;for(const [position,svg] of Object.entries(updated.changes))updated.slides[Number(position)]=svg;}update(updated);}}\n catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}\n}\nlet chatRequest=0,chatBusy=false,lastMessages='';\nfunction renderChat(chat){\n chatBusy=chat.running;\n byId('chat-send').disabled=chat.running;\n byId('chat-reset').disabled=chat.running;\n byId('chat-provider').disabled=chat.running;\n byId('chat-stop').hidden=!chat.running;\n byId('chat-status').textContent=chat.status;\n if(chat.messages.length)byId('chat-provider').value=chat.provider;\n const serialized=JSON.stringify(chat.messages);\n if(serialized===lastMessages)return;\n lastMessages=serialized;\n const log=byId('messages'),atBottom=log.scrollHeight-log.scrollTop-log.clientHeight<60;\n log.replaceChildren();\n if(!chat.messages.length){const intro=document.createElement('p');intro.className='chat-intro';intro.textContent='Ask for a focused slide edit or a change across the deck. Uses your locally installed CLI and its login. Sending allows edits to this project.';log.append(intro);}\n for(const message of chat.messages){\n const item=document.createElement('div'),label=document.createElement('small'),body=document.createElement('div');\n item.className='chat-message '+message.role;\n label.textContent=message.role==='user'?'You · '+(message.context?.slide?'Slide '+message.context.slide:'Whole project'):'Assistant';\n body.textContent=message.text||(chat.running?'Working…':'No response');item.append(label,body);log.append(item);\n }\n if(atBottom)log.scrollTop=log.scrollHeight;\n}\nasync function refreshChat(){\n const id=++chatRequest;\n try{const response=await fetch('/chat');if(!response.ok)throw new Error('Chat unavailable');const chat=await response.json();if(id===chatRequest)renderChat(chat);}\n catch(error){if(id===chatRequest)byId('chat-status').textContent=error.message;}\n}\nasync function chatAction(path,payload){\n ++chatRequest;\n const response=await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});\n const result=await response.json();if(!response.ok)throw new Error(result.error||'Chat request failed');\n await refreshChat();\n}\nbyId('chat-form').onsubmit=async event=>{\n event.preventDefault();if(chatBusy)return;\n const input=byId('chat-input'),message=input.value.trim();if(!message)return;\n chatBusy=true;byId('chat-send').disabled=true;\n try{await chatAction('/chat',{message,provider:byId('chat-provider').value,slide:state.slides.length?index:null,revision:state.revision??0});if(input.value.trim()===message)input.value='';}\n catch(error){byId('chat-status').textContent=error.message;chatBusy=false;byId('chat-send').disabled=false;}\n};\nbyId('chat-input').onkeydown=event=>{if(event.key==='Enter'&&(event.metaKey||event.ctrlKey)&&!event.isComposing){event.preventDefault();byId('chat-form').requestSubmit();}};\nfor(const action of ['stop','reset'])byId('chat-'+action).onclick=()=>chatAction('/chat/'+action,{}).catch(error=>{byId('chat-status').textContent=error.message;});\nbyId('toggle-chat').onclick=()=>{const hidden=document.body.classList.toggle('chat-hidden');byId('toggle-chat').setAttribute('aria-expanded',String(!hidden));resize();};\nvoid refreshChat();\nconst events=new EventSource('/events');events.onmessage=event=>{if(event.data==='chat'){void refreshChat();return;}if(event.data==='ready'){delete state.revision;void refreshChat();}void refresh();};events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();\n</script></html>`;\n","import { spawn, type ChildProcess } from 'node:child_process';\nimport { dirname, resolve } from 'node:path';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\n\nexport type Provider = 'claude' | 'codex';\nexport interface FocusContext {\n slide: number | null;\n count: number;\n revision: number;\n text: string;\n entry: string;\n files: string[];\n buildError: string | null;\n}\ninterface Message {\n role: 'user' | 'assistant';\n text: string;\n context?: FocusContext;\n}\n\n/** One shared conversation and one editing process per dev server. */\nexport function createChat(entry: string, notify: () => void) {\n let messages: Message[] = [];\n let provider: Provider = 'claude';\n let child: ChildProcess | undefined;\n let status = 'Ready';\n let stopping = false;\n let killTimer: ReturnType<typeof setTimeout> | undefined;\n let completion: Promise<void> = Promise.resolve();\n const snapshot = () => ({ messages, provider, running: !!child, status });\n function stop() {\n if (!child || stopping) return;\n stopping = true;\n status = 'Stopping…';\n const processToStop = child;\n const kill = (signal: NodeJS.Signals) => {\n try {\n if (process.platform !== 'win32' && processToStop.pid)\n process.kill(-processToStop.pid, signal);\n else processToStop.kill(signal);\n } catch {\n /* Already exited. */\n }\n };\n kill('SIGTERM');\n killTimer = setTimeout(() => kill('SIGKILL'), 2000);\n notify();\n }\n function start(text: string, selected: Provider, context: FocusContext) {\n provider = selected;\n const user: Message = { role: 'user', text, context };\n // Keep recent conversational intent, including each turn's original focus.\n const history = messages.slice(-12).map((message) => ({\n ...message,\n text: message.text.slice(-12000),\n }));\n const prompt = `You are editing a local PowerPoint TSX project in ${dirname(resolve(entry))}.\nRead the project's authoring instructions. Make focused patches to the TSX source, preserving unrelated work.\nThe preview rebuilds automatically on save; do not start another dev server.\nThe 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.\nSlide 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.\nRespond in the user's language, briefly describing edits or answering their question. If permissions prevent an action, explain that instead of claiming success.\nRecent conversation (JSON): ${JSON.stringify(history)}\nCurrent request and preview context (JSON): ${JSON.stringify(user)}`;\n messages.push(user);\n const answer: Message = { role: 'assistant', text: '' };\n messages.push(answer);\n if (messages.length > 100) messages = messages.slice(-100);\n status = 'Working…';\n stopping = false;\n const args =\n selected === 'codex'\n ? ['exec', '--json', '--sandbox', 'workspace-write', '--skip-git-repo-check', '-']\n : [\n '--print',\n '--verbose',\n '--output-format',\n 'stream-json',\n '--permission-mode',\n 'acceptEdits',\n '--tools',\n 'Read,Edit,Write,Glob,Grep',\n ];\n child = spawn(selected, args, {\n cwd: dirname(resolve(entry)),\n stdio: ['pipe', 'pipe', 'pipe'],\n detached: process.platform !== 'win32',\n shell: false,\n });\n const running = child;\n let buffer = '',\n stderr = '',\n failure = '',\n sawResult = false;\n function append(value: string) {\n answer.text = (answer.text + (answer.text ? '\\n\\n' : '') + value).slice(-100000);\n notify();\n }\n function consume(line: string) {\n if (!line.trim()) return;\n try {\n const event = JSON.parse(line);\n if (selected === 'codex') {\n if (event.type === 'item.completed' && event.item?.type === 'agent_message')\n append(event.item.text ?? '');\n if (event.type === 'item.started') {\n status = 'Editing / inspecting…';\n notify();\n }\n if (event.type === 'turn.completed') sawResult = true;\n if (event.type === 'turn.failed' || event.type === 'error')\n failure = event.error?.message ?? event.message ?? 'Agent failed';\n } else {\n if (event.type === 'assistant') {\n for (const block of event.message?.content ?? []) {\n if (block.type === 'text') append(block.text);\n if (block.type === 'tool_use') {\n status = 'Using ' + block.name + '…';\n notify();\n }\n }\n }\n if (event.type === 'result') {\n sawResult = true;\n if (event.is_error)\n failure = event.result || event.errors?.join('\\n') || 'Agent failed';\n else if (!answer.text && event.result) append(event.result);\n if (event.permission_denials?.length)\n failure = 'Some actions were denied by CLI permissions. ' + (event.result ?? '');\n }\n }\n } catch {\n failure = 'Unexpected CLI output. Update your CLI and try again.';\n }\n }\n running.stdout!.setEncoding('utf8');\n running.stdout!.on('data', (chunk: string) => {\n buffer += chunk;\n let end: number;\n while ((end = buffer.indexOf('\\n')) !== -1) {\n consume(buffer.slice(0, end));\n buffer = buffer.slice(end + 1);\n }\n if (buffer.length > 2_000_000) {\n failure = 'CLI output exceeded the limit.';\n stop();\n }\n });\n running.stderr!.setEncoding('utf8');\n running.stderr!.on('data', (chunk: string) => {\n stderr = (stderr + chunk).slice(-8000);\n });\n running.stdin!.on('error', () => {\n /* Early process exit is handled below. */\n });\n running.stdin!.end(prompt);\n completion = new Promise<void>((done) => {\n running.on('error', (error) => {\n failure = `Could not run ${selected}. Install it and log in from your terminal first. ${error.message}`;\n });\n running.on('close', (code) => {\n if (buffer.trim()) consume(buffer);\n clearTimeout(killTimer);\n status = stopping\n ? 'Stopped. Saved edits remain in the preview.'\n : failure ||\n (code !== 0\n ? stderr || `${selected} exited with code ${code}`\n : !sawResult\n ? 'CLI exited without a completed result.'\n : 'Done');\n if (status !== 'Done') append(status);\n child = undefined;\n notify();\n done();\n });\n });\n notify();\n }\n async function handle(\n request: IncomingMessage,\n response: ServerResponse,\n focus: (index: number | null, revision: number) => FocusContext,\n ) {\n const json = (code: number, value: unknown) => {\n response.writeHead(code, { 'Content-Type': 'application/json' });\n response.end(JSON.stringify(value));\n };\n if (request.method === 'GET' && request.url === '/chat') {\n json(200, snapshot());\n return;\n }\n if (request.method !== 'POST') {\n json(405, { error: 'Method not allowed' });\n return;\n }\n // Same-origin JSON prevents other sites from invoking local editing tools.\n if (\n request.headers.origin !== `http://${request.headers.host}` ||\n request.headers['content-type']?.split(';')[0] !== 'application/json'\n ) {\n json(403, { error: 'Same-origin JSON required' });\n return;\n }\n try {\n let body = '';\n request.setEncoding('utf8');\n for await (const chunk of request) {\n body += chunk;\n if (Buffer.byteLength(body) > 32000) {\n json(413, { error: 'Message too large' });\n return;\n }\n }\n const value = JSON.parse(body);\n if (request.url === '/chat/stop') {\n stop();\n json(200, snapshot());\n return;\n }\n if (child) {\n json(409, { error: 'An edit is already running' });\n return;\n }\n if (request.url === '/chat/reset') {\n messages = [];\n status = 'Ready';\n notify();\n json(200, snapshot());\n return;\n }\n if (request.url !== '/chat') {\n json(404, { error: 'Not found' });\n return;\n }\n if (\n typeof value.message !== 'string' ||\n !value.message.trim() ||\n value.message.length > 16000 ||\n !['claude', 'codex'].includes(value.provider) ||\n !(value.slide === null || (Number.isInteger(value.slide) && value.slide >= 0)) ||\n !Number.isInteger(value.revision)\n ) {\n json(400, { error: 'Invalid chat request' });\n return;\n }\n const context = focus(value.slide, value.revision);\n start(value.message.trim(), value.provider, context);\n json(202, snapshot());\n } catch (cause) {\n json(400, { error: cause instanceof Error ? cause.message : 'Invalid request' });\n }\n }\n return {\n handle,\n async close() {\n stop();\n await completion;\n },\n };\n}\n","import { createServer, type ServerResponse } from 'node:http';\nimport { watch } from 'node:fs';\nimport { dirname, resolve, sep } from 'node:path';\nimport type { BuildResult } from './build.ts';\nimport { createDeckBuilder } from './build-runner.ts';\nimport { page } from './page.ts';\nimport { createChat } from './chat.ts';\n\nexport async function serveDeck(entry: string, port = 4173) {\n let latest: BuildResult | undefined;\n let error: string | null = null;\n let building = false;\n let revision = 0;\n let generation = 0;\n let patch: Record<number, string> = {};\n let pending = false;\n let closed = false;\n const clients = new Set<ServerResponse>();\n const chat = createChat(entry, () => {\n for (const client of clients) client.write('data: chat\\n\\n');\n });\n const server = createServer((request, response) => {\n const host = request.headers.host;\n if (host !== `127.0.0.1:${actualPort}` && host !== `localhost:${actualPort}`) {\n response.writeHead(403).end();\n return;\n }\n response.setHeader('Cache-Control', 'no-store');\n if (request.url === '/chat' || request.url?.startsWith('/chat/')) {\n void chat.handle(request, response, (index, viewedRevision) => {\n if (viewedRevision !== revision)\n throw new Error('Preview changed. Review the current slide and send again.');\n if (index !== null && (!latest || index >= latest.slides.length))\n throw new Error('Slide no longer exists.');\n return {\n slide: index === null ? null : index + 1,\n count: latest?.slides.length ?? 0,\n revision,\n text: index === null ? '' : (latest?.slideTexts[index] ?? '').slice(0, 12000),\n entry: resolve(entry),\n files: latest?.dependencies ?? [resolve(entry)],\n buildError: error,\n };\n });\n } else if (request.method !== 'GET') {\n response.writeHead(405).end();\n } else if (request.url === '/events') {\n response.writeHead(200, { 'Content-Type': 'text/event-stream' });\n response.write('data: ready\\n\\n');\n clients.add(response);\n request.on('close', () => clients.delete(response));\n } else if (request.url === '/state' || request.url?.startsWith('/state?')) {\n const since = new URL(request.url, 'http://localhost').searchParams.get('since');\n const incremental =\n since !== null && (since === String(revision) || since === String(revision - 1));\n response.writeHead(200, { 'Content-Type': 'application/json' });\n response.end(\n JSON.stringify({\n revision,\n building,\n ...(incremental\n ? {\n changes: since === String(revision) ? {} : patch,\n count: latest?.slides.length ?? 0,\n }\n : { slides: latest?.slides ?? [] }),\n aspectRatio: latest?.aspectRatio ?? 16 / 9,\n error,\n diagnostics: latest?.diagnostics ?? [],\n }),\n );\n } else if (request.url === '/deck.pptx' && latest) {\n response.writeHead(200, {\n 'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n 'Content-Disposition': 'attachment; filename=\"deck.pptx\"',\n });\n response.end(latest.bytes);\n } else if (request.url === '/') {\n response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n response.end(page);\n } else {\n response.writeHead(404).end();\n }\n });\n async function rebuild() {\n if (closed) return;\n if (building) {\n pending = true;\n return;\n }\n building = true;\n const started = generation;\n for (const client of clients) client.write('data: building\\n\\n');\n try {\n const result = await builder.build();\n if (started === generation && !closed) {\n patch = {};\n result.slides.forEach((svg, index) => {\n if (svg !== latest?.slides[index]) patch[index] = svg;\n });\n latest = result;\n revision++;\n error = null;\n }\n } catch (cause) {\n if (started === generation)\n error = cause instanceof Error ? (cause.stack ?? cause.message) : String(cause);\n } finally {\n building = false;\n for (const client of clients) client.write('data: updated\\n\\n');\n if (pending && !timer) {\n pending = false;\n void rebuild();\n }\n }\n }\n let timer: ReturnType<typeof setTimeout> | undefined;\n const root = dirname(resolve(entry));\n const watcher = watch(root, { recursive: true }, (_, filename) => {\n if (\n !filename ||\n filename\n .split(sep)\n .some((part) => ['node_modules', '.git', 'dist', '.office-kit'].includes(part))\n )\n return;\n if (!/\\.([cm]?[jt]sx?|json|pptx|png|jpe?g|gif|bmp|tiff?|emf|wmf|svg)$/i.test(filename)) return;\n generation++;\n void builder.cancel();\n clearTimeout(timer);\n timer = setTimeout(() => {\n timer = undefined;\n pending = false;\n void rebuild();\n }, 30);\n });\n const builder = createDeckBuilder(entry);\n let actualPort = port;\n try {\n await new Promise<void>((resolveListen, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => {\n server.off('error', reject);\n resolveListen();\n });\n });\n } catch (cause) {\n watcher.close();\n await builder.close();\n throw cause;\n }\n const address = server.address();\n if (address && typeof address !== 'string') actualPort = address.port;\n await rebuild();\n return {\n url: `http://127.0.0.1:${actualPort}`,\n async close() {\n closed = true;\n clearTimeout(timer);\n watcher.close();\n await chat.close();\n await builder.close();\n for (const client of clients) client.end();\n await new Promise<void>((done, reject) =>\n server.close((cause) => (cause ? reject(cause) : done())),\n );\n },\n };\n}\n","#!/usr/bin/env node\nimport { resolve } from 'node:path';\nimport { exportDeck } from './index.ts';\nimport { serveDeck } from './server.ts';\nimport { initProject } from './init.ts';\nimport { inspectTemplate } from './inspect.ts';\n\nconst usage = `Usage: office-pptx init <new-directory>\n office-pptx dev <deck.tsx> [--port 4173]\n office-pptx build <deck.tsx> [--out deck.pptx]\n office-pptx inspect <template.pptx>`;\nconst [command, entry, ...args] = process.argv.slice(2);\ntry {\n if (command === '--help' || command === '-h') {\n console.log(usage);\n } else {\n if (!entry) throw new Error(usage);\n const option = command === 'build' ? '--out' : command === 'dev' ? '--port' : undefined;\n if (args.length && (args.length !== 2 || !option || args[0] !== option || !args[1])) {\n throw new Error(`Unexpected arguments: ${args.join(' ')}\\n${usage}`);\n }\n if (command === 'init') {\n const directory = await initProject(entry);\n console.log(`Created ${directory}\\nRun npm install, then npm run dev inside that directory.`);\n } else if (command === 'inspect') {\n console.log(JSON.stringify(await inspectTemplate(entry), null, 2));\n } else if (command === 'build') {\n const output = resolve(args[1] ?? 'deck.pptx');\n const result = await exportDeck(entry, output);\n console.log(`Wrote ${output} (${result.slides.length} slides)`);\n } else if (command === 'dev') {\n const port = args[1] === undefined ? 4173 : Number(args[1]);\n if (!Number.isInteger(port) || port < 0 || port > 65535)\n throw new Error('Port must be an integer between 0 and 65535.');\n const server = await serveDeck(entry, port);\n console.log(`Preview: ${server.url}`);\n const stop = () => {\n void server.close().then(() => process.exit(0));\n };\n process.once('SIGINT', stop);\n process.once('SIGTERM', stop);\n } else {\n throw new Error(usage);\n }\n }\n} catch (cause) {\n console.error(cause instanceof Error ? cause.message : cause);\n process.exitCode = 1;\n}\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -4,6 +4,7 @@ import { validatePresentation } from "@office-kit/pptx";
4
4
  interface BuildResult {
5
5
  bytes: Uint8Array;
6
6
  slides: string[];
7
+ slideTexts: string[];
7
8
  aspectRatio: number;
8
9
  dependencies: string[];
9
10
  diagnostics: ReturnType<typeof validatePresentation>;
@@ -16,8 +17,25 @@ declare function initProject(directory: string): Promise<string>;
16
17
  //#region src/inspect.d.ts
17
18
  /** Source references usable directly in Slide, Fill and Remove props. */
18
19
  declare function inspectTemplate(file: string): Promise<{
19
- slides: any;
20
- layouts: any;
20
+ slides: {
21
+ index: number;
22
+ part: string;
23
+ title: string | null;
24
+ shapes: {
25
+ id: number;
26
+ name: string;
27
+ kind: "shape" | "picture" | "group" | "graphicFrame" | "connector";
28
+ placeholder: {
29
+ type: string | null;
30
+ idx: number | null;
31
+ };
32
+ }[];
33
+ }[];
34
+ layouts: {
35
+ part: string;
36
+ name: string;
37
+ type: string | null;
38
+ }[];
21
39
  }>;
22
40
  //#endregion
23
41
  //#region src/index.d.ts
package/dist/worker.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { parentPort, workerData } from "node:worker_threads";
3
3
  import { join, resolve } from "node:path";
4
- import { getSlideSize, getSlides, loadPresentation, savePresentation, validatePresentation } from "@office-kit/pptx";
4
+ import { getSlideSize, getSlideText, getSlides, loadPresentation, savePresentation, validatePresentation } from "@office-kit/pptx";
5
5
  import { once } from "node:events";
6
6
  import { build, transform } from "esbuild";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -66,6 +66,7 @@ async function buildDeck(entry, directory) {
66
66
  aspectRatio: size ? size.width / size.height : 16 / 9,
67
67
  slides: getSlides(saved).map((slide) => renderSlideToSvg(saved, slide)),
68
68
  dependencies: Object.keys(result.metafile.inputs).map((path) => resolve(path)),
69
+ slideTexts: getSlides(saved).map((slide) => getSlideText(slide)),
69
70
  diagnostics
70
71
  };
71
72
  }
@@ -1 +1 @@
1
- {"version":3,"file":"worker.mjs","names":[],"sources":["../src/build.ts","../src/worker.ts"],"sourcesContent":["import { build, transform } from 'esbuild';\nimport { readFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { compile, type Node } from '@office-kit/pptx-dsl';\nimport {\n getSlideSize,\n getSlides,\n loadPresentation,\n savePresentation,\n validatePresentation,\n} from '@office-kit/pptx';\nimport { renderSlideToSvg } from '@office-kit/pptx-preview';\n\nconst resolvePackage = (name: string) => fileURLToPath(import.meta.resolve(name));\nexport interface BuildResult {\n bytes: Uint8Array;\n slides: string[];\n aspectRatio: number;\n dependencies: string[];\n diagnostics: ReturnType<typeof validatePresentation>;\n}\n/** Evaluates trusted local TSX. This is code execution, not a sandbox. */\nexport async function buildDeck(entry: string, directory: string): Promise<BuildResult> {\n const output = join(directory, 'deck.mjs');\n const result = await build({\n entryPoints: [resolve(entry)],\n outfile: output,\n bundle: true,\n platform: 'node',\n format: 'esm',\n target: 'node22',\n jsx: 'automatic',\n jsxImportSource: '@office-kit/pptx-dsl',\n sourcemap: 'inline',\n metafile: true,\n logLevel: 'silent',\n plugins: [\n {\n name: 'source-location',\n setup(builder) {\n // A bundled module lives in a temporary directory. Keep source-relative\n // asset URLs pointing at each original module, including imported files.\n builder.onLoad({ filter: /\\.[cm]?[jt]sx?$/ }, async ({ path }) => {\n const source = await readFile(path, 'utf8');\n const loader = path.endsWith('.tsx')\n ? 'tsx'\n : path.endsWith('.ts') || path.endsWith('.mts') || path.endsWith('.cts')\n ? 'ts'\n : path.endsWith('.jsx')\n ? 'jsx'\n : 'js';\n const transformed = await transform(source, {\n loader,\n sourcefile: path,\n sourcemap: 'inline',\n jsx: 'automatic',\n jsxDev: true,\n jsxImportSource: '@office-kit/pptx-dsl',\n define: { 'import.meta.url': JSON.stringify(pathToFileURL(path).href) },\n });\n return { contents: transformed.code, loader: 'js' };\n });\n },\n },\n {\n name: 'shared-office-kit',\n setup(builder) {\n // Core uses symbol-backed handles; all consumers must share its instance.\n builder.onResolve({ filter: /^@office-kit\\/pptx(?:-dsl)?(?:\\/.*)?$/ }, ({ path }) => ({\n path: resolvePackage(path),\n external: true,\n }));\n },\n },\n ],\n });\n const module: { default?: Node } = await import(pathToFileURL(output).href);\n if (!module.default) throw new Error('The TSX file must default-export a Presentation.');\n const presentation = await compile(module.default);\n const diagnostics = validatePresentation(presentation);\n const errors = diagnostics.filter((issue) => issue.severity === 'error');\n if (errors.length) throw new Error(`Invalid presentation: ${JSON.stringify(errors)}`);\n const bytes = await savePresentation(presentation);\n // Preview serialized output too, so persistence defects are visible during authoring.\n const saved = await loadPresentation(bytes);\n const size = getSlideSize(saved);\n return {\n bytes,\n aspectRatio: size ? size.width / size.height : 16 / 9,\n slides: getSlides(saved).map((slide) => renderSlideToSvg(saved, slide)),\n dependencies: Object.keys(result.metafile.inputs).map((path) => resolve(path)),\n diagnostics,\n };\n}\n","import { once } from 'node:events';\nimport { parentPort, workerData } from 'node:worker_threads';\nimport { buildDeck } from './build.ts';\n\nif (!parentPort) throw new Error('The build worker must run in a worker thread.');\nconst { entry, directory } = workerData as { entry: string; directory: string };\nawait once(parentPort, 'message');\nparentPort.postMessage(await buildDeck(entry, directory));\n"],"mappings":";;;;;;;;;;AAcA,MAAM,kBAAkB,SAAiB,cAAc,OAAO,KAAK,QAAQ,IAAI,CAAC;;AAShF,eAAsB,UAAU,OAAe,WAAyC;CACtF,MAAM,SAAS,KAAK,WAAW,UAAU;CACzC,MAAM,SAAS,MAAM,MAAM;EACzB,aAAa,CAAC,QAAQ,KAAK,CAAC;EAC5B,SAAS;EACT,QAAQ;EACR,UAAU;EACV,QAAQ;EACR,QAAQ;EACR,KAAK;EACL,iBAAiB;EACjB,WAAW;EACX,UAAU;EACV,UAAU;EACV,SAAS,CACP;GACE,MAAM;GACN,MAAM,SAAS;IAGb,QAAQ,OAAO,EAAE,QAAQ,kBAAkB,GAAG,OAAO,EAAE,WAAW;KAkBhE,OAAO;MAAE,WAAU,MATO,UAAU,MARf,SAAS,MAAM,MAAM,GAQE;OAC1C,QARa,KAAK,SAAS,MAAM,IAC/B,QACA,KAAK,SAAS,KAAK,KAAK,KAAK,SAAS,MAAM,KAAK,KAAK,SAAS,MAAM,IACnE,OACA,KAAK,SAAS,MAAM,IAClB,QACA;OAGN,YAAY;OACZ,WAAW;OACX,KAAK;OACL,QAAQ;OACR,iBAAiB;OACjB,QAAQ,EAAE,mBAAmB,KAAK,UAAU,cAAc,IAAI,CAAC,CAAC,IAAI,EAAE;MACxE,CAAC,EAAA,CAC8B;MAAM,QAAQ;KAAK;IACpD,CAAC;GACH;EACF,GACA;GACE,MAAM;GACN,MAAM,SAAS;IAEb,QAAQ,UAAU,EAAE,QAAQ,wCAAwC,IAAI,EAAE,YAAY;KACpF,MAAM,eAAe,IAAI;KACzB,UAAU;IACZ,EAAE;GACJ;EACF,CACF;CACF,CAAC;CACD,MAAM,SAA6B,MAAM,OAAO,cAAc,MAAM,CAAC,CAAC;CACtE,IAAI,CAAC,OAAO,SAAS,MAAM,IAAI,MAAM,kDAAkD;CACvF,MAAM,eAAe,MAAM,QAAQ,OAAO,OAAO;CACjD,MAAM,cAAc,qBAAqB,YAAY;CACrD,MAAM,SAAS,YAAY,QAAQ,UAAU,MAAM,aAAa,OAAO;CACvE,IAAI,OAAO,QAAQ,MAAM,IAAI,MAAM,yBAAyB,KAAK,UAAU,MAAM,GAAG;CACpF,MAAM,QAAQ,MAAM,iBAAiB,YAAY;CAEjD,MAAM,QAAQ,MAAM,iBAAiB,KAAK;CAC1C,MAAM,OAAO,aAAa,KAAK;CAC/B,OAAO;EACL;EACA,aAAa,OAAO,KAAK,QAAQ,KAAK,SAAS,KAAK;EACpD,QAAQ,UAAU,KAAK,CAAC,CAAC,KAAK,UAAU,iBAAiB,OAAO,KAAK,CAAC;EACtE,cAAc,OAAO,KAAK,OAAO,SAAS,MAAM,CAAC,CAAC,KAAK,SAAS,QAAQ,IAAI,CAAC;EAC7E;CACF;AACF;;;AC1FA,IAAI,CAAC,YAAY,MAAM,IAAI,MAAM,+CAA+C;AAChF,MAAM,EAAE,OAAO,cAAc;AAC7B,MAAM,KAAK,YAAY,SAAS;AAChC,WAAW,YAAY,MAAM,UAAU,OAAO,SAAS,CAAC"}
1
+ {"version":3,"file":"worker.mjs","names":[],"sources":["../src/build.ts","../src/worker.ts"],"sourcesContent":["import { build, transform } from 'esbuild';\nimport { readFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { compile, type Node } from '@office-kit/pptx-dsl';\nimport {\n getSlideSize,\n getSlides,\n getSlideText,\n loadPresentation,\n savePresentation,\n validatePresentation,\n} from '@office-kit/pptx';\nimport { renderSlideToSvg } from '@office-kit/pptx-preview';\n\nconst resolvePackage = (name: string) => fileURLToPath(import.meta.resolve(name));\nexport interface BuildResult {\n bytes: Uint8Array;\n slides: string[];\n slideTexts: string[];\n aspectRatio: number;\n dependencies: string[];\n diagnostics: ReturnType<typeof validatePresentation>;\n}\n/** Evaluates trusted local TSX. This is code execution, not a sandbox. */\nexport async function buildDeck(entry: string, directory: string): Promise<BuildResult> {\n const output = join(directory, 'deck.mjs');\n const result = await build({\n entryPoints: [resolve(entry)],\n outfile: output,\n bundle: true,\n platform: 'node',\n format: 'esm',\n target: 'node22',\n jsx: 'automatic',\n jsxImportSource: '@office-kit/pptx-dsl',\n sourcemap: 'inline',\n metafile: true,\n logLevel: 'silent',\n plugins: [\n {\n name: 'source-location',\n setup(builder) {\n // A bundled module lives in a temporary directory. Keep source-relative\n // asset URLs pointing at each original module, including imported files.\n builder.onLoad({ filter: /\\.[cm]?[jt]sx?$/ }, async ({ path }) => {\n const source = await readFile(path, 'utf8');\n const loader = path.endsWith('.tsx')\n ? 'tsx'\n : path.endsWith('.ts') || path.endsWith('.mts') || path.endsWith('.cts')\n ? 'ts'\n : path.endsWith('.jsx')\n ? 'jsx'\n : 'js';\n const transformed = await transform(source, {\n loader,\n sourcefile: path,\n sourcemap: 'inline',\n jsx: 'automatic',\n jsxDev: true,\n jsxImportSource: '@office-kit/pptx-dsl',\n define: { 'import.meta.url': JSON.stringify(pathToFileURL(path).href) },\n });\n return { contents: transformed.code, loader: 'js' };\n });\n },\n },\n {\n name: 'shared-office-kit',\n setup(builder) {\n // Core uses symbol-backed handles; all consumers must share its instance.\n builder.onResolve({ filter: /^@office-kit\\/pptx(?:-dsl)?(?:\\/.*)?$/ }, ({ path }) => ({\n path: resolvePackage(path),\n external: true,\n }));\n },\n },\n ],\n });\n const module: { default?: Node } = await import(pathToFileURL(output).href);\n if (!module.default) throw new Error('The TSX file must default-export a Presentation.');\n const presentation = await compile(module.default);\n const diagnostics = validatePresentation(presentation);\n const errors = diagnostics.filter((issue) => issue.severity === 'error');\n if (errors.length) throw new Error(`Invalid presentation: ${JSON.stringify(errors)}`);\n const bytes = await savePresentation(presentation);\n // Preview serialized output too, so persistence defects are visible during authoring.\n const saved = await loadPresentation(bytes);\n const size = getSlideSize(saved);\n return {\n bytes,\n aspectRatio: size ? size.width / size.height : 16 / 9,\n slides: getSlides(saved).map((slide) => renderSlideToSvg(saved, slide)),\n dependencies: Object.keys(result.metafile.inputs).map((path) => resolve(path)),\n slideTexts: getSlides(saved).map((slide) => getSlideText(slide)),\n diagnostics,\n };\n}\n","import { once } from 'node:events';\nimport { parentPort, workerData } from 'node:worker_threads';\nimport { buildDeck } from './build.ts';\n\nif (!parentPort) throw new Error('The build worker must run in a worker thread.');\nconst { entry, directory } = workerData as { entry: string; directory: string };\nawait once(parentPort, 'message');\nparentPort.postMessage(await buildDeck(entry, directory));\n"],"mappings":";;;;;;;;;;AAeA,MAAM,kBAAkB,SAAiB,cAAc,OAAO,KAAK,QAAQ,IAAI,CAAC;;AAUhF,eAAsB,UAAU,OAAe,WAAyC;CACtF,MAAM,SAAS,KAAK,WAAW,UAAU;CACzC,MAAM,SAAS,MAAM,MAAM;EACzB,aAAa,CAAC,QAAQ,KAAK,CAAC;EAC5B,SAAS;EACT,QAAQ;EACR,UAAU;EACV,QAAQ;EACR,QAAQ;EACR,KAAK;EACL,iBAAiB;EACjB,WAAW;EACX,UAAU;EACV,UAAU;EACV,SAAS,CACP;GACE,MAAM;GACN,MAAM,SAAS;IAGb,QAAQ,OAAO,EAAE,QAAQ,kBAAkB,GAAG,OAAO,EAAE,WAAW;KAkBhE,OAAO;MAAE,WAAU,MATO,UAAU,MARf,SAAS,MAAM,MAAM,GAQE;OAC1C,QARa,KAAK,SAAS,MAAM,IAC/B,QACA,KAAK,SAAS,KAAK,KAAK,KAAK,SAAS,MAAM,KAAK,KAAK,SAAS,MAAM,IACnE,OACA,KAAK,SAAS,MAAM,IAClB,QACA;OAGN,YAAY;OACZ,WAAW;OACX,KAAK;OACL,QAAQ;OACR,iBAAiB;OACjB,QAAQ,EAAE,mBAAmB,KAAK,UAAU,cAAc,IAAI,CAAC,CAAC,IAAI,EAAE;MACxE,CAAC,EAAA,CAC8B;MAAM,QAAQ;KAAK;IACpD,CAAC;GACH;EACF,GACA;GACE,MAAM;GACN,MAAM,SAAS;IAEb,QAAQ,UAAU,EAAE,QAAQ,wCAAwC,IAAI,EAAE,YAAY;KACpF,MAAM,eAAe,IAAI;KACzB,UAAU;IACZ,EAAE;GACJ;EACF,CACF;CACF,CAAC;CACD,MAAM,SAA6B,MAAM,OAAO,cAAc,MAAM,CAAC,CAAC;CACtE,IAAI,CAAC,OAAO,SAAS,MAAM,IAAI,MAAM,kDAAkD;CACvF,MAAM,eAAe,MAAM,QAAQ,OAAO,OAAO;CACjD,MAAM,cAAc,qBAAqB,YAAY;CACrD,MAAM,SAAS,YAAY,QAAQ,UAAU,MAAM,aAAa,OAAO;CACvE,IAAI,OAAO,QAAQ,MAAM,IAAI,MAAM,yBAAyB,KAAK,UAAU,MAAM,GAAG;CACpF,MAAM,QAAQ,MAAM,iBAAiB,YAAY;CAEjD,MAAM,QAAQ,MAAM,iBAAiB,KAAK;CAC1C,MAAM,OAAO,aAAa,KAAK;CAC/B,OAAO;EACL;EACA,aAAa,OAAO,KAAK,QAAQ,KAAK,SAAS,KAAK;EACpD,QAAQ,UAAU,KAAK,CAAC,CAAC,KAAK,UAAU,iBAAiB,OAAO,KAAK,CAAC;EACtE,cAAc,OAAO,KAAK,OAAO,SAAS,MAAM,CAAC,CAAC,KAAK,SAAS,QAAQ,IAAI,CAAC;EAC7E,YAAY,UAAU,KAAK,CAAC,CAAC,KAAK,UAAU,aAAa,KAAK,CAAC;EAC/D;CACF;AACF;;;AC7FA,IAAI,CAAC,YAAY,MAAM,IAAI,MAAM,+CAA+C;AAChF,MAAM,EAAE,OAAO,cAAc;AAC7B,MAAM,KAAK,YAAY,SAAS;AAChC,WAAW,YAAY,MAAM,UAAU,OAAO,SAAS,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@office-kit/pptx-dev",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Build and preview PowerPoint TSX presentations locally",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,9 +28,9 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "esbuild": "^0.28.1",
31
- "@office-kit/pptx": "^0.18.0",
31
+ "@office-kit/pptx": "^0.18.2",
32
32
  "@office-kit/pptx-dsl": "^0.5.0",
33
- "@office-kit/pptx-preview": "^0.9.6"
33
+ "@office-kit/pptx-preview": "^0.9.8"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^24.13.2",