slidemind 0.1.8 → 0.1.9

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.
@@ -4,23 +4,27 @@
4
4
  // `slidemind plan` local server injects it), and dormant inside an iframe /
5
5
  // thumbnail render, so nothing runs on an ordinary page.
6
6
  //
7
- // The gate is editor-first: a plain markdown editor (the raw plan.md body) on
8
- // the left, a live rendered preview on the right — re-rendered client-side via
9
- // the same dependency-free renderer the server used (`plan-md.mjs`, shipped
10
- // inline as window.__SLIDEMIND_RENDER_MD__). The preview pane is collapsible
11
- // so users who just want the editor can hide it.
7
+ // The gate is a storyboard "dossier": the plan.md body is rendered into ONE
8
+ // scrollable document surface. A plan that carries ```storyboard-canvas```
9
+ // fences renders as the dossier — a red-thread tab strip + the selected
10
+ // slide's editable storyboard canvas + a details aside — instead of a raw
11
+ // editor. A plain-markdown plan renders its blocks inline-editable
12
+ // (contenteditable) directly. Both paths edit the SAME markdown string, mapped
13
+ // back through the plan modules exposed on window.__SLIDEMIND_PLAN__ (bundled
14
+ // alongside window.__SLIDEMIND_RENDER_MD__ by plan.mjs).
12
15
  //
13
16
  // The decision lives in ONE always-visible chat-style composer bar pinned at
14
- // the bottom (no per-block pins, nothing behind a pill): a chat input ("tell
15
- // the agent what to change…"), a round send button, and — fused to its right —
16
- // the Approve button, whose sub-label says what approving means: the agent
17
- // builds the slides next. The previous round's feedback shows as a read-only
18
- // note above the input.
19
- // - send → decision "comments" with the overall comment. The tab does
20
- // NOT veil over: the workspace stays readable (editor locks,
21
- // read-only) and the composer bar becomes a spinner + wait
22
- // note while the heartbeat polling waits for the agent's next
23
- // `slidemind plan` round, which reloads this same tab.
17
+ // the bottom: a feedback input, a round send button (a "story iteration" pill
18
+ // while storyline edits are staged) and — fused to its right — the Approve
19
+ // button, whose sub-label says the agent builds the slides next. The previous
20
+ // round's feedback shows as a read-only note above the input.
21
+ // - send → decision "comments" with the overall comment (plus, when the
22
+ // red thread was restructured, a generated story-iteration
23
+ // instruction + the edited plan markdown). The tab does NOT
24
+ // veil over: the surface locks read-only and the composer bar
25
+ // becomes a spinner + wait note while heartbeat polling waits
26
+ // for the agent's next `slidemind plan` round, which reloads
27
+ // this same tab.
24
28
  // - Approve → decision "approve" (unsent feedback arm-confirms first),
25
29
  // then the full-screen brand animation — logo + drifting
26
30
  // wordmark + "the agent is building your slides" — before the
@@ -40,25 +44,45 @@
40
44
  if (window.top !== window.self) return;
41
45
  if (new URLSearchParams(location.search).get("thumb") === "1") return;
42
46
 
43
- var PREVIEW_PREF_KEY = "slidemind-plan-preview";
44
- var RENDER_DEBOUNCE_MS = 150;
47
+ // The bundled plan modules (schema / storyboard-markdown / backbone / edit /
48
+ // renderer) hang off this namespace — see plan.mjs bundlePlanModules.
49
+ var PLAN = window.__SLIDEMIND_PLAN__ || {};
50
+
45
51
  var APPROVE_CLOSE_DELAY_MS = 2400; // long enough to read the build animation
46
52
  var ROUND_READY_FLAG = "slidemind-plan-round-ready"; // sessionStorage marker across the reload
47
53
  var ATTENTION_TITLE = "● Plan ready — slidemind";
48
54
  var WAITING_TITLE = "⏳ Agent revising — slidemind";
55
+ var BEAT_DRAG_THRESHOLD = 4; // px before a tab press becomes a drag
56
+ var BLOCK_DRAG_THRESHOLD = 3; // px before a block press becomes a drag
49
57
 
50
58
  // ---------- state ----------
51
59
  var submitted = false;
52
60
  var approveArmed = false; // inline "discard unsent feedback?" confirm state
53
- var previewOpen = true;
61
+ var waiting = false; // comments round sent → the surface locks read-only
54
62
  var baseline = CFG.planBody || ""; // the served plan body; edits diff against it
55
- var renderTimer = null;
56
63
 
57
- var bar, wsEl, edPane, previewPane, editorEl, commentEl, composerEl;
58
- var editedBadge, approveBtn, sendBtn, previewBtn;
64
+ // The single source of truth the whole overlay renders from. `value` is the
65
+ // frontmatter-less plan body (edits index into it); `selectedIndex` is the
66
+ // beat on stage; `storyDraft` is a staged red-thread restructure (null when
67
+ // none), which blocks Approve and turns "send" into a story-iteration round.
68
+ var S = { value: baseline, selectedIndex: 0, storyDraft: null };
69
+
70
+ var feedbackText = ""; // the composer input, kept across composer rebuilds
71
+ var pendingComment = null; // resolved comment/editedPlan for the next submit
72
+ var pendingEdited = null;
73
+ var editingKey = null; // the beat whose move text is in rename mode
74
+ var newSlideCounter = 0; // unique keys for inserted slides
75
+ var focusAfterRender = null; // an element to focus once a render settles
76
+
77
+ var bar, wsEl, surfaceEl, composerEl, commentEl;
78
+ var editedBadge, approveBtn, sendBtn;
59
79
  var waitTextEl = null;
60
80
  var notifyPermPromise = null; // resolved Notification permission, asked at the send gesture
61
81
 
82
+ function locked() {
83
+ return waiting;
84
+ }
85
+
62
86
  // ---------- brand assets (identical to review-overlay.js) ----------
63
87
  var LOGO_SVG =
64
88
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="160 160 960 540" preserveAspectRatio="none">' +
@@ -105,16 +129,24 @@
105
129
  var ICONS = {
106
130
  check:
107
131
  '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
108
- panel:
109
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/></svg>',
110
132
  ghost:
111
133
  '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11a9 9 0 0 1 18 0v10l-3-2-3 2-3-2-3 2-3-2-3 2Z"/><path d="M9 11h.01"/><path d="M15 11h.01"/></svg>',
112
134
  send:
113
135
  '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>',
136
+ sparkles:
137
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .962 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.582a.5.5 0 0 1 0 .962L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.962 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>',
138
+ plus:
139
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>',
140
+ x:
141
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
142
+ ruler:
143
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg>',
114
144
  };
115
145
 
116
- // ---------- styles (chrome copied from review-overlay.js; pins/popover/
117
- // sidebar rules dropped, editor/preview/dock rules added) ----------
146
+ // ---------- styles (chrome copied from review-overlay.js; the editor/preview
147
+ // split is replaced by the storyboard dossier — plan-markdown.css ported and
148
+ // remapped onto the overlay's dark --sm-* palette; the two structural hues
149
+ // #3565d4 (blueprint blue) and #b41830 (storyline red) are literals) ----------
118
150
  var CSS =
119
151
  "@keyframes sm-drift-a{0%,100%{transform:translateX(-5px)}50%{transform:translateX(5px)}}" +
120
152
  "@keyframes sm-drift-b{0%,100%{transform:translateX(5px)}50%{transform:translateX(-5px)}}" +
@@ -124,8 +156,15 @@
124
156
  "@keyframes sm-pulse{0%,100%{opacity:.45}50%{opacity:.85}}" +
125
157
  ":root{--sm-bg:var(--deck-chrome-bg,#111113);--sm-surface:var(--deck-chrome-bg-2,#161618);" +
126
158
  "--sm-border:var(--deck-chrome-border,#2b2b30);--sm-fg:var(--deck-chrome-fg,#ededed);" +
127
- "--sm-dim:var(--deck-chrome-dim,#9a9aa0);--sm-accent:#a9c2ff;--sm-success:#2ecc71;--sm-danger:#f87171;" +
128
- "--sm-warn:#f6c454;--sm-font:'Poppins',ui-sans-serif,system-ui,sans-serif;--sm-word:'Urbanist',var(--sm-font);}" +
159
+ "--sm-dim:var(--deck-chrome-dim,#9a9aa0);--sm-muted:rgba(255,255,255,.05);--sm-accent:#a9c2ff;" +
160
+ "--sm-success:#2ecc71;--sm-danger:#f87171;--sm-warn:#f6c454;" +
161
+ "--sm-font:'Poppins',ui-sans-serif,system-ui,sans-serif;--sm-word:'Urbanist',var(--sm-font);" +
162
+ /* the two structural hues, brightened against the dark surface */
163
+ "--plan-annotation:color-mix(in srgb,#3565d4 60%,var(--sm-fg));" +
164
+ "--plan-annotation-line:color-mix(in srgb,#3565d4 52%,transparent);" +
165
+ "--plan-annotation-wash:color-mix(in srgb,#3565d4 12%,transparent);" +
166
+ "--plan-story:color-mix(in srgb,#b41830 72%,var(--sm-fg));" +
167
+ "--plan-story-line:color-mix(in srgb,#b41830 45%,transparent);}" +
129
168
  ".sm-o,.sm-o *{box-sizing:border-box;}" +
130
169
  /* header */
131
170
  ".sm-bar{position:fixed;top:0;left:0;right:0;z-index:2147483600;display:flex;align-items:center;" +
@@ -142,7 +181,7 @@
142
181
  ".sm-word .top{animation:sm-drift-a 20s ease-in-out infinite;}" +
143
182
  ".sm-word .bot{margin-left:.18em;animation:sm-drift-b 20s ease-in-out infinite;}" +
144
183
  ".sm-sep{width:1px;height:28px;background:var(--sm-border);}" +
145
- ".sm-meta{display:flex;flex-direction:column;gap:2px;}" +
184
+ ".sm-meta{display:flex;flex-direction:column;gap:2px;min-width:0;}" +
146
185
  ".sm-meta .t{font-weight:600;font-size:13px;display:flex;align-items:center;gap:8px;}" +
147
186
  ".sm-meta .h{font-size:11.5px;color:var(--sm-dim);}" +
148
187
  ".sm-edited{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--sm-warn);" +
@@ -153,72 +192,201 @@
153
192
  "padding:8px 14px;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;" +
154
193
  "transition:transform .14s ease,background .14s ease,box-shadow .14s ease,border-color .14s ease;white-space:nowrap;}" +
155
194
  ".sm-btn svg{width:15px;height:15px;flex-shrink:0;}" +
156
- ".sm-btn-primary{background:#f4f4f5;color:#0f0f0f;}" +
157
- ".sm-btn-primary:hover{background:#fff;transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,.45);}" +
158
- ".sm-btn-primary svg{color:var(--sm-success);}" +
159
- ".sm-btn-primary.is-armed{background:#f6c454;color:#241a00;}" +
160
- ".sm-btn-primary.is-armed svg{color:#241a00;}" +
161
- ".sm-btn-ghost{background:transparent;border-color:var(--sm-border);color:var(--sm-fg);}" +
162
- ".sm-btn-ghost:hover{background:rgba(255,255,255,.06);border-color:#3a3a40;}" +
163
- ".sm-btn-ghost.is-on{background:rgba(169,194,255,.14);border-color:var(--sm-accent);color:#dbe6ff;}" +
164
- ".sm-btn.is-off{opacity:.4;cursor:not-allowed;pointer-events:none;}" +
165
- /* workspace: editor + preview panes, comment dock */
195
+ /* workspace: one scrollable document surface + composer dock */
166
196
  ".sm-ws{position:fixed;left:0;right:0;top:var(--deck-chrome-h,62px);bottom:0;z-index:2147483540;" +
167
197
  "display:flex;flex-direction:column;background:var(--sm-bg);}" +
