@office-kit/pptx-dev 0.4.2 → 0.6.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 +30 -8
- package/dist/cli.mjs +253 -83
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +2 -19
- package/dist/terminal-client.js +18 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ latency. The deck is still evaluated in a fresh worker on every build. Changes t
|
|
|
50
50
|
shared resources, including themes, relationships, charts and images, redraw all
|
|
51
51
|
slides. Update time depends on the deck size and any code it runs.
|
|
52
52
|
|
|
53
|
-
##
|
|
53
|
+
## Agents in the preview
|
|
54
54
|
|
|
55
55
|
The right panel defaults to **Claude Code**. Click **Start** to open the locally
|
|
56
56
|
installed `claude` CLI in an interactive terminal. Install and sign in to Claude
|
|
@@ -75,19 +75,32 @@ CLI. Navigation while Claude is replying does not change that turn's context.
|
|
|
75
75
|
Stale input is rejected with a retry message. If your Claude settings or managed
|
|
76
76
|
policy disable hooks, automatic context attachment is unavailable; enable the
|
|
77
77
|
preview hook in Claude Code to use it. No project settings file is rewritten.
|
|
78
|
+
|
|
79
|
+
Use **Shift+Enter** to insert a newline in the embedded Claude prompt. On completion,
|
|
80
|
+
the preview checks the actual deck build. Build errors are returned to Claude's
|
|
81
|
+
Stop hook or to the Codex conversation for up to three repair attempts per user
|
|
82
|
+
request. If repair fails, the error stays visible and the last successful preview
|
|
83
|
+
is retained. Stopping the agent cancels further repair attempts.
|
|
84
|
+
|
|
85
|
+
Both agents receive guidance to check text against its actual background, including
|
|
86
|
+
inherited theme colors, and to use native `Text` paragraphs or mapped rows for lists
|
|
87
|
+
and agendas. `Bullets` and `TableOfContents` are not built-in DSL components; define
|
|
88
|
+
or import project helpers if using those names. See the
|
|
89
|
+
[TSX authoring reference](../../skill/references/tsx.md#lists-and-tables-of-contents)
|
|
90
|
+
for a complete agenda example. Contrast guidance still requires visual review;
|
|
91
|
+
a successful build is not a visual accessibility check.
|
|
78
92
|
Dependency paths are source candidates, not an exact slide-to-file mapping.
|
|
79
93
|
“This slide” requests a focused patch; requests about another slide or the
|
|
80
94
|
whole deck can edit other files or shared styling.
|
|
81
95
|
|
|
82
96
|
Reloading the browser reconnects to the same process and replays its recent
|
|
83
|
-
terminal output (up to 2 MB).
|
|
84
|
-
|
|
85
|
-
another tab. **End session** stops the process; saved edits remain. Start opens
|
|
97
|
+
terminal output (up to 2 MB). Each pane has its own session and terminal controls. Keep the owning tab open
|
|
98
|
+
while agents are working. **End session** stops the process; saved edits remain. Start opens
|
|
86
99
|
a new session, and Claude's `/resume` command can restore an earlier conversation.
|
|
87
|
-
Stopping the dev server also stops
|
|
100
|
+
Stopping the dev server also stops all embedded sessions. This is independent of
|
|
88
101
|
the Claude Code conversation that may have launched your dev server.
|
|
89
102
|
|
|
90
|
-
Select **Codex** while Claude Code is stopped to use the existing message-based
|
|
103
|
+
Select **Codex** while Claude Code in that pane is stopped to use the existing message-based
|
|
91
104
|
chat. Codex replies render Markdown (including lists, code blocks and tables);
|
|
92
105
|
raw HTML, unsafe links and remote images are not rendered. Codex still uses
|
|
93
106
|
`exec --json` with the `workspace-write` sandbox and its installed CLI login.
|
|
@@ -98,16 +111,25 @@ each request. The dev server retains up to 100 messages until it exits.
|
|
|
98
111
|
sends a message. Model/settings menus in the embedded terminal are currently
|
|
99
112
|
specific to Claude Code.
|
|
100
113
|
|
|
114
|
+
Use the **Split right** or **Split down** icons in a pane's title bar to run up to
|
|
115
|
+
four agents side by side or stacked. Each pane independently selects Claude Code
|
|
116
|
+
or Codex, retains its own conversation and receives the currently selected slide.
|
|
117
|
+
Drag the divider to adjust the split, or focus it and use the arrow keys. Layout
|
|
118
|
+
and Claude terminal sessions survive a browser reload. Splitting preserves running
|
|
119
|
+
sessions and unsent drafts. **Close** ends only that pane's agent; closing the last
|
|
120
|
+
pane opens a fresh one. All agents edit the same project directory, so assign
|
|
121
|
+
separate slides or files when working simultaneously.
|
|
122
|
+
|
|
101
123
|
Drag the left edge of the chat panel to adjust its width. The preview remembers the
|
|
102
124
|
width in this browser; double-click the edge to reset it. You can also focus the
|
|
103
125
|
edge and use the left/right arrow keys (Shift for larger steps), or Home/End for
|
|
104
126
|
the minimum/maximum width. Narrow screens keep the stacked layout.
|
|
105
127
|
|
|
106
|
-
Use **
|
|
128
|
+
Use **Agents** in the header to hide/show the panel. On narrow screens the panel
|
|
107
129
|
moves below the preview. Presentation mode hides it without ending the session.
|
|
108
130
|
Both providers save TSX directly, which triggers the usual automatic rebuild.
|
|
109
131
|
The server binds to loopback and requires same-origin JSON for terminal control;
|
|
110
|
-
the local context hook uses a separate per-
|
|
132
|
+
the local context hook uses a separate per-session token.
|
|
111
133
|
|
|
112
134
|
The terminal uses `node-pty` and xterm.js. Linux systems without a matching PTY
|
|
113
135
|
prebuild need the native build prerequisites documented by
|
package/dist/cli.mjs
CHANGED
|
@@ -8,38 +8,32 @@ import { createServer } from "node:http";
|
|
|
8
8
|
import { randomBytes } from "node:crypto";
|
|
9
9
|
import MarkdownIt from "markdown-it";
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
|
-
//#region src/
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
button,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}
|
|
21
|
-
button:hover:not(:disabled),.download:hover{background:#f0f3f9}button:disabled{opacity:.4;cursor:default}
|
|
22
|
-
:focus-visible{outline:2px solid #4967dd;outline-offset:3px}
|
|
23
|
-
header{display:flex;align-items:center;gap:16px;padding:0 20px;background:#fff;border-bottom:1px solid #d4d9e2;min-width:0}
|
|
24
|
-
.brand{font-weight:700;font-size:17px;white-space:nowrap}.badge{font-size:11px;color:#616b7c;background:#f1f3f7;border-radius:4px;padding:3px 6px}
|
|
25
|
-
#status{flex:1;color:#667085;min-width:0}#present{background:#293c73;color:#fff;border-color:#293c73}
|
|
11
|
+
//#region src/styles.ts
|
|
12
|
+
const previewStyles = `*{box-sizing:border-box}
|
|
13
|
+
body{margin:0;height:100dvh;overflow:hidden;background:#e9eaf4;color:#dce1f1;font:13px system-ui;display:grid;grid-template-rows:60px minmax(0,1fr) 42px}
|
|
14
|
+
button,a,select{font:inherit;color:inherit}button,select,.download{border:1px solid #30364d;border-radius:9px;background:#191e30;padding:7px 12px;text-decoration:none;cursor:pointer}
|
|
15
|
+
button:hover:not(:disabled),.download:hover{background:#292f48}button:disabled{opacity:.4;cursor:default}
|
|
16
|
+
:focus-visible{outline:2px solid #9b87ff;outline-offset:3px}
|
|
17
|
+
header{display:flex;align-items:center;gap:16px;padding:0 20px;background:linear-gradient(110deg,#171b2f,#211c38 60%,#171b2f);border-bottom:1px solid #38304d;min-width:0}
|
|
18
|
+
.brand{font-weight:700;font-size:17px;white-space:nowrap}.badge{font-size:11px;color:#b2a4fb;background:#342c54;border-radius:4px;padding:3px 6px}
|
|
19
|
+
#status{flex:1;color:#9ba5be;min-width:0}#present{background:#7154e8;color:#191e30;border-color:#7154e8}
|
|
26
20
|
.workspace{--filmstrip-width:190px;--chat-width:38vw;display:grid;grid-template-columns:var(--filmstrip-width) minmax(0,1fr) clamp(280px,var(--chat-width),calc(100vw - var(--filmstrip-width) - 240px));min-height:0}
|
|
27
|
-
.filmstrip{background:#
|
|
28
|
-
.filmstrip h2{margin:0 0 12px 26px;text-transform:uppercase;letter-spacing:.1em;font-size:10px;color:#
|
|
21
|
+
.filmstrip{background:#111625;border-right:1px solid #30364d;overflow:auto;overscroll-behavior:contain;padding:16px 12px}
|
|
22
|
+
.filmstrip h2{margin:0 0 12px 26px;text-transform:uppercase;letter-spacing:.1em;font-size:10px;color:#8490ac;font-weight:600}
|
|
29
23
|
#thumbnails{display:flex;flex-direction:column;gap:12px;margin:0;padding:0;list-style:none}
|
|
30
24
|
.thumbnail{display:flex;align-items:flex-start;gap:8px;width:100%;padding:4px 3px;border:0;background:transparent;border-radius:5px;text-align:left}
|
|
31
|
-
.slide-number{width:18px;flex:none;text-align:right;font-size:11px;color:#
|
|
32
|
-
.thumbnail img{display:block;min-width:0;width:calc(100% - 26px);background:white;box-shadow:0 1px 4px #
|
|
33
|
-
.thumbnail[aria-current="true"]{background:#
|
|
25
|
+
.slide-number{width:18px;flex:none;text-align:right;font-size:11px;color:#8995b0;padding-top:5px}
|
|
26
|
+
.thumbnail img{display:block;min-width:0;width:calc(100% - 26px);background:white;box-shadow:0 1px 4px #080b1933;border:2px solid transparent;border-radius:3px;aspect-ratio:var(--slide-ratio,16/9);object-fit:contain}
|
|
27
|
+
.thumbnail[aria-current="true"]{background:#31274e}.thumbnail[aria-current="true"] img{border-color:#9b87ff}.thumbnail[aria-current="true"] .slide-number{color:#b6a5ff;font-weight:700}
|
|
34
28
|
main{min-width:0;min-height:0;display:flex;flex-direction:column}
|
|
35
29
|
#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}
|
|
36
30
|
#error[hidden]{display:none}
|
|
37
|
-
#stage{flex:1;min-height:0;overflow:auto;display:flex;padding:32px;overscroll-behavior:contain}
|
|
38
|
-
#slide{position:relative;flex:none;margin:auto;background:white;box-shadow:0
|
|
31
|
+
#stage{background:radial-gradient(ellipse at 20% 15%,#ddd8f6aa,transparent 65%),radial-gradient(#a0a4bf33 1px,transparent 1px),#edeef6;background-size:auto,18px 18px,auto;flex:1;min-height:0;overflow:auto;display:flex;padding:32px;overscroll-behavior:contain}
|
|
32
|
+
#slide{position:relative;flex:none;margin:auto;background:white;box-shadow:0 20px 70px #28234825,0 2px 8px #28234815;overflow:hidden}
|
|
39
33
|
#slide{user-select:text;-webkit-user-select:text}
|
|
40
|
-
#empty{margin:auto;color:#
|
|
41
|
-
footer{display:flex;align-items:center;gap:14px;padding:0 16px;background:#
|
|
42
|
-
#count{min-width:90px}footer .hint{flex:1;color:#
|
|
34
|
+
#empty{margin:auto;color:#8995b0}
|
|
35
|
+
footer{display:flex;align-items:center;gap:14px;padding:0 16px;background:#191e30;border-top:1px solid #30364d;font-size:12px}
|
|
36
|
+
#count{min-width:90px}footer .hint{flex:1;color:#8995b0}footer button{padding:3px 10px}footer select{padding:3px 8px}
|
|
43
37
|
#presentation-controls{display:none}
|
|
44
38
|
body.presenting{grid-template-rows:minmax(0,1fr);background:#111}
|
|
45
39
|
.presenting header,.presenting footer,.presenting .filmstrip,.presenting #chat{display:none}
|
|
@@ -48,44 +42,50 @@ body.presenting{grid-template-rows:minmax(0,1fr);background:#111}
|
|
|
48
42
|
.presenting #presentation-controls:hover,.presenting #presentation-controls:focus-within{opacity:1}
|
|
49
43
|
#presentation-controls button{background:transparent;color:white;border-color:#5d6575}
|
|
50
44
|
@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}}
|
|
51
|
-
#chat{position:relative;min-width:0;min-height:0;display:flex;flex-direction:column;background:#
|
|
52
|
-
#chat-resizer{position:absolute;left:-5px;top:0;bottom:0;width:10px;z-index:3;cursor:col-resize;touch-action:none}#chat-resizer::after{content:"";position:absolute;left:4px;top:0;bottom:0;width:2px}#chat-resizer:hover::after,#chat-resizer:focus-visible::after,.resizing-chat #chat-resizer::after{background:#
|
|
53
|
-
.chat-heading{display:flex;align-items:center;gap:8px;padding:16px;border-bottom:1px solid #
|
|
45
|
+
#chat{position:relative;min-width:0;min-height:0;display:flex;flex-direction:column;background:#191e30;border-left:1px solid #30364d}
|
|
46
|
+
#chat-resizer{position:absolute;left:-5px;top:0;bottom:0;width:10px;z-index:3;cursor:col-resize;touch-action:none}#chat-resizer::after{content:"";position:absolute;left:4px;top:0;bottom:0;width:2px}#chat-resizer:hover::after,#chat-resizer:focus-visible::after,.resizing-chat #chat-resizer::after{background:#9b87ff}body.resizing-chat,body.resizing-chat *{cursor:col-resize!important;user-select:none!important}
|
|
47
|
+
.chat-heading{display:flex;align-items:center;gap:8px;padding:16px;border-bottom:1px solid #2a3046}.chat-heading strong{flex:1}.chat-heading button{padding:5px 8px}
|
|
54
48
|
#messages{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:16px;overscroll-behavior:contain}
|
|
55
|
-
.chat-message{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.6}.chat-message.user{background:#
|
|
56
|
-
#chat-form{padding:14px;border-top:1px solid #
|
|
49
|
+
.chat-message{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.6}.chat-message.user{background:#302752;border-radius:8px;padding:10px 12px}.chat-message small{display:block;color:#a4afc6;font-size:11px;margin-bottom:4px}.chat-intro{color:#8995b0;line-height:1.7}
|
|
50
|
+
#chat-form{padding:14px;border-top:1px solid #2a3046}#chat-context{font-size:11px;color:#9b87ff;margin-bottom:8px}#chat-input{font:inherit;resize:vertical;min-height:90px;max-height:200px;width:100%;padding:10px;border:1px solid #30364d;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:#7154e8;color:white}#chat-status{padding:0 14px 12px;color:#9ba5be;font-size:11px;white-space:pre-wrap;max-height:90px;overflow:auto}
|
|
57
51
|
body.chat-hidden .workspace{grid-template-columns:190px minmax(0,1fr)}body.chat-hidden #chat{display:none}
|
|
58
52
|
body.presenting .workspace{grid-template-columns:minmax(0,1fr)}
|
|
59
53
|
@media(max-width:1000px){.workspace{--filmstrip-width:120px;--chat-width:380px}.filmstrip{padding:12px 5px}#stage{padding:16px}}
|
|
60
|
-
@media(max-width:700px){#chat-resizer{display:none}.workspace{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(160px,1fr) minmax(200px,1fr)}.filmstrip{display:none}#chat{border-top:1px solid #
|
|
54
|
+
@media(max-width:700px){#chat-resizer{display:none}.workspace{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(160px,1fr) minmax(200px,1fr)}.filmstrip{display:none}#chat{border-top:1px solid #30364d}.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}}
|
|
61
55
|
@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
|
|
62
56
|
|
|
63
57
|
[hidden]{display:none!important}
|
|
64
58
|
.chat-heading{padding:10px 14px}.chat-heading select{flex:1;min-width:0;font-weight:600;border:0;padding-left:0}
|
|
65
|
-
#chat-context{margin:0;padding:8px 14px;border-bottom:1px solid #
|
|
59
|
+
#chat-context{margin:0;padding:8px 14px;border-bottom:1px solid #2a3046;color:#9ba5be;font-size:11px}
|
|
66
60
|
#claude-panel,#codex-panel{display:flex;flex-direction:column;flex:1;min-height:0}
|
|
67
61
|
#claude-panel{background:#171b24;color:#e4e8f0}
|
|
68
62
|
.terminal-toolbar{display:flex;align-items:center;gap:8px;padding:8px 12px;font-size:11px;min-height:40px}.terminal-toolbar span{flex:1;overflow-wrap:anywhere}.terminal-toolbar button{font-size:11px;padding:5px 8px;background:#262d3b;border-color:#45516a;color:#e4e8f0}
|
|
69
63
|
.terminal-toolbar button:hover:not(:disabled){background:#35415a}
|
|
70
|
-
#terminal{flex:1;min-height:0;overflow:hidden;padding:
|
|
64
|
+
#terminal{flex:1;min-height:0;overflow:hidden;padding:0}.xterm{height:100%}
|
|
71
65
|
#chat-input{min-height:70px;resize:vertical}.chat-shortcut{flex:1;color:#9098a7;font-size:10px}
|
|
72
|
-
.markdown{white-space:normal;line-height:1.65}.markdown>:first-child{margin-top:0}.markdown>:last-child{margin-bottom:0}.markdown p{margin:0 0 .8em}.markdown pre{overflow:auto;padding:10px;background:#
|
|
66
|
+
.markdown{white-space:normal;line-height:1.65}.markdown>:first-child{margin-top:0}.markdown>:last-child{margin-bottom:0}.markdown p{margin:0 0 .8em}.markdown pre{overflow:auto;padding:10px;background:#22283b;border:1px solid #2a3046;border-radius:6px;white-space:pre}.markdown code{font-size:.9em;background:#22283b;border-radius:3px;padding:1px 4px}.markdown pre code{padding:0}.markdown ul,.markdown ol{padding-left:22px}.markdown blockquote{margin-left:0;padding-left:12px;border-left:3px solid #30364d;color:#9ba5be}.markdown table{display:block;overflow:auto;border-collapse:collapse}.markdown th,.markdown td{padding:6px 8px;border:1px solid #30364d}.markdown h1,.markdown h2,.markdown h3{font-size:1.1em}.markdown a{color:#b6a5ff;text-decoration:underline}
|
|
67
|
+
|
|
68
|
+
.brand{display:flex;align-items:center;gap:10px;letter-spacing:-.5px}.brand svg{width:32px;height:32px;flex:none}.brand em{font-style:normal;font-weight:400;color:#aca4ce}.badge{text-transform:uppercase;letter-spacing:.14em;font-size:9px;border:1px solid #61508866;border-radius:5px}#status{font-size:11px}#present,#chat-send{color:#fff;background:linear-gradient(120deg,#8061ed,#634de0);border-color:#a38aef55;box-shadow:0 3px 15px #7455e82a}#toggle-chat[aria-expanded="true"]{background:#35294f;border-color:#6f528b;color:#d6c5ff}.download{background:transparent;color:#bcc5dc}header button,header .download{font-size:12px}.filmstrip{background:linear-gradient(180deg,#161b2c,#111625)}.thumbnail{transition:background .15s}.thumbnail[aria-current="true"]{box-shadow:inset 0 0 0 1px #9980ef33}.thumbnail[aria-current="true"] img{box-shadow:0 0 16px #9567ed22}footer{background:#141929;color:#b9c2d8}footer .hint{font-size:10px}#count{font-size:11px;font-weight:600}#chat{background:#101422}.workspace-heading{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;color:#c7bee5;font-size:10px;font-weight:600;letter-spacing:.12em}.workspace-heading b{font-size:9px;color:#89cdbc;font-weight:500;letter-spacing:.08em}.workspace-heading b::before{content:"";display:inline-block;width:5px;height:5px;background:#86d5bb;border-radius:50%;margin-right:6px;box-shadow:0 0 8px #86d5bb66}.chat-heading{background:#191e30}.chat-heading select{background:transparent;color:#e4e5f3;font-size:12px}.chat-heading option{background:#191e30}#chat-context{background:#1a1e30;color:#a59abb;font-size:10px}.terminal-toolbar{gap:6px;padding:8px;font-size:10px}.terminal-toolbar button{white-space:nowrap}#terminal-start{background:#6c52c9;border-color:#9678e9}.terminal-frame{display:flex;flex:1;min-height:0;min-width:0;padding:4px 8px 8px;overflow:hidden}#terminal{width:100%;min-width:0}#chat-input{color:#e2e5f3;background:#131827;border-color:#3b3f58}#chat-input:focus{outline:none;border-color:#9b87ff;box-shadow:0 0 0 3px #9b87ff18}.chat-message.user{border:1px solid #67509655}.chat-intro{font-size:12px}.markdown pre{border-color:#3a4059}.markdown code{color:#d2c6fa}#error{background:#38242b;color:#ffb8b0}
|
|
69
|
+
@media(max-width:700px){.brand svg{width:26px;height:26px}.brand{gap:6px}.workspace-heading{padding:7px 12px}header .download{font-size:10px;padding:7px}.badge{display:none}}
|
|
70
|
+
`;
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/page.ts
|
|
73
|
+
const page = `<!doctype html>
|
|
74
|
+
<html lang="en">
|
|
75
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width">
|
|
76
|
+
<title>Office Kit — PowerPoint preview</title>
|
|
77
|
+
<link rel="stylesheet" href="/terminal.css">
|
|
78
|
+
<style>
|
|
79
|
+
${previewStyles}
|
|
80
|
+
#agent-workspace{position:relative;flex:1;min-height:0;min-width:0;display:flex;overflow:auto}.agent-pane{position:absolute;padding:3px 5px;display:flex;flex-direction:column;flex:1;min-width:0;min-height:0;overflow:hidden}.agent-pane iframe{border:0;width:100%;flex:1;min-height:0;border-radius:0 0 9px 9px}.agent-tools{display:flex;gap:4px;align-items:center;flex-wrap:wrap;padding:6px;background:#20253a;border:1px solid #34334e;border-radius:9px 9px 0 0}.agent-tools span{flex:1;font-size:10px;letter-spacing:.04em;color:#b3abc9}.agent-tools svg{width:14px;height:14px}.agent-pane:focus-within .agent-tools{border-color:#8b6ed577;background:#29243e}.agent-tools button{font-size:11px;padding:5px;background:transparent;border:0;display:grid;place-items:center}.agent-divider{position:absolute;z-index:2;background:#101422;cursor:col-resize;touch-action:none}.agent-divider.vertical{cursor:row-resize}.agent-divider:hover,.agent-divider:focus-visible{background:#9b87ff}.splitting-agents iframe,.resizing-chat iframe{pointer-events:none}
|
|
73
81
|
</style>
|
|
74
|
-
<header><span class="brand">Office Kit</span><span class="badge">
|
|
82
|
+
<header><span class="brand"><svg viewBox="0 0 36 36" aria-hidden="true"><defs><linearGradient id="brand-gradient" x2="1" y2="1"><stop stop-color="#c39aff"/><stop offset="1" stop-color="#7165f4"/></linearGradient></defs><rect x="2" y="2" width="32" height="32" rx="10" fill="url(#brand-gradient)"/><path d="M11 10h9l6 6v10H11z" fill="none" stroke="white" stroke-width="1.8" stroke-linejoin="round"/><path d="M20 10v7h6M15 21h7M15 25h4" fill="none" stroke="white" stroke-width="1.8" stroke-linecap="round"/><path d="m28 3 1.2 3.8L33 8l-3.8 1.2L28 13l-1.2-3.8L23 8l3.8-1.2z" fill="#e7dbff"/></svg>Office <em>Kit</em></span><span class="badge">Studio</span><span id="status" role="status">Building…</span><button id="toggle-chat" aria-expanded="true" aria-controls="chat">✦ Agents</button><button id="present" disabled>Present</button><a class="download" href="/deck.pptx">Download PPTX</a></header>
|
|
75
83
|
<div class="workspace">
|
|
76
84
|
<nav class="filmstrip" aria-label="Slides"><h2>Slides</h2><ol id="thumbnails"></ol></nav>
|
|
77
85
|
<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>
|
|
78
86
|
<aside id="chat" aria-label="Slide chat">
|
|
79
87
|
<div id="chat-resizer" role="separator" tabindex="0" aria-label="Chat width" aria-orientation="vertical" aria-controls="chat" title="Drag to resize · Double-click to reset"></div>
|
|
80
|
-
<div class="
|
|
81
|
-
<div id="chat-context">No slide selected</div>
|
|
82
|
-
<section id="claude-panel" aria-label="Claude Code terminal"><div class="terminal-toolbar"><span id="terminal-status" role="status">Start Claude Code to edit your slides.</span><button id="terminal-start">Start</button><button id="terminal-stop" hidden>End session</button></div><div id="terminal"></div></section>
|
|
83
|
-
<div id="codex-panel" hidden>
|
|
84
|
-
<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 Codex. Sign in through its CLI first. Sending a message allows it to edit this project.</p></div>
|
|
85
|
-
<form id="chat-form"><textarea id="chat-input" aria-label="Message" maxlength="16000" placeholder="Ask for a change…" required></textarea><div class="chat-actions"><span class="chat-shortcut">⌘ / Ctrl + Enter</span><button id="chat-stop" type="button" hidden>Stop</button><button id="chat-send" type="submit">Send</button></div></form>
|
|
86
|
-
<div id="chat-status" role="status">Ready</div>
|
|
87
|
-
</div>
|
|
88
|
-
</aside>
|
|
88
|
+
<div class="workspace-heading"><span>✦ AI WORKSPACE</span><b>LOCAL</b></div><div id="agent-workspace"></div><div id="chat-context" hidden></div></aside>
|
|
89
89
|
</div>
|
|
90
90
|
<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>
|
|
91
91
|
<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>
|
|
@@ -144,6 +144,7 @@ function selectSlide(next,focusThumbnail=false,reveal=true){
|
|
|
144
144
|
const count=state.slides.length?'Slide '+(index+1)+' of '+state.slides.length:'No slides';
|
|
145
145
|
byId('chat-context').textContent=state.slides.length?count:'Whole project';
|
|
146
146
|
byId('chat-context').dataset.focus=JSON.stringify({slide:state.slides.length?index:null,revision:state.revision??0});
|
|
147
|
+
window.dispatchEvent(new Event('agent-focus'));
|
|
147
148
|
byId('count').textContent=count;byId('present-count').textContent=count;
|
|
148
149
|
for(const id of ['prev','present-prev'])byId(id).disabled=index===0;
|
|
149
150
|
for(const id of ['next','present-next'])byId(id).disabled=index>=state.slides.length-1;
|
|
@@ -234,6 +235,30 @@ async function refresh(){
|
|
|
234
235
|
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);}}
|
|
235
236
|
catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}
|
|
236
237
|
}
|
|
238
|
+
byId('toggle-chat').onclick=()=>{const hidden=document.body.classList.toggle('chat-hidden');byId('toggle-chat').setAttribute('aria-expanded',String(!hidden));resize();};
|
|
239
|
+
const events=new EventSource('/events');events.onmessage=event=>{if(event.data==='chat'){window.dispatchEvent(new Event('agent-chat'));return;}if(event.data==='ready'){delete state.revision;}void refresh();};events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();
|
|
240
|
+
<\/script><script type="module" src="/terminal.js"><\/script></html>`;
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/agent-page.ts
|
|
243
|
+
const agentPage = `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><link rel="stylesheet" href="/terminal.css"><style>${previewStyles}
|
|
244
|
+
body{display:flex;flex-direction:column;background:#191e30;height:100dvh;min-width:0;overflow:auto;overflow-x:hidden} .chat-heading{flex-wrap:wrap} .chat-heading select{min-width:100px}.chat-heading,#chat-context{flex-shrink:0}#claude-panel{min-height:100px}#codex-panel{min-height:240px}
|
|
245
|
+
</style><div class="chat-heading"><select id="chat-provider" aria-label="AI provider"><option value="claude">Claude Code</option><option value="codex">Codex</option></select><button id="chat-reset" hidden>New chat</button></div>
|
|
246
|
+
<div id="chat-context">No slide selected</div>
|
|
247
|
+
<section id="claude-panel" aria-label="Claude Code terminal"><div class="terminal-toolbar"><span id="terminal-status" role="status">Start Claude Code to edit your slides.</span><button id="terminal-start">Start</button><button id="terminal-stop" hidden>End session</button></div><div class="terminal-frame"><div id="terminal"></div></div></section>
|
|
248
|
+
<div id="codex-panel" hidden>
|
|
249
|
+
<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 Codex. Sign in through its CLI first. Sending a message allows it to edit this project.</p></div>
|
|
250
|
+
<form id="chat-form"><textarea id="chat-input" aria-label="Message" maxlength="16000" placeholder="Ask for a change…" required></textarea><div class="chat-actions"><span class="chat-shortcut">⌘ / Ctrl + Enter</span><button id="chat-stop" type="button" hidden>Stop</button><button id="chat-send" type="submit">Send</button></div></form>
|
|
251
|
+
<div id="chat-status" role="status">Ready</div>
|
|
252
|
+
</div>
|
|
253
|
+
<script>
|
|
254
|
+
const byId=id=>document.getElementById(id),base=location.pathname;
|
|
255
|
+
let focus={slide:null,revision:0};
|
|
256
|
+
window.addEventListener('message',event=>{
|
|
257
|
+
if(event.origin!==location.origin||event.source!==parent)return;
|
|
258
|
+
if(event.data?.type==='focus'){focus=event.data.focus;byId('chat-context').dataset.focus=JSON.stringify(focus);byId('chat-context').textContent=event.data.label;}
|
|
259
|
+
if(event.data?.type==='chat')void refreshChat();
|
|
260
|
+
});
|
|
261
|
+
parent.postMessage({type:'agent-ready'},location.origin);
|
|
237
262
|
let chatRequest=0,chatBusy=false,lastMessages='';
|
|
238
263
|
function renderChat(chat){
|
|
239
264
|
chatBusy=chat.running;
|
|
@@ -259,12 +284,12 @@ function renderChat(chat){
|
|
|
259
284
|
}
|
|
260
285
|
async function refreshChat(){
|
|
261
286
|
const id=++chatRequest;
|
|
262
|
-
try{const response=await fetch('/chat');if(!response.ok)throw new Error('Chat unavailable');const chat=await response.json();if(id===chatRequest)renderChat(chat);}
|
|
287
|
+
try{const response=await fetch(base+'/chat');if(!response.ok)throw new Error('Chat unavailable');const chat=await response.json();if(id===chatRequest)renderChat(chat);}
|
|
263
288
|
catch(error){if(id===chatRequest)byId('chat-status').textContent=error.message;}
|
|
264
289
|
}
|
|
265
290
|
async function chatAction(path,payload){
|
|
266
291
|
++chatRequest;
|
|
267
|
-
const response=await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
|
292
|
+
const response=await fetch(base+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
|
268
293
|
const result=await response.json();if(!response.ok)throw new Error(result.error||'Chat request failed');
|
|
269
294
|
await refreshChat();
|
|
270
295
|
}
|
|
@@ -272,21 +297,27 @@ byId('chat-form').onsubmit=async event=>{
|
|
|
272
297
|
event.preventDefault();if(chatBusy)return;
|
|
273
298
|
const input=byId('chat-input'),message=input.value.trim();if(!message)return;
|
|
274
299
|
chatBusy=true;byId('chat-send').disabled=true;
|
|
275
|
-
try{await chatAction('/chat',{message,provider:byId('chat-provider').value
|
|
300
|
+
try{await chatAction('/chat',{message,provider:byId('chat-provider').value,...focus});if(input.value.trim()===message)input.value='';}
|
|
276
301
|
catch(error){byId('chat-status').textContent=error.message;chatBusy=false;byId('chat-send').disabled=false;}
|
|
277
302
|
};
|
|
278
303
|
byId('chat-input').onkeydown=event=>{if(event.key==='Enter'&&(event.metaKey||event.ctrlKey)&&!event.isComposing){event.preventDefault();byId('chat-form').requestSubmit();}};
|
|
279
304
|
for(const action of ['stop','reset'])byId('chat-'+action).onclick=()=>chatAction('/chat/'+action,{}).catch(error=>{byId('chat-status').textContent=error.message;});
|
|
280
|
-
byId('
|
|
281
|
-
byId('chat-provider').
|
|
305
|
+
byId('chat-provider').onchange=()=>{try{sessionStorage.setItem('office-kit-provider'+base,byId('chat-provider').value);}catch{}const claude=byId('chat-provider').value==='claude';byId('claude-panel').hidden=!claude;byId('codex-panel').hidden=claude;byId('chat-reset').hidden=claude;};
|
|
306
|
+
try{byId('chat-provider').value=sessionStorage.getItem('office-kit-provider'+base)||'claude';}catch{}
|
|
307
|
+
byId('chat-provider').onchange();
|
|
282
308
|
void refreshChat();
|
|
283
|
-
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();
|
|
284
309
|
<\/script><script type="module" src="/terminal.js"><\/script></html>`;
|
|
285
310
|
//#endregion
|
|
311
|
+
//#region src/authoring-guidance.ts
|
|
312
|
+
/** Shared guidance for agents editing a deck through the preview. */
|
|
313
|
+
const authoringGuidance = `For lists and tables of contents, use Text paragraphs/bullets or mapped Text rows. Bullets and TableOfContents are not built-in DSL components: inspect installed exports, import existing local helpers or define them. Do not use Raw for these layouts.
|
|
314
|
+
Choose foreground and actual background colors together. Explicitly set readable text colors on slides, cards, tables, chart labels and footnotes; check inherited theme styles. Aim for 4.5:1 contrast for normal text and 3:1 for large text (18pt or 14pt bold). Use a solid backing over images or gradients if needed. Inspect rendered slides; compilation alone cannot verify contrast.`;
|
|
315
|
+
//#endregion
|
|
286
316
|
//#region src/terminal.ts
|
|
287
317
|
const REQUEST_LIMIT = 128e3;
|
|
288
318
|
/** A real interactive Claude session, shared across reloads of its owning tab. */
|
|
289
|
-
function createTerminal(entry, busy = () => false) {
|
|
319
|
+
function createTerminal(entry, busy = () => false, base = "", verify = async () => null) {
|
|
320
|
+
let repairs = 0;
|
|
290
321
|
let process;
|
|
291
322
|
let owner = "";
|
|
292
323
|
let output = "";
|
|
@@ -327,17 +358,48 @@ function createTerminal(entry, busy = () => false) {
|
|
|
327
358
|
response.writeHead(code, { "Content-Type": "application/json" });
|
|
328
359
|
response.end(JSON.stringify(value));
|
|
329
360
|
};
|
|
330
|
-
if (request.url === "/terminal/context") {
|
|
361
|
+
if (request.url === "/terminal/context" || request.url === "/terminal/verify") {
|
|
331
362
|
if (request.method !== "POST" || request.headers.authorization !== `Bearer ${token}`) {
|
|
332
363
|
json(403, { error: "Invalid hook credentials" });
|
|
333
364
|
return;
|
|
334
365
|
}
|
|
335
366
|
request.resume();
|
|
367
|
+
if (request.url === "/terminal/verify") {
|
|
368
|
+
if (closing || stopping || !process) {
|
|
369
|
+
json(200, {});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
let error;
|
|
373
|
+
try {
|
|
374
|
+
error = await verify();
|
|
375
|
+
} catch (cause) {
|
|
376
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
377
|
+
}
|
|
378
|
+
if (closing || stopping || !process) {
|
|
379
|
+
json(200, {});
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (error && repairs < 3) {
|
|
383
|
+
repairs++;
|
|
384
|
+
status = `Repairing preview (${repairs}/3)…`;
|
|
385
|
+
json(200, {
|
|
386
|
+
decision: "block",
|
|
387
|
+
reason: `The actual preview build failed. Repair the source before finishing (attempt ${repairs}/3). Preserve unrelated edits, inspect the installed DSL exports and local components before changing imports. Do not hide the error or remove requested content. No Raw is needed for lists or an agenda: use Text paragraphs/bullets or mapped Text rows. Build error:\n${error.slice(0, 16e3)}`
|
|
388
|
+
});
|
|
389
|
+
} else {
|
|
390
|
+
status = error ? "Preview still has errors after 3 repair attempts." : "Preview build verified";
|
|
391
|
+
json(200, {});
|
|
392
|
+
}
|
|
393
|
+
emit("state", snapshot());
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
repairs = 0;
|
|
336
397
|
json(200, { hookSpecificOutput: {
|
|
337
398
|
hookEventName: "UserPromptSubmit",
|
|
338
399
|
additionalContext: `You are editing the local PowerPoint TSX project at ${resolve(entry)}.
|
|
339
400
|
Read the project's authoring instructions. Make focused source patches and preserve unrelated work.
|
|
340
|
-
The preview rebuilds on save; do not start another dev server.
|
|
401
|
+
The preview rebuilds on save; do not start another dev server. A Stop hook checks the actual build and returns errors for repair.
|
|
402
|
+
${authoringGuidance}
|
|
341
403
|
The focused slide is context, NOT a restriction. For "this slide", edit only its relevant source. For other slides or deck-wide requests, locate the relevant source or shared theme.
|
|
342
404
|
Slide numbers are 1-based. Verify source before editing; dependency paths are candidates, not an exact slide-to-file mapping.
|
|
343
405
|
Preview context captured with the latest terminal input: ${JSON.stringify(context)}`
|
|
@@ -375,7 +437,7 @@ Preview context captured with the latest terminal input: ${JSON.stringify(contex
|
|
|
375
437
|
return;
|
|
376
438
|
}
|
|
377
439
|
const size = () => {
|
|
378
|
-
if (!Number.isInteger(value.cols) || value.cols <
|
|
440
|
+
if (!Number.isInteger(value.cols) || value.cols < 2 || value.cols > 500 || !Number.isInteger(value.rows) || value.rows < 1 || value.rows > 300) throw new Error("Invalid terminal dimensions");
|
|
379
441
|
};
|
|
380
442
|
const updateFocus = () => {
|
|
381
443
|
if (!(value.slide === null || Number.isInteger(value.slide) && value.slide >= 0) || !Number.isInteger(value.revision)) throw new Error("Invalid preview context");
|
|
@@ -398,12 +460,20 @@ Preview context captured with the latest terminal input: ${JSON.stringify(contex
|
|
|
398
460
|
}
|
|
399
461
|
const pty = await import("node-pty");
|
|
400
462
|
if (process || closing) throw new Error("Terminal state changed. Try again.");
|
|
401
|
-
const settings = { hooks: {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
463
|
+
const settings = { hooks: {
|
|
464
|
+
Stop: [{ hooks: [{
|
|
465
|
+
type: "http",
|
|
466
|
+
url: `${origin}${base}/terminal/verify`,
|
|
467
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
468
|
+
timeout: 40
|
|
469
|
+
}] }],
|
|
470
|
+
UserPromptSubmit: [{ hooks: [{
|
|
471
|
+
type: "http",
|
|
472
|
+
url: `${origin}${base}/terminal/context`,
|
|
473
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
474
|
+
timeout: 5
|
|
475
|
+
}] }]
|
|
476
|
+
} };
|
|
407
477
|
const env = { ...globalThis.process.env };
|
|
408
478
|
delete env.CLAUDECODE;
|
|
409
479
|
process = pty.spawn("claude", ["--settings", JSON.stringify(settings)], {
|
|
@@ -470,7 +540,7 @@ const markdown = new MarkdownIt({
|
|
|
470
540
|
});
|
|
471
541
|
markdown.renderer.rules.image = (tokens, index) => markdown.utils.escapeHtml(tokens[index]?.content ?? "");
|
|
472
542
|
/** One shared conversation and one editing process per dev server. */
|
|
473
|
-
function createChat(entry, notify, busy = () => false) {
|
|
543
|
+
function createChat(entry, notify, busy = () => false, verify = async () => null) {
|
|
474
544
|
let messages = [];
|
|
475
545
|
let provider = "codex";
|
|
476
546
|
let child;
|
|
@@ -492,6 +562,10 @@ function createChat(entry, notify, busy = () => false) {
|
|
|
492
562
|
stopping = true;
|
|
493
563
|
status = "Stopping…";
|
|
494
564
|
const processToStop = child;
|
|
565
|
+
if (processToStop.exitCode !== null || processToStop.signalCode !== null) {
|
|
566
|
+
notify();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
495
569
|
const kill = (signal) => {
|
|
496
570
|
try {
|
|
497
571
|
if (process.platform !== "win32" && processToStop.pid) process.kill(-processToStop.pid, signal);
|
|
@@ -502,7 +576,7 @@ function createChat(entry, notify, busy = () => false) {
|
|
|
502
576
|
killTimer = setTimeout(() => kill("SIGKILL"), 2e3);
|
|
503
577
|
notify();
|
|
504
578
|
}
|
|
505
|
-
function start(text, selected, context) {
|
|
579
|
+
function start(text, selected, context, repairs = 0) {
|
|
506
580
|
provider = selected;
|
|
507
581
|
const user = {
|
|
508
582
|
role: "user",
|
|
@@ -516,6 +590,7 @@ function createChat(entry, notify, busy = () => false) {
|
|
|
516
590
|
const prompt = `You are editing a local PowerPoint TSX project in ${dirname(resolve(entry))}.
|
|
517
591
|
Read the project's authoring instructions. Make focused patches to the TSX source, preserving unrelated work.
|
|
518
592
|
The preview rebuilds automatically on save; do not start another dev server.
|
|
593
|
+
${authoringGuidance}
|
|
519
594
|
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.
|
|
520
595
|
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.
|
|
521
596
|
Respond in the user's language, briefly describing edits or answering their question. If permissions prevent an action, explain that instead of claiming success.
|
|
@@ -591,10 +666,31 @@ Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
|
591
666
|
running.on("error", (error) => {
|
|
592
667
|
failure = `Could not run ${selected}. Install it and log in from your terminal first. ${error.message}`;
|
|
593
668
|
});
|
|
594
|
-
running.on("close", (code) => {
|
|
669
|
+
running.on("close", async (code) => {
|
|
595
670
|
if (buffer.trim()) consume(buffer);
|
|
596
671
|
clearTimeout(killTimer);
|
|
597
672
|
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");
|
|
673
|
+
if (status === "Done") {
|
|
674
|
+
status = "Verifying preview…";
|
|
675
|
+
notify();
|
|
676
|
+
let buildError;
|
|
677
|
+
try {
|
|
678
|
+
buildError = await verify();
|
|
679
|
+
} catch (cause) {
|
|
680
|
+
buildError = cause instanceof Error ? cause.message : String(cause);
|
|
681
|
+
}
|
|
682
|
+
if (!stopping && buildError && repairs < 3) {
|
|
683
|
+
child = void 0;
|
|
684
|
+
start(`The actual preview build failed. Repair the source and preserve unrelated work (attempt ${repairs + 1}/3). Do not remove requested content or hide the error.\n${buildError.slice(0, 16e3)}`, selected, {
|
|
685
|
+
...context,
|
|
686
|
+
buildError
|
|
687
|
+
}, repairs + 1);
|
|
688
|
+
await completion;
|
|
689
|
+
done();
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
status = stopping ? "Stopped. Saved edits remain in the preview." : buildError ? "Preview still has errors after 3 repair attempts.\n" + buildError.slice(0, 16e3) : "Done";
|
|
693
|
+
}
|
|
598
694
|
if (status !== "Done") append(status);
|
|
599
695
|
child = void 0;
|
|
600
696
|
notify();
|
|
@@ -687,10 +783,23 @@ async function serveDeck(entry, port = 4173) {
|
|
|
687
783
|
let pending = false;
|
|
688
784
|
let closed = false;
|
|
689
785
|
const clients = /* @__PURE__ */ new Set();
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
786
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
787
|
+
function session(id) {
|
|
788
|
+
let result = sessions.get(id);
|
|
789
|
+
if (!result) {
|
|
790
|
+
const chat = createChat(entry, () => {
|
|
791
|
+
for (const client of clients) client.write("data: chat\n\n");
|
|
792
|
+
}, () => terminal.isRunning(), verify);
|
|
793
|
+
const terminal = createTerminal(entry, () => chat.isRunning(), id ? "/agents/" + id : "", verify);
|
|
794
|
+
result = {
|
|
795
|
+
chat,
|
|
796
|
+
terminal,
|
|
797
|
+
closing: false
|
|
798
|
+
};
|
|
799
|
+
sessions.set(id, result);
|
|
800
|
+
}
|
|
801
|
+
return result;
|
|
802
|
+
}
|
|
694
803
|
const server = createServer((request, response) => {
|
|
695
804
|
const host = request.headers.host;
|
|
696
805
|
if (host !== `127.0.0.1:${actualPort}` && host !== `localhost:${actualPort}`) {
|
|
@@ -698,20 +807,73 @@ async function serveDeck(entry, port = 4173) {
|
|
|
698
807
|
return;
|
|
699
808
|
}
|
|
700
809
|
response.setHeader("Cache-Control", "no-store");
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
return
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
810
|
+
const match = request.url?.match(/^\/agents\/([\w-]{1,64})(\/(?:chat(?:\/(?:stop|reset))?|terminal\/(?:start|input|resize|stop|events|context|verify)|close))?$/);
|
|
811
|
+
if (request.url?.startsWith("/agents/") && !match) {
|
|
812
|
+
response.writeHead(404).end();
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const id = match?.[1] ?? "";
|
|
816
|
+
if (match) {
|
|
817
|
+
if (request.headers["sec-fetch-site"] === "cross-site" || request.headers.origin && request.headers.origin !== `http://${host}`) {
|
|
818
|
+
response.writeHead(403).end();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (sessions.get(id)?.closing) {
|
|
822
|
+
response.writeHead(409).end("{\"error\":\"Agent is closing\"}");
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (match[2] === "/close") {
|
|
826
|
+
if (request.method !== "POST" || request.headers.origin !== `http://${host}` || request.headers["content-type"] !== "application/json") {
|
|
827
|
+
response.writeHead(403).end();
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
request.resume();
|
|
831
|
+
const current = sessions.get(id);
|
|
832
|
+
(async () => {
|
|
833
|
+
if (current) {
|
|
834
|
+
current.closing = true;
|
|
835
|
+
await Promise.all([current.terminal.close(), current.chat.close()]);
|
|
836
|
+
sessions.delete(id);
|
|
837
|
+
}
|
|
838
|
+
response.writeHead(200).end("{}");
|
|
839
|
+
})().catch(() => response.writeHead(500).end());
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (!sessions.has(id) && sessions.size >= 8) {
|
|
843
|
+
response.writeHead(429).end("Too many agent sessions. Close an existing pane.");
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (!match[2]) {
|
|
847
|
+
if (request.method !== "GET") {
|
|
848
|
+
response.writeHead(405).end();
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
session(id);
|
|
852
|
+
response.writeHead(200, { "Content-Type": "text/html" }).end(agentPage);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (!sessions.has(id)) {
|
|
856
|
+
response.writeHead(404).end();
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
request.url = match[2];
|
|
860
|
+
}
|
|
861
|
+
if (request.url === "/chat" || request.url?.startsWith("/chat/") || request.url?.startsWith("/terminal/")) {
|
|
862
|
+
const current = session(id);
|
|
863
|
+
(request.url.startsWith("/terminal/") ? current.terminal : current.chat).handle(request, response, (index, viewedRevision) => {
|
|
864
|
+
if (viewedRevision !== revision) throw new Error("Preview changed. Review the current slide and send again.");
|
|
865
|
+
if (index !== null && (!latest || index >= latest.slides.length)) throw new Error("Slide no longer exists.");
|
|
866
|
+
return {
|
|
867
|
+
slide: index === null ? null : index + 1,
|
|
868
|
+
count: latest?.slides.length ?? 0,
|
|
869
|
+
revision,
|
|
870
|
+
text: index === null ? "" : (latest?.slideTexts[index] ?? "").slice(0, 12e3),
|
|
871
|
+
entry: resolve(entry),
|
|
872
|
+
files: latest?.dependencies ?? [resolve(entry)],
|
|
873
|
+
buildError: error
|
|
874
|
+
};
|
|
875
|
+
});
|
|
876
|
+
} else if (request.method === "GET" && ["/terminal.js", "/terminal.css"].includes(request.url ?? "")) {
|
|
715
877
|
const asset = request.url === "/terminal.js" ? "terminal-client.js" : "terminal-client.css";
|
|
716
878
|
readFile(new URL(asset, import.meta.url)).then((bytes) => {
|
|
717
879
|
response.writeHead(200, { "Content-Type": asset.endsWith(".css") ? "text/css" : "text/javascript" });
|
|
@@ -749,6 +911,15 @@ async function serveDeck(entry, port = 4173) {
|
|
|
749
911
|
response.end(page);
|
|
750
912
|
} else response.writeHead(404).end();
|
|
751
913
|
});
|
|
914
|
+
async function verify() {
|
|
915
|
+
rebuild();
|
|
916
|
+
const deadline = Date.now() + 3e4;
|
|
917
|
+
while (!closed && (building || timer || pending)) {
|
|
918
|
+
if (Date.now() >= deadline) return "Preview verification timed out. Inspect the build before claiming success.";
|
|
919
|
+
await new Promise((done) => setTimeout(done, 50));
|
|
920
|
+
}
|
|
921
|
+
return closed ? "Preview server closed." : error;
|
|
922
|
+
}
|
|
752
923
|
async function rebuild() {
|
|
753
924
|
if (closed) return;
|
|
754
925
|
if (building) {
|
|
@@ -822,8 +993,7 @@ async function serveDeck(entry, port = 4173) {
|
|
|
822
993
|
closed = true;
|
|
823
994
|
clearTimeout(timer);
|
|
824
995
|
watcher.close();
|
|
825
|
-
await terminal.close();
|
|
826
|
-
await chat.close();
|
|
996
|
+
await Promise.all([...sessions.values()].flatMap(({ terminal, chat }) => [terminal.close(), chat.close()]));
|
|
827
997
|
await builder.close();
|
|
828
998
|
for (const client of clients) client.end();
|
|
829
999
|
await new Promise((done, reject) => server.close((cause) => cause ? reject(cause) : done()));
|