168
- ".sm-panes{flex:1;display:flex;min-height:0;}" +
169
- ".sm-ed{flex:1;min-width:0;display:flex;flex-direction:column;border-right:1px solid var(--sm-border);}" +
170
- ".sm-panes.preview-hidden .sm-ed{border-right:0;}" +
171
- ".sm-ed-ta{flex:1;width:100%;resize:none;border:0;outline:none;background:#0c0c0e;color:#e8e8ea;" +
172
- "padding:22px 24px;font:14px/1.6 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;tab-size:2;}" +
173
- ".sm-prev{flex:1;min-width:0;overflow-y:auto;background:#0e0e10;}" +
174
- ".sm-panes.preview-hidden .sm-prev{display:none;}" +
175
- ".sm-prev #plan-root{padding:28px 30px 90px;}" +
176
- /* chat composer bar — always visible; owns the whole decision */
198
+ ".sm-ws-surface{flex:1;min-height:0;overflow-y:auto;background:var(--sm-bg);padding:22px 22px 30px;}" +
199
+ ".sm-ws.is-waiting .sm-ws-surface{pointer-events:none;opacity:.62;}" +
200
+ /* markdown document (Mode A + the storyboard headings share this scale) */
201
+ ".plan-md-root{max-width:min(1120px,100%);margin:0 auto;line-height:1.65;font-size:15.5px;color:var(--sm-fg);}" +
202
+ ".plan-md-root--storyboards{max-width:100%;display:flex;flex-direction:column;gap:0;}" +
203
+ ".plan-md-root h1,.plan-md-root h2,.plan-md-root h3,.plan-md-root h4,.plan-md-root h5,.plan-md-root h6{" +
204
+ "color:var(--sm-fg);font-weight:600;line-height:1.28;margin:1.6em 0 .5em;}" +
205
+ ".plan-md-root h1{font-size:1.95em;margin-top:.2em;}" +
206
+ ".plan-md-root h2{font-size:1.5em;padding-bottom:.24em;border-bottom:1px solid var(--sm-border);}" +
207
+ ".plan-md-root h3{font-size:1.22em;}.plan-md-root h4{font-size:1.05em;}" +
208
+ ".plan-md-root h5,.plan-md-root h6{font-size:.95em;color:var(--sm-dim);}" +
209
+ ".plan-md-root p{margin:.7em 0;}" +
210
+ ".plan-md-root ul,.plan-md-root ol{margin:.6em 0;padding-left:1.5em;}" +
211
+ ".plan-md-root ul{list-style:disc;}.plan-md-root ol{list-style:decimal;}" +
212
+ ".plan-md-root li{margin:.3em 0;}" +
213
+ ".plan-md-root blockquote{margin:.95em 0;padding:.45em 1em;border-left:3px solid var(--sm-border);" +
214
+ "color:var(--sm-dim);background:var(--sm-muted);border-radius:0 6px 6px 0;}" +
215
+ ".plan-md-root code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;" +
216
+ "background:var(--sm-muted);padding:.12em .42em;border-radius:5px;}" +
217
+ ".plan-md-root pre{background:#0c0c0e;border:1px solid var(--sm-border);border-radius:10px;padding:14px 16px;overflow-x:auto;line-height:1.5;}" +
218
+ ".plan-md-root pre code{background:none;padding:0;font-size:.86em;}" +
219
+ ".plan-md-root strong{color:var(--sm-fg);font-weight:600;}.plan-md-root em{font-style:italic;}" +
220
+ ".plan-md-root a{color:var(--sm-accent);}" +
221
+ ".plan-md-root [contenteditable=\"true\"]{border:0;background:transparent;border-radius:0;box-shadow:none;caret-color:currentColor;outline:none;}" +
222
+ ".plan-md-root [contenteditable=\"true\"]:focus{outline:none;box-shadow:none;background:transparent;}" +
223
+ /* dossier card */
224
+ ".plan-dossier{max-width:1120px;margin:0 auto;border:1px solid var(--sm-border);border-radius:14px;" +
225
+ "background:var(--sm-surface);overflow:hidden;}" +
226
+ ".plan-dossier-body{display:flex;flex-direction:column;gap:16px;padding:18px 20px 20px;}" +
227
+ ".plan-dossier-new-slide{display:flex;align-items:flex-start;gap:10px;border:1.5px dashed var(--plan-story-line);" +
228
+ "border-radius:10px;background:color-mix(in srgb,#b41830 6%,transparent);color:var(--sm-dim);font-size:13px;line-height:1.5;padding:22px 18px;}" +
229
+ ".plan-dossier-new-slide svg{width:14px;height:14px;flex:none;margin-top:2px;color:var(--plan-story);}" +
230
+ ".plan-dossier-new-slide p{margin:0;}.plan-dossier-new-slide strong{color:var(--sm-fg);font-weight:600;}" +
231
+ /* hand-off note */
232
+ ".plan-storyboard-draft-note{display:flex;align-items:flex-start;gap:10px;margin:0;padding:9px 13px;" +
233
+ "border:1px dashed color-mix(in srgb,var(--sm-dim) 45%,transparent);border-radius:9px;background:var(--sm-muted);" +
234
+ "color:var(--sm-dim);font-size:12px;line-height:1.5;}" +
235
+ ".plan-storyboard-draft-note svg{width:14px;height:14px;flex:none;margin-top:2px;}" +
236
+ ".plan-storyboard-draft-note p{margin:0;}.plan-storyboard-draft-note strong{color:var(--sm-fg);font-weight:600;}" +
237
+ /* storyline tab strip */
238
+ ".plan-storyline{margin:0;}" +
239
+ ".plan-storyline-head{display:flex;flex-direction:column;align-items:flex-start;gap:3px;padding:16px 20px 12px;}" +
240
+ ".plan-storyline-label{color:var(--plan-story);font-size:9px;font-weight:700;letter-spacing:.18em;line-height:1.2;text-transform:uppercase;}" +
241
+ ".plan-storyline-track{display:flex;flex-wrap:wrap;align-items:stretch;margin:0;padding:4px 14px 10px;list-style:none;" +
242
+ "border-bottom:1px solid var(--sm-border);row-gap:2px;}" +
243
+ ".plan-storyline-beat{position:relative;display:flex;flex:1 1 150px;min-width:132px;max-width:260px;flex-direction:row;" +
244
+ "align-items:baseline;gap:9px;padding:8px 8px 9px;border:0;border-radius:8px;background:transparent;cursor:pointer;transition:background-color 130ms ease;}" +
245
+ ".plan-storyline-beat:hover{background:var(--sm-muted);}" +
246
+ ".plan-storyline-beat--active{background:color-mix(in srgb,var(--sm-fg) 8%,transparent);}" +
247
+ ".plan-storyline-beat--draggable{touch-action:none;}" +
248
+ ".plan-storyline-beat--dragging{z-index:20;cursor:grabbing;background:var(--sm-surface);box-shadow:0 16px 36px rgba(0,0,0,.5);transition:none;}" +
249
+ ".plan-storyline-beat--editing{cursor:text;}" +
250
+ ".plan-storyline-beat-slide{flex:none;color:color-mix(in srgb,var(--sm-dim) 55%,transparent);" +
251
+ "font-family:Georgia,'Times New Roman',serif;font-size:21px;font-weight:400;line-height:1;transition:color 130ms ease;}" +
252
+ ".plan-storyline-beat--active .plan-storyline-beat-slide{color:var(--plan-story);}" +
253
+ ".plan-storyline-beat-text{display:-webkit-box;min-width:0;overflow:hidden;color:var(--sm-dim);font-size:12px;" +
254
+ "font-weight:500;line-height:1.35;-webkit-box-orient:vertical;-webkit-line-clamp:2;}" +
255
+ ".plan-storyline-beat--active .plan-storyline-beat-text{color:var(--sm-fg);font-weight:600;}" +
256
+ ".plan-storyline-beat-text[contenteditable=\"true\"]{display:block;border-radius:4px;cursor:text;outline:none;-webkit-line-clamp:unset;}" +
257
+ ".plan-storyline-gap{position:relative;display:flex;flex:none;align-items:center;justify-content:center;width:22px;}" +
258
+ ".plan-storyline-gap[data-active=\"true\"]::before{position:absolute;top:18%;bottom:18%;left:50%;width:3px;border-radius:2px;background:var(--plan-story);content:\"\";transform:translateX(-50%);}" +
259
+ ".plan-storyline-insert{position:relative;display:flex;align-items:center;justify-content:center;width:18px;height:18px;" +
260
+ "border:1px solid var(--sm-border);border-radius:999px;background:var(--sm-surface);color:var(--sm-dim);cursor:pointer;" +
261
+ "opacity:0;padding:0;pointer-events:none;transform:scale(.85);transition:opacity 140ms ease,transform 140ms ease,background-color 140ms ease,border-color 140ms ease,color 140ms ease;}" +
262
+ ".plan-storyline-insert svg{width:12px;height:12px;}" +
263
+ ".plan-storyline-track:hover .plan-storyline-insert,.plan-storyline-insert:focus-visible{opacity:1;pointer-events:auto;transform:scale(1);}" +
264
+ ".plan-storyline-insert:hover{background:var(--plan-story);border-color:var(--plan-story);color:var(--sm-bg);transform:scale(1.1);}" +
265
+ ".plan-storyline-beat-remove{position:absolute;top:-7px;right:-7px;display:inline-flex;align-items:center;justify-content:center;" +
266
+ "width:19px;height:19px;border:1px solid var(--sm-border);border-radius:999px;background:var(--sm-surface);box-shadow:0 2px 6px rgba(0,0,0,.35);" +
267
+ "color:var(--sm-dim);cursor:pointer;opacity:0;padding:0;pointer-events:none;transition:opacity 120ms ease;}" +
268
+ ".plan-storyline-beat-remove svg{width:12px;height:12px;}" +
269
+ ".plan-storyline-beat:hover .plan-storyline-beat-remove{opacity:1;pointer-events:auto;}" +
270
+ ".plan-storyline-beat--dragging .plan-storyline-beat-remove,.plan-storyline-beat--editing .plan-storyline-beat-remove{opacity:0;pointer-events:none;}" +
271
+ ".plan-storyline-beat-remove:disabled{cursor:default;opacity:0;}" +
272
+ /* storyboard canvas */
273
+ ".plan-storyboard{margin:0;}" +
274
+ ".plan-storyboard-header{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:8px;" +
275
+ "font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--sm-dim);}" +
276
+ ".plan-storyboard-legend{display:inline-flex;flex:none;align-items:baseline;gap:10px;letter-spacing:0;text-transform:none;}" +
277
+ ".plan-storyboard-legend-item--element{border:1px dashed var(--plan-annotation-line);border-radius:4px;background:var(--plan-annotation-wash);" +
278
+ "color:var(--plan-annotation);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:8.5px;font-weight:650;letter-spacing:.08em;padding:2px 6px;text-transform:uppercase;}" +
279
+ ".plan-storyboard-legend-item--content{color:var(--sm-fg);font-size:10.5px;font-weight:500;}" +
280
+ ".plan-storyboard-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(200px,26%);gap:14px;align-items:start;}" +
281
+ ".plan-storyboard-frame{position:relative;aspect-ratio:16/9;width:100%;overflow:hidden;border:0;border-radius:8px;" +
282
+ "background:color-mix(in srgb,var(--sm-bg) 78%,white);}" +
283
+ ".slide-emboss{box-shadow:0 1px 2px rgba(0,0,0,.45),0 10px 26px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.06);}" +
284
+ ".plan-storyboard-reserved{position:absolute;left:0;right:0;display:flex;align-items:center;justify-content:center;" +
285
+ "background:repeating-linear-gradient(-45deg,var(--sm-muted),var(--sm-muted) 8px,transparent 8px,transparent 16px);" +
286
+ "color:var(--sm-dim);font-size:clamp(9px,1.6vw,11px);font-weight:500;}" +
287
+ ".plan-storyboard-reserved-top{top:0;height:12.2%;border-bottom:1px solid var(--sm-border);}" +
288
+ ".plan-storyboard-reserved-bottom{bottom:0;height:13.3%;border-top:1px solid var(--sm-border);}" +
289
+ ".plan-storyboard-body{position:absolute;inset:12.2% 5% 13.3%;" +
290
+ "background:linear-gradient(color-mix(in srgb,var(--plan-annotation) 13%,transparent) 1px,transparent 1px)," +
291
+ "linear-gradient(90deg,color-mix(in srgb,var(--plan-annotation) 13%,transparent) 1px,transparent 1px);" +
292
+ "background-size:calc(100%/12) calc(100%/6);background-color:color-mix(in srgb,#3565d4 3%,transparent);}" +
293
+ ".plan-storyboard-block{position:absolute;display:flex;min-width:0;min-height:0;flex-direction:column;gap:clamp(4px,.72vw,7px);" +
294
+ "overflow:hidden;border:1.5px dashed color-mix(in srgb,var(--plan-annotation) 44%,transparent);border-radius:6px;" +
295
+ "background:color-mix(in srgb,var(--sm-surface) 62%,transparent);padding:clamp(6px,.95vw,10px);}" +
296
+ ".plan-storyboard-block[data-emphasis=\"primary\"]{border-width:2px;border-color:color-mix(in srgb,var(--plan-annotation) 70%,transparent);}" +
297
+ ".plan-storyboard-block[data-emphasis=\"tertiary\"]{opacity:.78;}" +
298
+ ".plan-storyboard-block--movable{cursor:grab;touch-action:none;}" +
299
+ ".plan-storyboard-block--movable:hover{border-color:color-mix(in srgb,var(--plan-annotation) 66%,transparent);}" +
300
+ ".plan-storyboard-block--dragging{z-index:10;cursor:grabbing;border-style:solid;box-shadow:0 10px 26px rgba(0,0,0,.45);}" +
301
+ ".plan-storyboard-block-remove{position:absolute;top:3px;right:3px;display:inline-flex;align-items:center;justify-content:center;" +
302
+ "width:18px;height:18px;border:1px solid var(--sm-border);border-radius:999px;background:var(--sm-surface);box-shadow:0 2px 6px rgba(0,0,0,.35);" +
303
+ "color:var(--sm-dim);cursor:pointer;opacity:0;padding:0;pointer-events:none;transition:opacity 120ms ease;}" +
304
+ ".plan-storyboard-block-remove svg{width:12px;height:12px;}" +
305
+ ".plan-storyboard-block:hover .plan-storyboard-block-remove{opacity:1;pointer-events:auto;}" +
306
+ ".plan-storyboard-block--dragging .plan-storyboard-block-remove{opacity:0;pointer-events:none;}" +
307
+ ".plan-storyboard-block-resize{position:absolute;right:0;bottom:0;width:16px;height:16px;border:0;border-radius:0 0 5px 0;" +
308
+ "background:linear-gradient(135deg,transparent 0 52%,var(--plan-annotation-line) 52% 60%,transparent 60% 72%,var(--plan-annotation-line) 72% 80%,transparent 80%);" +
309
+ "cursor:nwse-resize;opacity:0;padding:0;touch-action:none;transition:opacity 120ms ease;}" +
310
+ ".plan-storyboard-block:hover .plan-storyboard-block-resize,.plan-storyboard-block--dragging .plan-storyboard-block-resize{opacity:1;}" +
311
+ ".plan-storyboard-reserved-top .plan-storyboard-add{position:absolute;top:50%;right:10px;transform:translateY(-50%);}" +
312
+ ".plan-storyboard-add{display:inline-flex;align-items:center;gap:4px;border:1px dashed var(--plan-annotation-line);border-radius:5px;" +
313
+ "background:var(--plan-annotation-wash);color:var(--plan-annotation);cursor:pointer;font-size:10px;font-weight:600;letter-spacing:.02em;padding:2px 7px;" +
314
+ "font-family:var(--sm-font);transition:background-color 130ms ease,border-color 130ms ease;}" +
315
+ ".plan-storyboard-add svg{width:11px;height:11px;}" +
316
+ ".plan-storyboard-add:hover:not(:disabled){border-color:var(--plan-annotation);background:color-mix(in srgb,#3565d4 18%,transparent);}" +
317
+ ".plan-storyboard-add:disabled{cursor:default;opacity:.45;}" +
318
+ ".plan-storyboard-field{min-width:0;}" +
319
+ ".plan-storyboard-field-control{display:block;width:100%;min-height:1.25em;overflow:hidden;border:0;background:transparent;padding:0;" +
320
+ "color:var(--sm-fg);font:inherit;line-height:1.16;outline:none;box-shadow:none;white-space:normal;word-break:break-word;}" +
321
+ ".plan-storyboard-field-control:focus{outline:none;box-shadow:none;background:transparent;}" +
322
+ ".plan-storyboard-field[data-field=\"element\"]{display:inline-flex;width:fit-content;max-width:100%;flex-direction:column;" +
323
+ "border:1px dashed var(--plan-annotation-line);border-radius:4px;background:var(--plan-annotation-wash);padding:3px 6px;}" +
324
+ ".plan-storyboard-field[data-field=\"element\"] .plan-storyboard-field-control{color:var(--plan-annotation);" +
325
+ "font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.07em;line-height:1.15;text-transform:uppercase;}" +
326
+ ".plan-storyboard-field[data-field=\"content\"] .plan-storyboard-field-control{color:var(--sm-fg);font-weight:500;line-height:1.18;}" +
327
+ ".plan-storyboard-detail{position:sticky;top:0;max-height:min(62vh,620px);overflow:auto;padding:30px 0 0;}" +
328
+ ".plan-storyboard-detail-label{margin:0 0 5px;color:var(--sm-dim);font-size:10px;font-weight:700;letter-spacing:.04em;line-height:1.2;text-transform:uppercase;}" +
329
+ ".plan-storyboard-key-message{margin:0 0 18px;color:var(--sm-fg);font-size:15px;font-weight:650;line-height:1.34;white-space:pre-wrap;word-break:break-word;outline:none;}" +
330
+ ".plan-storyboard-detail-text{min-height:9em;border:0;background:transparent;color:var(--sm-fg);font-size:13.5px;font-weight:400;line-height:1.42;outline:none;white-space:pre-wrap;word-break:break-word;}" +
331
+ ".plan-storyboard-warnings{margin:.75em 0 0;padding-left:1.2em;font-size:.86em;color:var(--sm-dim);}" +
332
+ ".plan-storyboard-error{margin:1em 0;border:1px solid var(--sm-danger);border-radius:8px;background:color-mix(in srgb,var(--sm-danger) 10%,transparent);padding:12px;}" +
333
+ ".plan-storyboard-error p{margin:0 0 8px;font-weight:600;}" +
334
+ ".plan-storyboard-error pre{margin:0;overflow-x:auto;font-size:.82em;color:var(--sm-dim);}" +
335
+ "@media (max-width:900px){.plan-storyline-beat{max-width:none;}.plan-storyboard-layout{grid-template-columns:1fr;}" +
336
+ ".plan-storyboard-detail{position:static;max-height:none;padding-top:8px;}}" +
337
+ /* chat composer bar */
177
338
  ".sm-composer{flex-shrink:0;display:flex;flex-direction:column;gap:8px;padding:11px 16px;" +
178
339
  "border-top:1px solid var(--sm-border);background:var(--sm-surface);font-family:var(--sm-font);}" +
179
- ".sm-prev-note{display:flex;align-items:flex-start;gap:8px;font-size:12px;color:var(--sm-dim);" +
180
- "line-height:1.5;padding:7px 10px;border:1px dashed #3a3a40;border-radius:9px;background:rgba(255,255,255,.02);}" +
340
+ ".sm-story-banner{display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:12px;line-height:1.5;" +
341
+ "border:1px solid color-mix(in srgb,#b41830 30%,transparent);border-radius:9px;background:color-mix(in srgb,#b41830 8%,transparent);}" +
342
+ ".sm-story-banner svg{width:14px;height:14px;flex-shrink:0;color:var(--plan-story);}" +
343
+ ".sm-story-banner .txt{flex:1;min-width:0;color:var(--sm-dim);}" +
344
+ ".sm-story-banner .txt b{color:var(--sm-fg);font-weight:600;}" +
345
+ ".sm-story-discard{flex-shrink:0;border:0;background:transparent;color:var(--sm-dim);font-family:var(--sm-font);font-weight:500;" +
346
+ "font-size:12px;padding:4px 8px;border-radius:6px;cursor:pointer;transition:background .12s,color .12s;}" +
347
+ ".sm-story-discard:hover{background:var(--sm-muted);color:var(--sm-fg);}" +
348
+ ".sm-prev-note{display:flex;align-items:flex-start;gap:8px;font-size:12px;color:var(--sm-dim);line-height:1.5;" +
349
+ "padding:7px 10px;border:1px dashed #3a3a40;border-radius:9px;background:var(--sm-muted);}" +
181
350
  ".sm-prev-note svg{width:14px;height:14px;flex-shrink:0;margin-top:2px;color:#71717a;}" +
182
351
  ".sm-prev-note .lbl{font-weight:600;color:#a1a1aa;white-space:nowrap;}" +
183
352
  ".sm-prev-note .txt{white-space:pre-wrap;word-break:break-word;color:#b9b9be;}" +
184
353
  ".sm-composer-row{display:flex;align-items:flex-end;gap:10px;}" +
185
354
  ".sm-chat-ta{flex:1;min-height:40px;max-height:150px;resize:none;background:#0c0c0e;color:var(--sm-fg);" +
186
- "border:1px solid var(--sm-border);border-radius:14px;padding:10px 15px;font:13px/1.5 var(--sm-font);" +
187
- "outline:none;transition:border-color .15s;}" +
355
+ "border:1px solid var(--sm-border);border-radius:14px;padding:10px 15px;font:13px/1.5 var(--sm-font);outline:none;transition:border-color .15s;}" +
188
356
  ".sm-chat-ta:focus{border-color:var(--sm-accent);}.sm-chat-ta::placeholder{color:#6b6b72;}" +
189
- ".sm-send{width:38px;height:38px;border-radius:50%;border:0;cursor:pointer;flex-shrink:0;" +
190
- "display:flex;align-items:center;justify-content:center;background:var(--sm-accent);color:#0f1220;" +
191
- "transition:transform .14s ease,box-shadow .14s ease,opacity .14s ease;}" +
357
+ ".sm-send{width:38px;height:38px;border-radius:50%;border:0;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;" +
358
+ "background:var(--sm-accent);color:#0f1220;transition:transform .14s ease,box-shadow .14s ease,opacity .14s ease;}" +
192
359
  ".sm-send svg{width:16px;height:16px;margin:1px 1px 0 0;}" +
193
360
  ".sm-send:hover{transform:translateY(-1px) scale(1.05);box-shadow:0 8px 22px rgba(169,194,255,.35);}" +
194
361
  ".sm-send.is-off{opacity:.35;cursor:not-allowed;pointer-events:none;}" +
362
+ /* story-iteration pill (send while a storyline draft is staged) */
363
+ ".sm-send-pill{display:inline-flex;align-items:center;gap:7px;height:38px;flex-shrink:0;border:0;border-radius:999px;" +
364
+ "padding:0 16px;background:var(--sm-accent);color:#0f1220;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;white-space:nowrap;" +
365
+ "transition:transform .14s ease,box-shadow .14s ease,opacity .14s ease;}" +
366
+ ".sm-send-pill svg{width:15px;height:15px;}" +
367
+ ".sm-send-pill:hover{transform:translateY(-1px);box-shadow:0 8px 22px rgba(169,194,255,.35);}" +
368
+ ".sm-send-pill.is-off{opacity:.35;cursor:not-allowed;pointer-events:none;}" +
195
369
  ".sm-composer-sep{width:1px;align-self:stretch;background:var(--sm-border);}" +
196
- /* approve — the sub-label announces the consequence: the agent builds next */
197
- ".sm-approve{display:inline-flex;flex-direction:column;align-items:center;gap:1px;border:0;" +
198
- "border-radius:12px;padding:7px 16px;background:#f4f4f5;color:#0f0f0f;cursor:pointer;font-family:var(--sm-font);" +
199
- "transition:transform .14s ease,background .14s ease,box-shadow .14s ease;white-space:nowrap;}" +
370
+ ".sm-approve{display:inline-flex;flex-direction:column;align-items:center;gap:1px;border:0;border-radius:12px;padding:7px 16px;" +
371
+ "background:#f4f4f5;color:#0f0f0f;cursor:pointer;font-family:var(--sm-font);transition:transform .14s ease,background .14s ease,box-shadow .14s ease;white-space:nowrap;}" +
200
372
  ".sm-approve:hover{background:#fff;transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,.45);}" +
201
373
  ".sm-approve .main{display:flex;align-items:center;gap:7px;font-weight:600;font-size:12.5px;}" +
202
374
  ".sm-approve .main svg{width:15px;height:15px;color:var(--sm-success);}" +
203
375
  ".sm-approve .sub{font-size:9.5px;font-weight:500;color:#61616a;}" +
204
376
  ".sm-approve.is-armed{background:#f6c454;color:#241a00;}" +
205
377
  ".sm-approve.is-armed .main svg{color:#241a00;}" +
206
- /* waiting state — feedback sent, the agent is revising; tab stays readable */
378
+ ".sm-approve.is-off{opacity:.45;cursor:not-allowed;pointer-events:none;}" +
379
+ /* waiting state */
207
380
  "@keyframes sm-rotate{to{transform:rotate(360deg)}}" +
208
- ".sm-wait{display:flex;align-items:center;gap:12px;padding:7px 2px;font-size:12.5px;" +
209
- "color:var(--sm-dim);line-height:1.5;}" +
381
+ ".sm-wait{display:flex;align-items:center;gap:12px;padding:7px 2px;font-size:12.5px;color:var(--sm-dim);line-height:1.5;}" +
210
382
  ".sm-wait b{color:var(--sm-fg);font-weight:600;}" +
211
- ".sm-spin{width:17px;height:17px;border-radius:50%;border:2px solid var(--sm-border);" +
212
- "border-top-color:var(--sm-accent);animation:sm-rotate .9s linear infinite;flex-shrink:0;}" +
213
- ".sm-ws.is-waiting .sm-ed-ta{opacity:.55;}" +
383
+ ".sm-spin{width:17px;height:17px;border-radius:50%;border:2px solid var(--sm-border);border-top-color:var(--sm-accent);animation:sm-rotate .9s linear infinite;flex-shrink:0;}" +
214
384
  /* done veil */
215
- ".sm-veil{position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;" +
216
- "justify-content:center;gap:30px;font-family:var(--sm-font);color:var(--sm-fg);" +
217
- "background:radial-gradient(ellipse at center,rgba(16,16,18,.93),rgba(5,5,7,.98));" +
385
+ ".sm-veil{position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:30px;" +
386
+ "font-family:var(--sm-font);color:var(--sm-fg);background:radial-gradient(ellipse at center,rgba(16,16,18,.93),rgba(5,5,7,.98));" +
218
387
  "-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);animation:sm-veil-in .4s ease;}" +
219
388
  ".sm-veil .sm-logo{width:188px;height:106px;border-radius:14px;animation:sm-rise .6s cubic-bezier(.2,.7,.2,1) both;}" +
220
- ".sm-veil .word{display:flex;flex-direction:column;gap:6px;font-family:var(--sm-word);font-weight:300;" +
221
- "font-size:36px;letter-spacing:.6em;text-align:center;animation:sm-rise .6s .06s cubic-bezier(.2,.7,.2,1) both;}" +
389
+ ".sm-veil .word{display:flex;flex-direction:column;gap:6px;font-family:var(--sm-word);font-weight:300;font-size:36px;letter-spacing:.6em;text-align:center;animation:sm-rise .6s .06s cubic-bezier(.2,.7,.2,1) both;}" +
222
390
  ".sm-veil .word b{font-weight:300;}" +
223
391
  ".sm-veil .word .top{animation:sm-drift-a 18s ease-in-out infinite;}" +
224
392
  ".sm-veil .word .bot{margin-left:.3em;animation:sm-drift-b 18s ease-in-out infinite;}" +
@@ -234,9 +402,6 @@
234
402
  if (html != null) e.innerHTML = html;
235
403
  return e;
236
404
  }
237
- function planRoot() {
238
- return document.getElementById("plan-root");
239
- }
240
405
  function wordmarkHtml() {
241
406
  return '<b class="top">slide</b><b class="bot">mind</b>';
242
407
  }
@@ -254,20 +419,28 @@
254
419
  box.appendChild(el("div", "sm-logo-svg", idSuffix ? logoWithIds(idSuffix) : LOGO_SVG));
255
420
  return box;
256
421
  }
257
- function mkButton(label, variant, iconName) {
258
- var b = el("button", "sm-btn sm-btn-" + variant);
259
- b.innerHTML = (iconName ? ICONS[iconName] : "") + "<span>" + label + "</span>";
260
- return b;
261
- }
262
- function btnLabel(b, text) {
263
- var s = b.querySelector("span");
264
- if (s) s.textContent = text;
422
+ function clamp(v, min, max) {
423
+ return Math.min(Math.max(v, min), max);
265
424
  }
266
425
  function planWasEdited() {
267
- return !!editorEl && editorEl.value !== baseline;
426
+ return S.value !== baseline;
268
427
  }
269
428
  function commentText() {
270
- return commentEl ? commentEl.value.trim() : "";
429
+ return (commentEl ? commentEl.value : feedbackText).trim();
430
+ }
431
+ function placeCaretAtEnd(node) {
432
+ try {
433
+ node.focus();
434
+ var sel = window.getSelection();
435
+ if (!sel) return;
436
+ var range = document.createRange();
437
+ range.selectNodeContents(node);
438
+ range.collapse(false);
439
+ sel.removeAllRanges();
440
+ sel.addRange(range);
441
+ } catch {
442
+ /* headless / detached — ignore */
443
+ }
271
444
  }
272
445
 
273
446
  // ---------- chrome (top bar) ----------
@@ -289,95 +462,965 @@
289
462
  title.appendChild(editedBadge);
290
463
  meta.appendChild(title);
291
464
  meta.appendChild(
292
- el("div", "h", "Edit the markdown directly · the preview follows · decide below when it reads right"),
465
+ el(
466
+ "div",
467
+ "h",
468
+ "Shape the red thread and storyboards below · drag to reorder, rename moves, edit blocks · decide when it reads right",
469
+ ),
293
470
  );
294
471
  bar.appendChild(meta);
295
-
296
472
  bar.appendChild(el("div", "sm-grow"));
297
473
 
298
- previewBtn = mkButton("Preview", "ghost", "panel");
299
- previewBtn.onclick = togglePreview;
300
- bar.appendChild(previewBtn);
301
-
302
474
  document.body.appendChild(bar);
303
475
  }
304
476
  function refreshChrome() {
305
477
  if (editedBadge) editedBadge.style.display = planWasEdited() ? "" : "none";
306
478
 
307
479
  var hasFeedback = commentText().length > 0;
308
- if (sendBtn) {
309
- sendBtn.classList.toggle("is-off", !hasFeedback);
310
- }
480
+ var canSend = !!S.storyDraft || hasFeedback;
481
+ if (sendBtn) sendBtn.classList.toggle("is-off", !canSend);
482
+
311
483
  if (approveBtn) {
312
484
  var main = approveBtn.querySelector(".main span");
313
485
  var sub = approveBtn.querySelector(".sub");
314
- if (approveArmed && hasFeedback) {
315
- approveBtn.classList.add("is-armed");
316
- if (main) main.textContent = "Feedback not sent — approve anyway?";
317
- if (sub) sub.style.display = "none";
318
- } else {
486
+ if (S.storyDraft) {
487
+ // Approve is blocked until the staged storyline is sent or discarded.
319
488
  approveArmed = false;
320
489
  approveBtn.classList.remove("is-armed");
490
+ approveBtn.classList.add("is-off");
321
491
  if (main) main.textContent = "Approve plan";
322
- if (sub) sub.style.display = "";
492
+ if (sub) {
493
+ sub.style.display = "";
494
+ sub.textContent = "Send or discard your storyline changes first";
495
+ }
496
+ } else {
497
+ approveBtn.classList.remove("is-off");
498
+ if (approveArmed && hasFeedback) {
499
+ approveBtn.classList.add("is-armed");
500
+ if (main) main.textContent = "Feedback not sent — approve anyway?";
501
+ if (sub) sub.style.display = "none";
502
+ } else {
503
+ approveArmed = false;
504
+ approveBtn.classList.remove("is-armed");
505
+ if (main) main.textContent = "Approve plan";
506
+ if (sub) {
507
+ sub.style.display = "";
508
+ sub.textContent = "the agent builds the slides next";
509
+ }
510
+ }
323
511
  }
324
512
  }
325
- if (previewBtn) {
326
- previewBtn.classList.toggle("is-on", previewOpen);
327
- btnLabel(previewBtn, previewOpen ? "Hide preview" : "Show preview");
328
- }
329
513
  }
330
514
 
331
- // ---------- workspace: editor + live preview + comment dock ----------
515
+ // ---------- workspace: one scrollable document surface + composer ----------
332
516
  function buildWorkspace() {
333
517
  wsEl = el("div", "sm-o sm-ws");
334
518
  wsEl.setAttribute("data-sm-overlay", "1");
335
519
 
336
- var panes = el("div", "sm-panes");
520
+ surfaceEl = el("div", "sm-ws-surface");
521
+ // Delegated inline-edit commit for Mode A rendered markdown blocks. Attached
522
+ // to the persistent surface so a Mode-B re-render (which replaces the
523
+ // surface's children) keeps these live. commitRenderedEdit is a no-op for
524
+ // anything that isn't a stamped, editable, non-storyboard block.
525
+ surfaceEl.addEventListener(
526
+ "blur",
527
+ function (e) {
528
+ commitRenderedEdit(e.target);
529
+ },
530
+ true,
531
+ );
532
+ surfaceEl.addEventListener(
533
+ "keydown",
534
+ function (e) {
535
+ if (e.key !== "Enter" || e.shiftKey) return;
536
+ var t = e.target;
537
+ if (!(t instanceof HTMLElement)) return;
538
+ if (t.closest(".plan-storyboard")) return;
539
+ if (!t.closest("[data-md-line][data-md-kind]")) return;
540
+ e.preventDefault();
541
+ commitRenderedEdit(t);
542
+ t.blur();
543
+ },
544
+ true,
545
+ );
546
+ wsEl.appendChild(surfaceEl);
547
+ document.body.appendChild(wsEl);
548
+
549
+ composerEl = el("div", "sm-composer");
550
+ composerEl.setAttribute("data-sm-overlay", "1");
551
+ wsEl.appendChild(composerEl);
552
+
553
+ renderSurface();
554
+ renderComposer();
555
+ }
337
556
 
338
- edPane = el("div", "sm-ed");
339
- editorEl = el("textarea", "sm-ed-ta");
340
- editorEl.spellcheck = false;
341
- editorEl.value = baseline;
342
- editorEl.addEventListener("input", function () {
343
- schedulePreview();
557
+ // Map an edited Mode-A block's innerText back into S.value at its stamped
558
+ // source line WITHOUT re-rendering the node (caret preservation).
559
+ function commitRenderedEdit(target) {
560
+ if (locked()) return;
561
+ if (!(target instanceof HTMLElement)) return;
562
+ var editable = target.closest("[data-md-line][data-md-kind]");
563
+ if (!editable || editable.closest(".plan-storyboard")) return;
564
+ var next = PLAN.replaceRenderedMarkdownBlock({
565
+ markdown: S.value,
566
+ line: Number(editable.dataset.mdLine),
567
+ kind: editable.dataset.mdKind || "",
568
+ text: editable.innerText != null ? editable.innerText : editable.textContent || "",
569
+ });
570
+ if (next !== S.value) {
571
+ S.value = next;
572
+ approveArmed = false; // any plan edit disarms a pending approve-confirm
344
573
  refreshChrome();
574
+ }
575
+ }
576
+
577
+ // ---------- surface render ----------
578
+ // A plan with storyboard fences renders as the dossier; otherwise the plain
579
+ // markdown renders inline-editable. Mode B re-renders in full on every
580
+ // committed model change (nothing is focused at commit — edits commit on
581
+ // blur / pointerup — so no caret is lost); Mode A never re-renders here.
582
+ function renderSurface() {
583
+ if (!surfaceEl) return;
584
+ surfaceEl.innerHTML = "";
585
+ focusAfterRender = null;
586
+ var segments = PLAN.splitPlanMarkdownIntoSegments(S.value);
587
+ var hasStoryboards = segments.some(function (s) {
588
+ return s.type === "storyboard";
345
589
  });
346
- // Tab indents instead of walking focus out of the editor.
347
- editorEl.addEventListener("keydown", function (e) {
348
- if (e.key === "Tab") {
349
- e.preventDefault();
350
- var s = editorEl.selectionStart;
351
- var t = editorEl.selectionEnd;
352
- editorEl.value = editorEl.value.slice(0, s) + " " + editorEl.value.slice(t);
353
- editorEl.selectionStart = editorEl.selectionEnd = s + 2;
354
- schedulePreview();
355
- refreshChrome();
590
+ if (hasStoryboards) renderDossier(segments);
591
+ else renderModeA(segments);
592
+ if (focusAfterRender) {
593
+ placeCaretAtEnd(focusAfterRender);
594
+ focusAfterRender = null;
595
+ }
596
+ }
597
+
598
+ function renderModeA(segments) {
599
+ var root = el("div", "plan-md-root");
600
+ segments.forEach(function (seg) {
601
+ if (seg.type !== "markdown") return;
602
+ var html = PLAN.renderPlanMarkdown(seg.text, { lineOffset: seg.lineOffset });
603
+ if (!locked()) html = PLAN.addEditableAttributes(html);
604
+ var holder = el("div");
605
+ holder.innerHTML = html;
606
+ while (holder.firstChild) root.appendChild(holder.firstChild);
607
+ });
608
+ surfaceEl.appendChild(root);
609
+ }
610
+
611
+ // ---------- dossier (storyboard mode) ----------
612
+ function renderDossier(segments) {
613
+ var storyboardSegments = segments.filter(function (s) {
614
+ return s.type === "storyboard";
615
+ });
616
+ var storyboardData = storyboardSegments
617
+ .map(function (s) {
618
+ return s.data;
619
+ })
620
+ .filter(Boolean);
621
+ var redThreadMessages = PLAN.extractRedThreadMessages(S.value);
622
+ // Tabs need a beat per board even without a `## Red thread` section — fall
623
+ // back to each board's key message (read-only tabs then).
624
+ var beatMessages = redThreadMessages.length
625
+ ? redThreadMessages
626
+ : storyboardSegments.map(function (seg, i) {
627
+ return (seg.data && seg.data.keyMessage) || "Slide " + ((seg.data && seg.data.slide) || i + 1);
628
+ });
629
+ var committed = PLAN.backboneFromMessages(beatMessages);
630
+ var slides = S.storyDraft || committed;
631
+ var backboneEditable = PLAN.parsePlanBackbone(S.value) !== null;
632
+ var canEdit = !locked() && backboneEditable;
633
+
634
+ if (S.selectedIndex >= slides.length) S.selectedIndex = slides.length - 1;
635
+ if (S.selectedIndex < 0) S.selectedIndex = 0;
636
+ var activeIndex = S.selectedIndex;
637
+
638
+ // A staged draft resolves a tab to the board of the slide it originally was;
639
+ // new slides (originalIndex null) have no board yet.
640
+ var activeSegment;
641
+ if (S.storyDraft) {
642
+ var oi = slides[activeIndex] ? slides[activeIndex].originalIndex : undefined;
643
+ activeSegment = oi === null || oi === undefined ? undefined : storyboardSegments[oi];
644
+ } else {
645
+ activeSegment = storyboardSegments[activeIndex];
646
+ }
647
+
648
+ var dossier = el("div", "plan-dossier");
649
+ var root = el("div", "plan-md-root plan-md-root--storyboards");
650
+ dossier.appendChild(root);
651
+
652
+ root.appendChild(
653
+ renderStrip(slides, committed, beatMessages, storyboardData, canEdit, activeIndex),
654
+ );
655
+
656
+ var body = el("div", "plan-dossier-body");
657
+ if (activeSegment) {
658
+ if (activeSegment.data) {
659
+ body.appendChild(renderCanvas(activeSegment));
660
+ } else {
661
+ var err = el("div", "plan-storyboard-error");
662
+ err.appendChild(el("p", null, "Storyboard block could not be rendered."));
663
+ var pre = el("pre");
664
+ var codeNode = document.createElement("code");
665
+ codeNode.textContent = activeSegment.raw || "";
666
+ pre.appendChild(codeNode);
667
+ err.appendChild(pre);
668
+ body.appendChild(err);
356
669
  }
670
+ } else {
671
+ // A staged new slide (or a beat with no board yet): the agent builds its
672
+ // storyboard once the story round is sent.
673
+ var placeholder = el("div", "plan-dossier-new-slide");
674
+ placeholder.innerHTML =
675
+ ICONS.ruler +
676
+ "<p><strong>Storyline edited.</strong> Sending asks the agent to build storyboards and content for your new and renamed slides — add comments below if you like.</p>";
677
+ body.appendChild(placeholder);
678
+ }
679
+
680
+ var note = el("div", "plan-storyboard-draft-note");
681
+ note.innerHTML =
682
+ ICONS.ruler +
683
+ "<p><strong>Draft storyboard.</strong> A rough sketch of what goes where — it guides the design agent, and the finished slides will follow your corporate design.</p>";
684
+ body.appendChild(note);
685
+
686
+ root.appendChild(body);
687
+ surfaceEl.appendChild(dossier);
688
+ }
689
+
690
+ // ---------- storyline tab strip ----------
691
+ function renderStrip(slides, committed, committedMessages, storyboardData, canEdit, activeIndex) {
692
+ var section = el("section", "plan-storyline");
693
+ var head = el("div", "plan-storyline-head");
694
+ head.appendChild(el("span", "plan-storyline-label", "Red thread"));
695
+ section.appendChild(head);
696
+
697
+ var track = el("ol", "plan-storyline-track");
698
+ section.appendChild(track);
699
+
700
+ var cardEls = [];
701
+ var gapEls = [];
702
+ var stripDrag = null;
703
+ var dropGap = null;
704
+
705
+ function paintGaps() {
706
+ for (var i = 0; i < gapEls.length; i++) {
707
+ if (stripDrag && dropGap === i) gapEls[i].setAttribute("data-active", "true");
708
+ else gapEls[i].removeAttribute("data-active");
709
+ }
710
+ }
711
+ function nearestGap(cx, cy) {
712
+ var best = 0;
713
+ var bd = Infinity;
714
+ for (var i = 0; i < slides.length; i++) {
715
+ var card = cardEls[i];
716
+ if (!card) continue;
717
+ var r = card.getBoundingClientRect();
718
+ var dy = cy - (r.top + r.height / 2);
719
+ var before = (cx - r.left) * (cx - r.left) + dy * dy;
720
+ if (before < bd) {
721
+ bd = before;
722
+ best = i;
723
+ }
724
+ var after = (cx - r.right) * (cx - r.right) + dy * dy;
725
+ if (after < bd) {
726
+ bd = after;
727
+ best = i + 1;
728
+ }
729
+ }
730
+ return best;
731
+ }
732
+
733
+ // Sequence-only edits (reorder / remove) cascade to the storyboards now;
734
+ // renames / inserts stage a draft the agent has to build.
735
+ function beatCommit(next) {
736
+ var sequenceOnly = next.every(function (s) {
737
+ return s.originalIndex !== null && committedMessages[s.originalIndex] === s.message;
738
+ });
739
+ if (sequenceOnly) {
740
+ S.storyDraft = null;
741
+ var applied = PLAN.applyPlanBackbone(S.value, next);
742
+ if (applied) S.value = applied;
743
+ } else {
744
+ S.storyDraft = next;
745
+ }
746
+ approveArmed = false; // a storyline edit disarms a pending approve-confirm
747
+ refreshChrome();
748
+ renderComposer();
749
+ renderSurface();
750
+ }
751
+ function renameSlide(index, message) {
752
+ var cur = slides[index];
753
+ if (!cur || cur.message === message) {
754
+ editingKey = null;
755
+ renderSurface();
756
+ return;
757
+ }
758
+ editingKey = null;
759
+ beatCommit(
760
+ slides.map(function (s, i) {
761
+ return i === index ? { key: s.key, originalIndex: s.originalIndex, message: message } : s;
762
+ }),
763
+ );
764
+ }
765
+ function insertSlideAt(position) {
766
+ newSlideCounter += 1;
767
+ var next = slides.slice();
768
+ next.splice(position, 0, {
769
+ key: "new-" + newSlideCounter,
770
+ originalIndex: null,
771
+ message: "New slide",
772
+ });
773
+ S.selectedIndex = position;
774
+ beatCommit(next);
775
+ }
776
+ function removeSlide(index) {
777
+ if (slides.length <= 1) return;
778
+ var next = slides.filter(function (_, i) {
779
+ return i !== index;
780
+ });
781
+ if (index < S.selectedIndex) S.selectedIndex = S.selectedIndex - 1;
782
+ else if (index === S.selectedIndex) S.selectedIndex = Math.min(index, slides.length - 2);
783
+ beatCommit(next);
784
+ }
785
+ function reorderTo(from, gap) {
786
+ var next = slides.slice();
787
+ var moved = next.splice(from, 1)[0];
788
+ if (!moved) return;
789
+ var target = from < gap ? gap - 1 : gap;
790
+ if (target === from) return;
791
+ next.splice(target, 0, moved);
792
+ S.selectedIndex = target;
793
+ beatCommit(next);
794
+ }
795
+
796
+ function gapAt(position) {
797
+ var li = el("li", "plan-storyline-gap");
798
+ li.setAttribute("aria-hidden", canEdit ? "false" : "true");
799
+ if (canEdit) {
800
+ var btn = el("button", "plan-storyline-insert", ICONS.plus);
801
+ btn.type = "button";
802
+ btn.title = "Insert slide here";
803
+ btn.setAttribute("aria-label", "Insert slide here");
804
+ btn.onclick = function () {
805
+ insertSlideAt(position);
806
+ };
807
+ li.appendChild(btn);
808
+ }
809
+ gapEls[position] = li;
810
+ return li;
811
+ }
812
+
813
+ slides.forEach(function (item, index) {
814
+ track.appendChild(gapAt(index));
815
+
816
+ var li = el(
817
+ "li",
818
+ "plan-storyline-beat" +
819
+ (index === activeIndex ? " plan-storyline-beat--active" : "") +
820
+ (canEdit ? " plan-storyline-beat--draggable" : "") +
821
+ (editingKey === item.key ? " plan-storyline-beat--editing" : ""),
822
+ );
823
+ cardEls[index] = li;
824
+
825
+ var numeral = S.storyDraft ? index + 1 : (storyboardData[index] && storyboardData[index].slide) || index + 1;
826
+ var slideNo = el("div", "plan-storyline-beat-slide");
827
+ slideNo.setAttribute("aria-hidden", "true");
828
+ slideNo.textContent = numeral;
829
+ li.appendChild(slideNo);
830
+
831
+ var textEl = el("div", "plan-storyline-beat-text");
832
+ textEl.setAttribute("role", "textbox");
833
+ textEl.textContent = item.message;
834
+ var isEditing = canEdit && editingKey === item.key;
835
+ if (isEditing) {
836
+ textEl.contentEditable = "true";
837
+ textEl.spellcheck = true;
838
+ focusAfterRender = textEl;
839
+ // Commit-once guard: renameSlide re-renders and detaches this focused
840
+ // node, firing a second blur that must not commit again.
841
+ var renamed = false;
842
+ textEl.addEventListener("blur", function () {
843
+ if (renamed) return;
844
+ renamed = true;
845
+ var t = (textEl.textContent || "").replace(/\s+/g, " ").trim();
846
+ if (t) renameSlide(index, t);
847
+ else {
848
+ editingKey = null;
849
+ renderSurface();
850
+ }
851
+ });
852
+ textEl.addEventListener("keydown", function (e) {
853
+ if (e.key === "Enter") {
854
+ e.preventDefault();
855
+ textEl.blur();
856
+ } else if (e.key === "Escape") {
857
+ textEl.textContent = item.message;
858
+ editingKey = null;
859
+ textEl.blur();
860
+ }
861
+ });
862
+ }
863
+ li.appendChild(textEl);
864
+
865
+ if (canEdit) {
866
+ var rm = el("button", "plan-storyline-beat-remove", ICONS.x);
867
+ rm.type = "button";
868
+ rm.setAttribute("data-no-drag", "");
869
+ rm.title = "Remove slide";
870
+ rm.setAttribute("aria-label", "Remove slide");
871
+ rm.disabled = slides.length <= 1;
872
+ rm.addEventListener("pointerdown", function (e) {
873
+ e.stopPropagation();
874
+ });
875
+ rm.addEventListener("click", function () {
876
+ removeSlide(index);
877
+ });
878
+ li.appendChild(rm);
879
+ }
880
+
881
+ // Pointer: drag ≥ threshold reorders; a click selects; a click on the
882
+ // active tab (when editable) enters rename mode.
883
+ li.addEventListener("pointerdown", function (e) {
884
+ if (e.button !== 0) return;
885
+ if (editingKey === item.key) return;
886
+ if (e.target.closest && e.target.closest("[data-no-drag]")) return;
887
+ stripDrag = {
888
+ index: index,
889
+ key: item.key,
890
+ pointerId: e.pointerId,
891
+ ox: e.clientX,
892
+ oy: e.clientY,
893
+ moved: false,
894
+ };
895
+ try {
896
+ li.setPointerCapture(e.pointerId);
897
+ } catch {
898
+ /* headless */
899
+ }
900
+ });
901
+ li.addEventListener("pointermove", function (e) {
902
+ if (!stripDrag || stripDrag.pointerId !== e.pointerId) return;
903
+ if (!canEdit) return;
904
+ var dx = e.clientX - stripDrag.ox;
905
+ var dy = e.clientY - stripDrag.oy;
906
+ if (!stripDrag.moved && Math.hypot(dx, dy) < BEAT_DRAG_THRESHOLD) return;
907
+ stripDrag.moved = true;
908
+ li.classList.add("plan-storyline-beat--dragging");
909
+ li.style.transform = "translate(" + dx + "px," + dy + "px)";
910
+ dropGap = nearestGap(e.clientX, e.clientY);
911
+ paintGaps();
912
+ });
913
+ function endBeat(e) {
914
+ if (!stripDrag || stripDrag.pointerId !== e.pointerId) return;
915
+ try {
916
+ li.releasePointerCapture(e.pointerId);
917
+ } catch {
918
+ /* ignore */
919
+ }
920
+ var moved = stripDrag.moved;
921
+ var gap = dropGap;
922
+ var idx = stripDrag.index;
923
+ var key = stripDrag.key;
924
+ stripDrag = null;
925
+ dropGap = null;
926
+ li.style.transform = "";
927
+ li.classList.remove("plan-storyline-beat--dragging");
928
+ paintGaps();
929
+ if (moved) {
930
+ if (gap !== null) reorderTo(idx, gap);
931
+ } else if (idx !== activeIndex) {
932
+ S.selectedIndex = idx;
933
+ renderSurface();
934
+ } else if (canEdit) {
935
+ editingKey = key;
936
+ renderSurface();
937
+ }
938
+ }
939
+ li.addEventListener("pointerup", endBeat);
940
+ li.addEventListener("pointercancel", function (e) {
941
+ if (!stripDrag || stripDrag.pointerId !== e.pointerId) return;
942
+ stripDrag = null;
943
+ dropGap = null;
944
+ li.style.transform = "";
945
+ li.classList.remove("plan-storyline-beat--dragging");
946
+ paintGaps();
947
+ });
948
+
949
+ track.appendChild(li);
357
950
  });
358
- edPane.appendChild(editorEl);
359
- panes.appendChild(edPane);
951
+ track.appendChild(gapAt(slides.length));
360
952
 
361
- // The preview pane adopts the server-rendered #plan-root; live re-renders
362
- // replace its innerHTML from the editor content.
363
- previewPane = el("div", "sm-prev");
364
- var root = planRoot();
365
- if (root) previewPane.appendChild(root);
366
- panes.appendChild(previewPane);
953
+ return section;
954
+ }
367
955
 
368
- wsEl.appendChild(panes);
369
- document.body.appendChild(wsEl);
370
- applyPreviewState(readPreviewPref());
956
+ // ---------- storyboard canvas ----------
957
+ function fittedFontSize(value, variant) {
958
+ var len = value.trim().length;
959
+ if (variant === "element") return Math.max(8.5, 12.5 - Math.max(0, len - 14) * 0.22);
960
+ return Math.max(8, 13 - Math.max(0, len - 12) * 0.3);
961
+ }
962
+ function compactSingleLine(text, variant) {
963
+ var lim = variant === "element" ? PLAN.MAX_DESIGN_ELEMENT_LENGTH : PLAN.MAX_CONTENT_LENGTH;
964
+ return text
965
+ .replace(/\u00a0/g, " ")
966
+ .replace(/\s+/g, " ")
967
+ .trim()
968
+ .slice(0, lim);
969
+ }
970
+ function notesToText(notes) {
971
+ return notes && notes.length ? notes.join("\n") : "";
972
+ }
973
+ function textToNotes(text) {
974
+ var notes = text
975
+ .replace(/\u00a0/g, " ")
976
+ .split(/\r?\n/)
977
+ .map(function (l) {
978
+ return l.trim();
979
+ })
980
+ .filter(Boolean);
981
+ return notes.length ? notes : undefined;
982
+ }
983
+ function updateBlock(data, blockId, patch) {
984
+ var blocks = data.blocks.map(function (b) {
985
+ if (b.id !== blockId) return b;
986
+ var next = {};
987
+ for (var k in b) next[k] = b[k];
988
+ for (var p in patch) next[p] = patch[p];
989
+ return next;
990
+ });
991
+ return withBlocks(data, blocks);
992
+ }
993
+ function withBlocks(data, blocks) {
994
+ var next = {};
995
+ for (var k in data) next[k] = data[k];
996
+ next.blocks = blocks;
997
+ return next;
998
+ }
999
+
1000
+ // Commit a storyboard mutation into S.value and re-render the surface. Safe to
1001
+ // full-re-render because every canvas edit commits on blur / pointerup, so no
1002
+ // field is focused at commit time.
1003
+ function commitStoryboard(segment, nextData) {
1004
+ if (locked()) return;
1005
+ S.value = PLAN.replaceStoryboardSegment(S.value, segment, nextData);
1006
+ approveArmed = false; // a storyboard edit disarms a pending approve-confirm
1007
+ refreshChrome();
1008
+ renderSurface();
1009
+ }
1010
+
1011
+ function renderField(variant, value, onCommit) {
1012
+ var wrap = el("div", "plan-storyboard-field");
1013
+ wrap.setAttribute("data-field", variant);
1014
+ var ctrl = el("div", "plan-storyboard-field-control");
1015
+ ctrl.setAttribute("role", "textbox");
1016
+ ctrl.setAttribute("aria-label", variant === "element" ? "Design element" : "Content");
1017
+ ctrl.textContent = value;
1018
+ ctrl.style.fontSize = fittedFontSize(value, variant) + "px";
1019
+ if (!locked()) {
1020
+ ctrl.contentEditable = "true";
1021
+ ctrl.spellcheck = true;
1022
+ }
1023
+ // Commit exactly once per edit session. A commit re-renders the surface,
1024
+ // which detaches this (still-focused) node and fires a SECOND blur; the
1025
+ // `committed` guard makes that second blur a no-op so we never splice the
1026
+ // stale `segment` offsets back in (double-commit → corrupted fences).
1027
+ var committed = false;
1028
+ function commit() {
1029
+ if (committed) return;
1030
+ committed = true;
1031
+ var normalized = compactSingleLine(ctrl.textContent || "", variant);
1032
+ ctrl.textContent = normalized;
1033
+ ctrl.style.fontSize = fittedFontSize(normalized, variant) + "px";
1034
+ if (normalized !== value) onCommit(normalized);
1035
+ }
1036
+ ctrl.addEventListener("input", function () {
1037
+ ctrl.style.fontSize = fittedFontSize(ctrl.textContent || "", variant) + "px";
1038
+ });
1039
+ ctrl.addEventListener("blur", commit);
1040
+ // Enter blurs and lets the blur handler be the single committer (matches the
1041
+ // beat-rename pattern) — never commit directly here, or the follow-on blur
1042
+ // would commit a second time against the mutated markdown.
1043
+ ctrl.addEventListener("keydown", function (e) {
1044
+ if (e.key !== "Enter") return;
1045
+ e.preventDefault();
1046
+ ctrl.blur();
1047
+ });
1048
+ wrap.appendChild(ctrl);
1049
+ return wrap;
1050
+ }
1051
+
1052
+ function renderBlock(frameEl, block, data, segment, canRemove) {
1053
+ var node = el("div", "plan-storyboard-block" + (locked() ? "" : " plan-storyboard-block--movable"));
1054
+ node.setAttribute("data-emphasis", block.emphasis || "secondary");
1055
+ node.style.left = (block.x / PLAN.STORYBOARD_WIDTH) * 100 + "%";
1056
+ node.style.top = (block.y / PLAN.STORYBOARD_HEIGHT) * 100 + "%";
1057
+ node.style.width = (block.w / PLAN.STORYBOARD_WIDTH) * 100 + "%";
1058
+ node.style.height = (block.h / PLAN.STORYBOARD_HEIGHT) * 100 + "%";
1059
+
1060
+ function change(patch) {
1061
+ commitStoryboard(segment, updateBlock(data, block.id, patch));
1062
+ }
1063
+
1064
+ node.appendChild(
1065
+ renderField("element", block.designElement, function (v) {
1066
+ change({ designElement: v });
1067
+ }),
1068
+ );
1069
+ node.appendChild(
1070
+ renderField("content", block.content, function (v) {
1071
+ change({ content: v });
1072
+ }),
1073
+ );
1074
+
1075
+ if (!locked() && canRemove) {
1076
+ var rm = el("button", "plan-storyboard-block-remove", ICONS.x);
1077
+ rm.type = "button";
1078
+ rm.setAttribute("data-no-drag", "");
1079
+ rm.title = "Remove container";
1080
+ rm.setAttribute("aria-label", "Remove container");
1081
+ rm.addEventListener("pointerdown", function (e) {
1082
+ e.stopPropagation();
1083
+ });
1084
+ rm.addEventListener("click", function () {
1085
+ if (data.blocks.length <= 1) return;
1086
+ commitStoryboard(
1087
+ segment,
1088
+ withBlocks(
1089
+ data,
1090
+ data.blocks.filter(function (b) {
1091
+ return b.id !== block.id;
1092
+ }),
1093
+ ),
1094
+ );
1095
+ });
1096
+ node.appendChild(rm);
1097
+ }
1098
+
1099
+ if (!locked()) {
1100
+ node.title = "Drag to reposition";
1101
+ wireBlockDrag(node, frameEl, block, change);
1102
+ var rz = el("button", "plan-storyboard-block-resize");
1103
+ rz.type = "button";
1104
+ rz.setAttribute("data-no-drag", "");
1105
+ rz.title = "Drag to resize";
1106
+ rz.setAttribute("aria-label", "Drag to resize");
1107
+ wireBlockResize(rz, frameEl, block, change);
1108
+ node.appendChild(rz);
1109
+ }
1110
+ return node;
1111
+ }
1112
+
1113
+ function clampPos(block, x, y) {
1114
+ var A = PLAN.BODY_SAFE_AREA;
1115
+ return {
1116
+ x: clamp(x, A.x, A.x + A.w - block.w),
1117
+ y: clamp(y, A.y, A.y + A.h - block.h),
1118
+ };
1119
+ }
1120
+ function clampSize(block, w, h) {
1121
+ var A = PLAN.BODY_SAFE_AREA;
1122
+ return {
1123
+ w: clamp(w, PLAN.MIN_BLOCK_WIDTH, A.x + A.w - block.x),
1124
+ h: clamp(h, PLAN.MIN_BLOCK_HEIGHT, A.y + A.h - block.y),
1125
+ };
1126
+ }
1127
+ function wireBlockDrag(node, frameEl, block, change) {
1128
+ var st = null;
1129
+ node.addEventListener("pointerdown", function (e) {
1130
+ if (locked() || e.button !== 0) return;
1131
+ if (e.target.closest && e.target.closest(".plan-storyboard-field, [data-no-drag]")) return;
1132
+ var rect = frameEl.getBoundingClientRect();
1133
+ if (!rect || rect.width <= 0) return;
1134
+ st = {
1135
+ pointerId: e.pointerId,
1136
+ ox: e.clientX,
1137
+ oy: e.clientY,
1138
+ sx: block.x,
1139
+ sy: block.y,
1140
+ scale: PLAN.STORYBOARD_WIDTH / rect.width,
1141
+ moved: false,
1142
+ };
1143
+ try {
1144
+ node.setPointerCapture(e.pointerId);
1145
+ } catch {
1146
+ /* headless */
1147
+ }
1148
+ });
1149
+ node.addEventListener("pointermove", function (e) {
1150
+ if (!st || st.pointerId !== e.pointerId) return;
1151
+ var dx = e.clientX - st.ox;
1152
+ var dy = e.clientY - st.oy;
1153
+ if (!st.moved && Math.hypot(dx, dy) < BLOCK_DRAG_THRESHOLD) return;
1154
+ st.moved = true;
1155
+ node.classList.add("plan-storyboard-block--dragging");
1156
+ var p = clampPos(block, st.sx + dx * st.scale, st.sy + dy * st.scale);
1157
+ node.style.left = (p.x / PLAN.STORYBOARD_WIDTH) * 100 + "%";
1158
+ node.style.top = (p.y / PLAN.STORYBOARD_HEIGHT) * 100 + "%";
1159
+ });
1160
+ function end(e) {
1161
+ if (!st || st.pointerId !== e.pointerId) return;
1162
+ try {
1163
+ node.releasePointerCapture(e.pointerId);
1164
+ } catch {
1165
+ /* ignore */
1166
+ }
1167
+ var s = st;
1168
+ st = null;
1169
+ if (!s.moved) {
1170
+ node.classList.remove("plan-storyboard-block--dragging");
1171
+ return;
1172
+ }
1173
+ var p = clampPos(block, s.sx + (e.clientX - s.ox) * s.scale, s.sy + (e.clientY - s.oy) * s.scale);
1174
+ var x = Math.round(p.x);
1175
+ var y = Math.round(p.y);
1176
+ if (x !== block.x || y !== block.y) change({ x: x, y: y });
1177
+ else node.classList.remove("plan-storyboard-block--dragging");
1178
+ }
1179
+ node.addEventListener("pointerup", end);
1180
+ node.addEventListener("pointercancel", function (e) {
1181
+ if (!st || st.pointerId !== e.pointerId) return;
1182
+ st = null;
1183
+ node.classList.remove("plan-storyboard-block--dragging");
1184
+ });
1185
+ }
1186
+ function wireBlockResize(handle, frameEl, block, change) {
1187
+ var st = null;
1188
+ handle.addEventListener("pointerdown", function (e) {
1189
+ if (locked() || e.button !== 0) return;
1190
+ e.stopPropagation();
1191
+ var rect = frameEl.getBoundingClientRect();
1192
+ if (!rect || rect.width <= 0) return;
1193
+ st = {
1194
+ pointerId: e.pointerId,
1195
+ ox: e.clientX,
1196
+ oy: e.clientY,
1197
+ sw: block.w,
1198
+ sh: block.h,
1199
+ scale: PLAN.STORYBOARD_WIDTH / rect.width,
1200
+ moved: false,
1201
+ };
1202
+ try {
1203
+ handle.setPointerCapture(e.pointerId);
1204
+ } catch {
1205
+ /* headless */
1206
+ }
1207
+ });
1208
+ handle.addEventListener("pointermove", function (e) {
1209
+ if (!st || st.pointerId !== e.pointerId) return;
1210
+ var dx = e.clientX - st.ox;
1211
+ var dy = e.clientY - st.oy;
1212
+ if (!st.moved && Math.hypot(dx, dy) < BLOCK_DRAG_THRESHOLD) return;
1213
+ st.moved = true;
1214
+ var node = handle.parentNode;
1215
+ var p = clampSize(block, st.sw + dx * st.scale, st.sh + dy * st.scale);
1216
+ node.classList.add("plan-storyboard-block--dragging");
1217
+ node.style.width = (p.w / PLAN.STORYBOARD_WIDTH) * 100 + "%";
1218
+ node.style.height = (p.h / PLAN.STORYBOARD_HEIGHT) * 100 + "%";
1219
+ });
1220
+ function end(e) {
1221
+ if (!st || st.pointerId !== e.pointerId) return;
1222
+ try {
1223
+ handle.releasePointerCapture(e.pointerId);
1224
+ } catch {
1225
+ /* ignore */
1226
+ }
1227
+ var s = st;
1228
+ st = null;
1229
+ if (!s.moved) return;
1230
+ var p = clampSize(block, s.sw + (e.clientX - s.ox) * s.scale, s.sh + (e.clientY - s.oy) * s.scale);
1231
+ var w = Math.round(p.w);
1232
+ var h = Math.round(p.h);
1233
+ if (w !== block.w || h !== block.h) change({ w: w, h: h });
1234
+ }
1235
+ handle.addEventListener("pointerup", end);
1236
+ handle.addEventListener("pointercancel", function (e) {
1237
+ if (st && st.pointerId === e.pointerId) st = null;
1238
+ });
1239
+ }
1240
+
1241
+ function renderDetailField(className, ariaLabel, value, multiline, onCommit) {
1242
+ var field = el("div", className);
1243
+ field.setAttribute("role", "textbox");
1244
+ field.setAttribute("aria-label", ariaLabel);
1245
+ if (multiline) field.setAttribute("aria-multiline", "true");
1246
+ field.textContent = value;
1247
+ if (!locked()) {
1248
+ field.contentEditable = "true";
1249
+ field.spellcheck = true;
1250
+ }
1251
+ // Commit-once guard: a commit re-renders the surface and detaches this
1252
+ // focused field, firing a second blur \u2014 which must not re-splice the stale
1253
+ // segment (double-commit \u2192 corrupted fences on the single-line beat field).
1254
+ var committed = false;
1255
+ function commit() {
1256
+ if (committed) return;
1257
+ committed = true;
1258
+ var normalized = (field.textContent || "").replace(/\u00a0/g, " ").trim();
1259
+ field.textContent = normalized;
1260
+ if (normalized !== value) onCommit(normalized);
1261
+ }
1262
+ field.addEventListener("blur", commit);
1263
+ if (!multiline) {
1264
+ // Enter blurs; the blur handler is the single committer.
1265
+ field.addEventListener("keydown", function (e) {
1266
+ if (e.key !== "Enter") return;
1267
+ e.preventDefault();
1268
+ field.blur();
1269
+ });
1270
+ }
1271
+ return field;
1272
+ }
1273
+
1274
+ function renderDetails(data, segment) {
1275
+ var aside = el("aside", "plan-storyboard-detail");
1276
+ aside.appendChild(el("div", "plan-storyboard-detail-label", "Story beat"));
1277
+ aside.appendChild(
1278
+ renderDetailField(
1279
+ "plan-storyboard-key-message",
1280
+ "Story beat",
1281
+ data.keyMessage || "",
1282
+ false,
1283
+ function (next) {
1284
+ if (next) {
1285
+ var d = {};
1286
+ for (var k in data) d[k] = data[k];
1287
+ d.keyMessage = next;
1288
+ commitStoryboard(segment, d);
1289
+ } else {
1290
+ var d2 = {};
1291
+ for (var k2 in data) if (k2 !== "keyMessage") d2[k2] = data[k2];
1292
+ commitStoryboard(segment, d2);
1293
+ }
1294
+ },
1295
+ ),
1296
+ );
1297
+ aside.appendChild(el("div", "plan-storyboard-detail-label", "Content draft"));
1298
+ var currentContent = notesToText(data.contentNotes);
1299
+ aside.appendChild(
1300
+ renderDetailField(
1301
+ "plan-storyboard-detail-text",
1302
+ "Content draft",
1303
+ currentContent,
1304
+ true,
1305
+ function (next) {
1306
+ var notes = textToNotes(next);
1307
+ if (notesToText(notes) === currentContent) return;
1308
+ var d = {};
1309
+ for (var k in data) if (k !== "contentNotes" && k !== "depthNotes") d[k] = data[k];
1310
+ if (notes) d.contentNotes = notes;
1311
+ commitStoryboard(segment, d);
1312
+ },
1313
+ ),
1314
+ );
1315
+ return aside;
1316
+ }
1317
+
1318
+ function renderCanvas(segment) {
1319
+ var data = segment.data;
1320
+ var figure = el("figure", "plan-storyboard");
1321
+
1322
+ var header = el("div", "plan-storyboard-header");
1323
+ header.appendChild(el("span", null, "Slide " + data.slide + " body storyboard"));
1324
+ var legend = el("span", "plan-storyboard-legend");
1325
+ legend.appendChild(el("span", "plan-storyboard-legend-item plan-storyboard-legend-item--element", "Design element"));
1326
+ legend.appendChild(el("span", "plan-storyboard-legend-item plan-storyboard-legend-item--content", "Content"));
1327
+ header.appendChild(legend);
1328
+ figure.appendChild(header);
1329
+
1330
+ var layout = el("div", "plan-storyboard-layout");
1331
+ var frame = el("div", "plan-storyboard-frame slide-emboss");
1332
+
1333
+ var top = el("div", "plan-storyboard-reserved plan-storyboard-reserved-top");
1334
+ top.appendChild(document.createTextNode("Template header/title area not planned here"));
1335
+ if (!locked()) {
1336
+ var add = el("button", "plan-storyboard-add", ICONS.plus + "<span>Add container</span>");
1337
+ add.type = "button";
1338
+ add.title = "Add container";
1339
+ add.setAttribute("aria-label", "Add container");
1340
+ add.disabled = data.blocks.length >= PLAN.MAX_BLOCKS;
1341
+ add.addEventListener("click", function () {
1342
+ if (data.blocks.length >= PLAN.MAX_BLOCKS) return;
1343
+ var serial = data.blocks.length + 1;
1344
+ while (
1345
+ data.blocks.some(function (b) {
1346
+ return b.id === "block-" + serial;
1347
+ })
1348
+ )
1349
+ serial += 1;
1350
+ var A = PLAN.BODY_SAFE_AREA;
1351
+ var w = 360;
1352
+ var h = 150;
1353
+ var offset = (data.blocks.length % 4) * 48;
1354
+ var nextBlocks = data.blocks.concat([
1355
+ {
1356
+ id: "block-" + serial,
1357
+ x: clamp(A.x + 40 + offset, A.x, A.x + A.w - w),
1358
+ y: clamp(A.y + 40 + offset, A.y, A.y + A.h - h),
1359
+ w: w,
1360
+ h: h,
1361
+ designElement: "container",
1362
+ content: "New content",
1363
+ },
1364
+ ]);
1365
+ commitStoryboard(segment, withBlocks(data, nextBlocks));
1366
+ });
1367
+ top.appendChild(add);
1368
+ }
1369
+ frame.appendChild(top);
1370
+
1371
+ var grid = el("div", "plan-storyboard-body");
1372
+ grid.setAttribute("aria-hidden", "true");
1373
+ frame.appendChild(grid);
1374
+
1375
+ var canRemove = !locked() && data.blocks.length > 1;
1376
+ data.blocks.forEach(function (block) {
1377
+ frame.appendChild(renderBlock(frame, block, data, segment, canRemove));
1378
+ });
1379
+
1380
+ var bottom = el("div", "plan-storyboard-reserved plan-storyboard-reserved-bottom", "Footer/template area not planned here");
1381
+ frame.appendChild(bottom);
1382
+
1383
+ layout.appendChild(frame);
1384
+ layout.appendChild(renderDetails(data, segment));
1385
+ figure.appendChild(layout);
1386
+
1387
+ if (segment.warnings && segment.warnings.length) {
1388
+ var ul = el("ul", "plan-storyboard-warnings");
1389
+ segment.warnings.forEach(function (w) {
1390
+ var li = document.createElement("li");
1391
+ li.textContent = w;
1392
+ ul.appendChild(li);
1393
+ });
1394
+ figure.appendChild(ul);
1395
+ }
1396
+ return figure;
371
1397
  }
372
1398
 
373
1399
  // ---------- chat composer bar ----------
374
- // Always visible at the bottom of the workspace — the whole decision in one
375
- // strip: previous round's feedback read-only, the chat input ("tell the
376
- // agent what to change…"), the round send button, and Approve with its
377
- // consequence spelled out ("the agent builds the slides next").
378
- function buildComposerBar() {
379
- composerEl = el("div", "sm-composer");
380
- composerEl.setAttribute("data-sm-overlay", "1");
1400
+ // Rebuilt on structural state changes (a storyline draft staged / discarded),
1401
+ // NOT on every keystroke (keystrokes only call refreshChrome). The typed
1402
+ // feedback survives a rebuild via feedbackText.
1403
+ function renderComposer() {
1404
+ if (!composerEl || waiting) return;
1405
+ composerEl.innerHTML = "";
1406
+
1407
+ if (S.storyDraft) {
1408
+ var banner = el("div", "sm-story-banner");
1409
+ banner.innerHTML =
1410
+ ICONS.sparkles +
1411
+ '<span class="txt"><b>Storyline edited.</b> Sending asks the agent to build storyboards and content for your new and renamed slides — add comments below if you like.</span>';
1412
+ var discard = el("button", "sm-story-discard", "Discard changes");
1413
+ discard.type = "button";
1414
+ discard.onclick = function () {
1415
+ S.storyDraft = null;
1416
+ approveArmed = false; // discarding the storyline disarms approve-confirm
1417
+ refreshChrome();
1418
+ renderComposer();
1419
+ renderSurface();
1420
+ };
1421
+ banner.appendChild(discard);
1422
+ composerEl.appendChild(banner);
1423
+ }
381
1424
 
382
1425
  if (CFG.previousComment) {
383
1426
  var prev = el("div", "sm-prev-note");
@@ -391,8 +1434,11 @@
391
1434
  var row = el("div", "sm-composer-row");
392
1435
  commentEl = el("textarea", "sm-chat-ta");
393
1436
  commentEl.rows = 1;
394
- commentEl.placeholder = "Tell the agent what to change — or edit the plan above yourself…";
1437
+ commentEl.placeholder = "What should change? Leave a note, or edit the plan directly.";
1438
+ commentEl.value = feedbackText;
395
1439
  commentEl.addEventListener("input", function () {
1440
+ feedbackText = commentEl.value;
1441
+ approveArmed = false; // typing more feedback disarms a pending approve-confirm
396
1442
  commentEl.style.height = "auto";
397
1443
  commentEl.style.height = Math.min(commentEl.scrollHeight, 150) + "px";
398
1444
  refreshChrome();
@@ -402,21 +1448,31 @@
402
1448
  });
403
1449
  row.appendChild(commentEl);
404
1450
 
405
- sendBtn = el("button", "sm-send is-off");
406
- sendBtn.title = "Send — the agent revises the plan and comes back";
407
- sendBtn.innerHTML = ICONS.send;
1451
+ if (S.storyDraft) {
1452
+ // The round is sendable as-is — "send" grows the story-iteration label.
1453
+ sendBtn = el("button", "sm-send-pill", ICONS.sparkles + "<span>Iterate on the story</span>");
1454
+ sendBtn.type = "button";
1455
+ sendBtn.title = "Iterate on the story";
1456
+ } else {
1457
+ sendBtn = el("button", "sm-send", ICONS.send);
1458
+ sendBtn.type = "button";
1459
+ sendBtn.title = "Send feedback";
1460
+ sendBtn.setAttribute("aria-label", "Send feedback");
1461
+ }
408
1462
  sendBtn.onclick = requestChanges;
409
1463
  row.appendChild(sendBtn);
410
1464
 
411
1465
  row.appendChild(el("div", "sm-composer-sep"));
412
1466
 
413
1467
  approveBtn = el("button", "sm-approve");
1468
+ approveBtn.type = "button";
414
1469
  approveBtn.innerHTML =
415
1470
  '<span class="main">' +
416
1471
  ICONS.check +
417
1472
  "<span>Approve plan</span></span>" +
418
1473
  '<span class="sub">the agent builds the slides next</span>';
419
1474
  approveBtn.onclick = function () {
1475
+ if (S.storyDraft) return; // blocked while a storyline draft is staged
420
1476
  // Approving with unsent feedback in the input discards it — confirm inline.
421
1477
  if (commentText() && !approveArmed) {
422
1478
  approveArmed = true;
@@ -428,16 +1484,17 @@
428
1484
  row.appendChild(approveBtn);
429
1485
 
430
1486
  composerEl.appendChild(row);
431
- wsEl.appendChild(composerEl);
1487
+ refreshChrome();
432
1488
  }
433
1489
 
434
1490
  // ---------- waiting state (comments sent) ----------
435
- // The tab does NOT veil over on a comments round: the plan stays readable,
436
- // the editor locks, and the composer bar becomes the wait signal until the
437
- // agent's next `slidemind plan` round reloads this tab (heartbeat polling).
1491
+ // The tab does NOT veil over on a comments round: the plan stays readable, the
1492
+ // surface locks, and the composer becomes the wait signal until the agent's
1493
+ // next `slidemind plan` round reloads this tab (heartbeat polling).
438
1494
  function enterWaiting() {
439
- if (editorEl) editorEl.readOnly = true;
1495
+ waiting = true;
440
1496
  if (wsEl) wsEl.classList.add("is-waiting");
1497
+ renderSurface(); // rebuild the surface read-only (no contenteditable attrs)
441
1498
  document.title = WAITING_TITLE; // the tab label signals the state from the tab bar
442
1499
  if (composerEl) {
443
1500
  composerEl.innerHTML = "";
@@ -463,9 +1520,6 @@
463
1520
  }
464
1521
 
465
1522
  // ---------- next-round attention ----------
466
- // A background tab cannot steal focus (by browser design). What it CAN do:
467
- // fire a system notification whose click is allowed to focus the tab, and
468
- // flag itself in the tab bar with an attention title until it is focused.
469
1523
  function notifyPermission() {
470
1524
  try {
471
1525
  return "Notification" in window ? Notification.permission : "unsupported";
@@ -502,7 +1556,7 @@
502
1556
  }
503
1557
  if (!flagged) return;
504
1558
  try {
505
- window.focus(); // best-effort; most browsers ignore it for background tabs
1559
+ window.focus();
506
1560
  } catch {
507
1561
  /* ignore */
508
1562
  }
@@ -523,7 +1577,7 @@
523
1577
  if (notifyPermission() === "granted") {
524
1578
  var n = new Notification("slidemind — plan ready", {
525
1579
  body: "The revised plan is ready for your review. Click to jump back to the tab.",
526
- tag: "slidemind-plan-round", // replaces an earlier round's notification
1580
+ tag: "slidemind-plan-round",
527
1581
  });
528
1582
  n.onclick = function () {
529
1583
  try {
@@ -539,58 +1593,20 @@
539
1593
  }
540
1594
  }
541
1595
 
542
- // ---------- live preview ----------
543
- // Debounced re-render of the preview from the editor's current text, via the
544
- // renderer the page ships (plan-md.mjs run in-browser). Absent the renderer
545
- // the preview simply stays at the served render.
546
- function schedulePreview() {
547
- if (!window.__SLIDEMIND_RENDER_MD__) return;
548
- if (renderTimer) clearTimeout(renderTimer);
549
- renderTimer = setTimeout(renderPreview, RENDER_DEBOUNCE_MS);
550
- }
551
- function renderPreview() {
552
- renderTimer = null;
553
- var root = planRoot();
554
- var render = window.__SLIDEMIND_RENDER_MD__;
555
- if (!root || !render) return;
556
- var st = previewPane ? previewPane.scrollTop : 0;
557
- try {
558
- root.innerHTML = render(editorEl.value, {
559
- lineOffset: (CFG.bodyStartLine || 1) - 1,
560
- });
561
- } catch {
562
- return; // never let a renderer hiccup take down the editor
563
- }
564
- if (previewPane) previewPane.scrollTop = st;
565
- }
566
-
567
- // ---------- preview collapse ----------
568
- function readPreviewPref() {
569
- try {
570
- return localStorage.getItem(PREVIEW_PREF_KEY) !== "hidden";
571
- } catch {
572
- return true;
573
- }
574
- }
575
- function applyPreviewState(open) {
576
- previewOpen = open;
577
- var panes = wsEl && wsEl.querySelector(".sm-panes");
578
- if (panes) panes.classList.toggle("preview-hidden", !previewOpen);
579
- refreshChrome();
580
- }
581
- function togglePreview() {
582
- applyPreviewState(!previewOpen);
583
- try {
584
- localStorage.setItem(PREVIEW_PREF_KEY, previewOpen ? "shown" : "hidden");
585
- } catch {
586
- /* private mode etc. — the toggle still works for this tab */
587
- }
588
- if (previewOpen) renderPreview(); // catch up on edits made while hidden
589
- }
590
-
591
1596
  // ---------- submit ----------
592
1597
  function requestChanges() {
593
- if (!commentText()) return;
1598
+ if (waiting) return;
1599
+ if (S.storyDraft) {
1600
+ var staged = PLAN.stageStoryIteration(S.value, S.storyDraft);
1601
+ if (!staged) return;
1602
+ var typed = commentText();
1603
+ pendingComment = typed ? staged.feedback + "\n\n" + typed : staged.feedback;
1604
+ pendingEdited = staged.editedPlanMarkdown;
1605
+ } else {
1606
+ if (!commentText()) return;
1607
+ pendingComment = commentText();
1608
+ pendingEdited = planWasEdited() ? S.value : null;
1609
+ }
594
1610
  // Ask for notification permission HERE — the click is a user gesture, the
595
1611
  // only context where the browser will actually show the prompt.
596
1612
  notifyPermPromise = requestNotifyPermission();
@@ -601,8 +1617,8 @@
601
1617
  submitted = true;
602
1618
  var payload = {
603
1619
  decision: decision,
604
- comment: decision === "comments" ? commentText() : null,
605
- editedPlan: planWasEdited() ? editorEl.value : null,
1620
+ comment: decision === "comments" ? pendingComment : null,
1621
+ editedPlan: decision === "comments" ? pendingEdited : planWasEdited() ? S.value : null,
606
1622
  };
607
1623
  fetch(CFG.endpoint, {
608
1624
  method: "POST",
@@ -610,10 +1626,6 @@
610
1626
  body: JSON.stringify(payload),
611
1627
  })
612
1628
  .then(function () {
613
- // Approve ends the session — the brand build animation plays, then the
614
- // tab closes itself. A comments round keeps the tab OPEN and readable:
615
- // the composer becomes a spinner + wait note while the heartbeat
616
- // polling waits for the next run, which reloads this same tab.
617
1629
  if (decision === "approve") {
618
1630
  showApproved();
619
1631
  } else {
@@ -638,7 +1650,12 @@
638
1650
  }
639
1651
  // After a comments round the tab stays open and polls the heartbeat so the
640
1652
  // NEXT run (which binds the same deterministic port) reloads this tab instead
641
- // of opening a fresh browser window.
1653
+ // of opening a fresh browser window. The poll reports this tab's round nonce
1654
+ // and its visibility: only a VISIBLE tab may claim the next round for reuse —
1655
+ // a tab buried in a background window must not swallow the round invisibly, so
1656
+ // the server opens a fresh browser tab instead and answers `action: "close"`
1657
+ // here, which retires this tab with a notice rather than reloading it into a
1658
+ // hidden duplicate.
642
1659
  function startNextRoundPolling() {
643
1660
  var startedAt = Date.now();
644
1661
  var pollMs = CFG.pollMs || 1000;
@@ -650,27 +1667,37 @@
650
1667
  showSessionEnded();
651
1668
  return;
652
1669
  }
653
- fetch(hb, { cache: "no-store" })
654
- .then(function (r) { return r.ok ? r.json() : null; })
1670
+ var visible = document.visibilityState === "visible" ? "1" : "0";
1671
+ var url =
1672
+ hb +
1673
+ (hb.indexOf("?") < 0 ? "?" : "&") +
1674
+ "round=" +
1675
+ encodeURIComponent(CFG.round || "") +
1676
+ "&visible=" +
1677
+ visible;
1678
+ fetch(url, { cache: "no-store" })
1679
+ .then(function (r) {
1680
+ return r.ok ? r.json() : null;
1681
+ })
655
1682
  .then(function (data) {
656
- // Reload only for a genuine NEWER slidemind plan round: shape-guard the
657
- // service marker (a foreign app / the preview gate may have taken the
658
- // port) and require a different nonce (same nonce = this run's own
659
- // orphan — never self-reload).
660
1683
  if (!data || data.service !== "slidemind-plan" || !data.round) return;
661
- if (data.round !== CFG.round) {
1684
+ if (data.round === CFG.round) return;
1685
+ if (data.action === "close") {
662
1686
  clearInterval(timer);
663
- markRoundReadyAndReload();
1687
+ showSuperseded();
1688
+ return;
664
1689
  }
1690
+ if (data.action === "wait") return; // grace window still deciding — keep polling
1691
+ clearInterval(timer);
1692
+ markRoundReadyAndReload();
665
1693
  })
666
- .catch(function () { /* server down between rounds — keep polling */ });
1694
+ .catch(function () {
1695
+ /* server down between rounds — keep polling */
1696
+ });
667
1697
  }, pollMs);
668
1698
  }
669
1699
 
670
1700
  // ---------- done veil ----------
671
- // Shared full-screen veil (logo + wordmark + status + sub). Replaces any veil
672
- // already shown rather than stacking a second one, so a comments round's
673
- // "waiting" veil can be swapped for "session ended" in place.
674
1701
  function showVeil(statusHtml, subText, subPulse) {
675
1702
  if (bar) bar.style.display = "none";
676
1703
  if (wsEl) wsEl.style.display = "none";
@@ -684,10 +1711,6 @@
684
1711
  veil.appendChild(el("div", "sub" + (subPulse ? " sm-pulse" : ""), subText));
685
1712
  document.body.appendChild(veil);
686
1713
  }
687
- // Approve's exit moment: the full-screen brand animation — rising logo,
688
- // drifting wordmark — announcing that the agent now builds the slides. The
689
- // tab closes itself after the animation has had time to land; if the browser
690
- // refuses, the pulsing veil keeps telling the user what's happening.
691
1714
  function showApproved() {
692
1715
  showVeil(
693
1716
  ICONS.check + "<span>Plan approved</span>",
@@ -699,6 +1722,13 @@
699
1722
  function showSessionEnded() {
700
1723
  showVeil("<span>Session ended</span>", "You can close this tab.");
701
1724
  }
1725
+ function showSuperseded() {
1726
+ document.title = "Plan review moved — slidemind";
1727
+ showVeil(
1728
+ "<span>Continued in a new tab</span>",
1729
+ "The next plan round opened in a fresh browser tab — you can close this one.",
1730
+ );
1731
+ }
702
1732
 
703
1733
  // ---------- assets ----------
704
1734
  function injectAssets() {
@@ -725,17 +1755,18 @@
725
1755
 
726
1756
  // ---------- wire up ----------
727
1757
  function init() {
728
- var root = planRoot();
729
- if (!root) {
730
- setTimeout(init, 50);
1758
+ if (!document.body) {
1759
+ setTimeout(init, 20);
731
1760
  return;
732
1761
  }
1762
+ // The server-rendered #plan-root is the no-JS fallback only; the overlay
1763
+ // renders the interactive surface from S.value, so drop the static node.
1764
+ var stale = document.getElementById("plan-root");
1765
+ if (stale && stale.parentNode) stale.parentNode.removeChild(stale);
1766
+
733
1767
  injectAssets();
734
1768
  buildChrome();
735
1769
  buildWorkspace();
736
- buildComposerBar();
737
- refreshChrome();
738
- editorEl.focus();
739
1770
  announceNewRound(); // notification + title marker when this load IS the next round
740
1771
  }
741
1772