slidemind 0.1.7 → 0.1.8
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/CLAUDE.md +3 -1
- package/README.md +1 -0
- package/bin/slidemind.mjs +16 -0
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +7 -6
- package/src/builders/build.mjs +52 -10
- package/src/builders/gate-server.mjs +223 -0
- package/src/builders/plan-md.mjs +216 -0
- package/src/builders/plan-overlay.js +747 -0
- package/src/builders/plan.mjs +575 -0
- package/src/builders/resolve-skill.mjs +40 -5
- package/src/builders/review-overlay.js +152 -11
- package/src/builders/review.mjs +136 -70
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
// Plan sign-off overlay — the CLI's plan-review gate, styled as the same
|
|
2
|
+
// branded slidemind "app" as the preview-review overlay (review-overlay.js).
|
|
3
|
+
// Dormant unless `window.__SLIDEMIND_PLAN_REVIEW__` is set (only the
|
|
4
|
+
// `slidemind plan` local server injects it), and dormant inside an iframe /
|
|
5
|
+
// thumbnail render, so nothing runs on an ordinary page.
|
|
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.
|
|
12
|
+
//
|
|
13
|
+
// 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.
|
|
24
|
+
// - Approve → decision "approve" (unsent feedback arm-confirms first),
|
|
25
|
+
// then the full-screen brand animation — logo + drifting
|
|
26
|
+
// wordmark + "the agent is building your slides" — before the
|
|
27
|
+
// tab closes itself.
|
|
28
|
+
// Both POST {decision, comment, editedPlan} to the local server so the
|
|
29
|
+
// `slidemind plan` command unblocks.
|
|
30
|
+
//
|
|
31
|
+
// Branding is lifted verbatim from review-overlay.js: the embossed "raised
|
|
32
|
+
// slide" logo (inline-SVG fallback when no per-brand orb PNG is bundled), the
|
|
33
|
+
// drifting "slide"/"mind" wordmark, Poppins/Urbanist type, Lucide icons, and
|
|
34
|
+
// the neutral dark product palette.
|
|
35
|
+
|
|
36
|
+
(function () {
|
|
37
|
+
var CFG = window.__SLIDEMIND_PLAN_REVIEW__;
|
|
38
|
+
if (!CFG) return;
|
|
39
|
+
// Dormant inside an iframe / thumbnail render (mirrors review-overlay).
|
|
40
|
+
if (window.top !== window.self) return;
|
|
41
|
+
if (new URLSearchParams(location.search).get("thumb") === "1") return;
|
|
42
|
+
|
|
43
|
+
var PREVIEW_PREF_KEY = "slidemind-plan-preview";
|
|
44
|
+
var RENDER_DEBOUNCE_MS = 150;
|
|
45
|
+
var APPROVE_CLOSE_DELAY_MS = 2400; // long enough to read the build animation
|
|
46
|
+
var ROUND_READY_FLAG = "slidemind-plan-round-ready"; // sessionStorage marker across the reload
|
|
47
|
+
var ATTENTION_TITLE = "● Plan ready — slidemind";
|
|
48
|
+
var WAITING_TITLE = "⏳ Agent revising — slidemind";
|
|
49
|
+
|
|
50
|
+
// ---------- state ----------
|
|
51
|
+
var submitted = false;
|
|
52
|
+
var approveArmed = false; // inline "discard unsent feedback?" confirm state
|
|
53
|
+
var previewOpen = true;
|
|
54
|
+
var baseline = CFG.planBody || ""; // the served plan body; edits diff against it
|
|
55
|
+
var renderTimer = null;
|
|
56
|
+
|
|
57
|
+
var bar, wsEl, edPane, previewPane, editorEl, commentEl, composerEl;
|
|
58
|
+
var editedBadge, approveBtn, sendBtn, previewBtn;
|
|
59
|
+
var waitTextEl = null;
|
|
60
|
+
var notifyPermPromise = null; // resolved Notification permission, asked at the send gesture
|
|
61
|
+
|
|
62
|
+
// ---------- brand assets (identical to review-overlay.js) ----------
|
|
63
|
+
var LOGO_SVG =
|
|
64
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="160 160 960 540" preserveAspectRatio="none">' +
|
|
65
|
+
"<defs>" +
|
|
66
|
+
'<linearGradient id="rimTop" gradientUnits="userSpaceOnUse" x1="160" y1="0" x2="1120" y2="0">' +
|
|
67
|
+
'<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.85"/>' +
|
|
68
|
+
'<stop offset="35%" stop-color="#fff" stop-opacity="0.5"/>' +
|
|
69
|
+
'<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
|
|
70
|
+
'<linearGradient id="rimLeft" gradientUnits="userSpaceOnUse" x1="0" y1="160" x2="0" y2="700">' +
|
|
71
|
+
'<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.6"/>' +
|
|
72
|
+
'<stop offset="40%" stop-color="#fff" stop-opacity="0.28"/>' +
|
|
73
|
+
'<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
|
|
74
|
+
'<linearGradient id="rimBottom" gradientUnits="userSpaceOnUse" x1="1120" y1="0" x2="160" y2="0">' +
|
|
75
|
+
'<stop offset="0%" stop-color="#000" stop-opacity="0.55"/>' +
|
|
76
|
+
'<stop offset="60%" stop-color="#000" stop-opacity="0.2"/>' +
|
|
77
|
+
'<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
|
|
78
|
+
'<linearGradient id="rimRight" gradientUnits="userSpaceOnUse" x1="0" y1="700" x2="0" y2="160">' +
|
|
79
|
+
'<stop offset="0%" stop-color="#000" stop-opacity="0.45"/>' +
|
|
80
|
+
'<stop offset="60%" stop-color="#000" stop-opacity="0.16"/>' +
|
|
81
|
+
'<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
|
|
82
|
+
'<linearGradient id="faceFill" gradientUnits="userSpaceOnUse" x1="160" y1="160" x2="1120" y2="700">' +
|
|
83
|
+
'<stop offset="0%" stop-color="#fff" stop-opacity="0.08"/>' +
|
|
84
|
+
'<stop offset="50%" stop-color="#808080" stop-opacity="0.02"/>' +
|
|
85
|
+
'<stop offset="100%" stop-color="#000" stop-opacity="0.10"/></linearGradient>' +
|
|
86
|
+
"</defs>" +
|
|
87
|
+
'<rect x="160" y="160" width="960" height="540" rx="40" ry="40" fill="url(#faceFill)"/>' +
|
|
88
|
+
'<path d="M 160,200 A 40,40 0 0 1 200,160 L 1080,160 A 40,40 0 0 1 1120,200" fill="none" stroke="url(#rimTop)" stroke-width="3" stroke-linecap="round"/>' +
|
|
89
|
+
'<path d="M 160,200 L 160,660 A 40,40 0 0 0 200,700" fill="none" stroke="url(#rimLeft)" stroke-width="2.4" stroke-linecap="round"/>' +
|
|
90
|
+
'<path d="M 1120,660 A 40,40 0 0 1 1080,700 L 200,700 A 40,40 0 0 1 160,660" fill="none" stroke="url(#rimBottom)" stroke-width="3" stroke-linecap="round"/>' +
|
|
91
|
+
'<path d="M 1120,660 L 1120,200 A 40,40 0 0 0 1080,160" fill="none" stroke="url(#rimRight)" stroke-width="2.4" stroke-linecap="round"/>' +
|
|
92
|
+
'<rect x="160" y="160" width="960" height="540" rx="40" ry="40" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.4"/>' +
|
|
93
|
+
"</svg>";
|
|
94
|
+
|
|
95
|
+
// Give a copy of the logo unique gradient ids so two instances (header + done
|
|
96
|
+
// veil) can coexist without id collisions.
|
|
97
|
+
function logoWithIds(suffix) {
|
|
98
|
+
return LOGO_SVG.replace(/id="([^"]+)"/g, 'id="$1' + suffix + '"').replace(
|
|
99
|
+
/url\(#([^)]+)\)/g,
|
|
100
|
+
"url(#$1" + suffix + ")",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Lucide icons (MIT) — the same family review-overlay uses.
|
|
105
|
+
var ICONS = {
|
|
106
|
+
check:
|
|
107
|
+
'<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
|
+
ghost:
|
|
111
|
+
'<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
|
+
send:
|
|
113
|
+
'<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>',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ---------- styles (chrome copied from review-overlay.js; pins/popover/
|
|
117
|
+
// sidebar rules dropped, editor/preview/dock rules added) ----------
|
|
118
|
+
var CSS =
|
|
119
|
+
"@keyframes sm-drift-a{0%,100%{transform:translateX(-5px)}50%{transform:translateX(5px)}}" +
|
|
120
|
+
"@keyframes sm-drift-b{0%,100%{transform:translateX(5px)}50%{transform:translateX(-5px)}}" +
|
|
121
|
+
"@keyframes sm-bar-in{from{transform:translateY(-100%);opacity:0}to{transform:none;opacity:1}}" +
|
|
122
|
+
"@keyframes sm-veil-in{from{opacity:0}to{opacity:1}}" +
|
|
123
|
+
"@keyframes sm-rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}" +
|
|
124
|
+
"@keyframes sm-pulse{0%,100%{opacity:.45}50%{opacity:.85}}" +
|
|
125
|
+
":root{--sm-bg:var(--deck-chrome-bg,#111113);--sm-surface:var(--deck-chrome-bg-2,#161618);" +
|
|
126
|
+
"--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);}" +
|
|
129
|
+
".sm-o,.sm-o *{box-sizing:border-box;}" +
|
|
130
|
+
/* header */
|
|
131
|
+
".sm-bar{position:fixed;top:0;left:0;right:0;z-index:2147483600;display:flex;align-items:center;" +
|
|
132
|
+
"gap:16px;height:var(--deck-chrome-h,62px);padding:0 18px;color:var(--sm-fg);font-family:var(--sm-font);font-size:13px;" +
|
|
133
|
+
"background:var(--sm-bg);border-bottom:1px solid var(--sm-border);" +
|
|
134
|
+
"user-select:none;animation:sm-bar-in .5s cubic-bezier(.2,.7,.2,1);}" +
|
|
135
|
+
".sm-brand{display:flex;align-items:center;gap:11px;}" +
|
|
136
|
+
".sm-logo{position:relative;width:48px;height:27px;border-radius:5px;overflow:hidden;flex-shrink:0;background:#0a0c11;box-shadow:0 1px 3px rgba(0,0,0,.5);}" +
|
|
137
|
+
".sm-logo-svg{position:absolute;inset:0;z-index:1;}.sm-logo-svg svg{width:100%;height:100%;display:block;}" +
|
|
138
|
+
".sm-logo-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;}" +
|
|
139
|
+
".sm-word{display:flex;flex-direction:column;line-height:.98;font-family:var(--sm-word);" +
|
|
140
|
+
"font-weight:300;font-size:13px;letter-spacing:.46em;color:var(--sm-fg);}" +
|
|
141
|
+
".sm-word b{font-weight:300;display:block;will-change:transform;}" +
|
|
142
|
+
".sm-word .top{animation:sm-drift-a 20s ease-in-out infinite;}" +
|
|
143
|
+
".sm-word .bot{margin-left:.18em;animation:sm-drift-b 20s ease-in-out infinite;}" +
|
|
144
|
+
".sm-sep{width:1px;height:28px;background:var(--sm-border);}" +
|
|
145
|
+
".sm-meta{display:flex;flex-direction:column;gap:2px;}" +
|
|
146
|
+
".sm-meta .t{font-weight:600;font-size:13px;display:flex;align-items:center;gap:8px;}" +
|
|
147
|
+
".sm-meta .h{font-size:11.5px;color:var(--sm-dim);}" +
|
|
148
|
+
".sm-edited{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--sm-warn);" +
|
|
149
|
+
"border:1px solid rgba(246,196,84,.4);border-radius:999px;padding:1px 7px;}" +
|
|
150
|
+
".sm-grow{flex:1;}" +
|
|
151
|
+
/* buttons */
|
|
152
|
+
".sm-btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:9px;" +
|
|
153
|
+
"padding:8px 14px;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;" +
|
|
154
|
+
"transition:transform .14s ease,background .14s ease,box-shadow .14s ease,border-color .14s ease;white-space:nowrap;}" +
|
|
155
|
+
".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 */
|
|
166
|
+
".sm-ws{position:fixed;left:0;right:0;top:var(--deck-chrome-h,62px);bottom:0;z-index:2147483540;" +
|
|
167
|
+
"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 */
|
|
177
|
+
".sm-composer{flex-shrink:0;display:flex;flex-direction:column;gap:8px;padding:11px 16px;" +
|
|
178
|
+
"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);}" +
|
|
181
|
+
".sm-prev-note svg{width:14px;height:14px;flex-shrink:0;margin-top:2px;color:#71717a;}" +
|
|
182
|
+
".sm-prev-note .lbl{font-weight:600;color:#a1a1aa;white-space:nowrap;}" +
|
|
183
|
+
".sm-prev-note .txt{white-space:pre-wrap;word-break:break-word;color:#b9b9be;}" +
|
|
184
|
+
".sm-composer-row{display:flex;align-items:flex-end;gap:10px;}" +
|
|
185
|
+
".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;}" +
|
|
188
|
+
".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;}" +
|
|
192
|
+
".sm-send svg{width:16px;height:16px;margin:1px 1px 0 0;}" +
|
|
193
|
+
".sm-send:hover{transform:translateY(-1px) scale(1.05);box-shadow:0 8px 22px rgba(169,194,255,.35);}" +
|
|
194
|
+
".sm-send.is-off{opacity:.35;cursor:not-allowed;pointer-events:none;}" +
|
|
195
|
+
".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;}" +
|
|
200
|
+
".sm-approve:hover{background:#fff;transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,.45);}" +
|
|
201
|
+
".sm-approve .main{display:flex;align-items:center;gap:7px;font-weight:600;font-size:12.5px;}" +
|
|
202
|
+
".sm-approve .main svg{width:15px;height:15px;color:var(--sm-success);}" +
|
|
203
|
+
".sm-approve .sub{font-size:9.5px;font-weight:500;color:#61616a;}" +
|
|
204
|
+
".sm-approve.is-armed{background:#f6c454;color:#241a00;}" +
|
|
205
|
+
".sm-approve.is-armed .main svg{color:#241a00;}" +
|
|
206
|
+
/* waiting state — feedback sent, the agent is revising; tab stays readable */
|
|
207
|
+
"@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;}" +
|
|
210
|
+
".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;}" +
|
|
214
|
+
/* 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));" +
|
|
218
|
+
"-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);animation:sm-veil-in .4s ease;}" +
|
|
219
|
+
".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;}" +
|
|
222
|
+
".sm-veil .word b{font-weight:300;}" +
|
|
223
|
+
".sm-veil .word .top{animation:sm-drift-a 18s ease-in-out infinite;}" +
|
|
224
|
+
".sm-veil .word .bot{margin-left:.3em;animation:sm-drift-b 18s ease-in-out infinite;}" +
|
|
225
|
+
".sm-veil .status{display:flex;align-items:center;gap:10px;font-size:18px;font-weight:600;animation:sm-rise .6s .12s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
226
|
+
".sm-veil .status svg{width:24px;height:24px;color:var(--sm-success);}" +
|
|
227
|
+
".sm-veil .sub{color:var(--sm-dim);font-size:13.5px;animation:sm-rise .6s .18s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
228
|
+
".sm-veil .sub.sm-pulse{animation:sm-rise .6s .18s cubic-bezier(.2,.7,.2,1) both,sm-pulse 2s .9s ease-in-out infinite;}";
|
|
229
|
+
|
|
230
|
+
// ---------- helpers ----------
|
|
231
|
+
function el(tag, cls, html) {
|
|
232
|
+
var e = document.createElement(tag);
|
|
233
|
+
if (cls) e.className = cls;
|
|
234
|
+
if (html != null) e.innerHTML = html;
|
|
235
|
+
return e;
|
|
236
|
+
}
|
|
237
|
+
function planRoot() {
|
|
238
|
+
return document.getElementById("plan-root");
|
|
239
|
+
}
|
|
240
|
+
function wordmarkHtml() {
|
|
241
|
+
return '<b class="top">slide</b><b class="bot">mind</b>';
|
|
242
|
+
}
|
|
243
|
+
// Genuine path: the add-in's per-brand orb PNG (CFG.logoMark). Fallback: the
|
|
244
|
+
// inline embossed slide glyph — no CLI-side orb re-creation.
|
|
245
|
+
function logoNode(cls, idSuffix) {
|
|
246
|
+
var box = el("div", cls);
|
|
247
|
+
if (CFG.logoMark) {
|
|
248
|
+
var img = el("img", "sm-logo-img");
|
|
249
|
+
img.src = CFG.logoMark;
|
|
250
|
+
img.alt = "slidemind";
|
|
251
|
+
box.appendChild(img);
|
|
252
|
+
return box;
|
|
253
|
+
}
|
|
254
|
+
box.appendChild(el("div", "sm-logo-svg", idSuffix ? logoWithIds(idSuffix) : LOGO_SVG));
|
|
255
|
+
return box;
|
|
256
|
+
}
|
|
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;
|
|
265
|
+
}
|
|
266
|
+
function planWasEdited() {
|
|
267
|
+
return !!editorEl && editorEl.value !== baseline;
|
|
268
|
+
}
|
|
269
|
+
function commentText() {
|
|
270
|
+
return commentEl ? commentEl.value.trim() : "";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------- chrome (top bar) ----------
|
|
274
|
+
function buildChrome() {
|
|
275
|
+
bar = el("div", "sm-o sm-bar");
|
|
276
|
+
bar.setAttribute("data-sm-overlay", "1");
|
|
277
|
+
|
|
278
|
+
var brand = el("div", "sm-brand");
|
|
279
|
+
brand.appendChild(logoNode("sm-logo"));
|
|
280
|
+
brand.appendChild(el("div", "sm-word", wordmarkHtml()));
|
|
281
|
+
bar.appendChild(brand);
|
|
282
|
+
bar.appendChild(el("div", "sm-sep"));
|
|
283
|
+
|
|
284
|
+
var meta = el("div", "sm-meta");
|
|
285
|
+
var title = el("div", "t");
|
|
286
|
+
title.appendChild(document.createTextNode("Plan review"));
|
|
287
|
+
editedBadge = el("span", "sm-edited", "edited");
|
|
288
|
+
editedBadge.style.display = "none";
|
|
289
|
+
title.appendChild(editedBadge);
|
|
290
|
+
meta.appendChild(title);
|
|
291
|
+
meta.appendChild(
|
|
292
|
+
el("div", "h", "Edit the markdown directly · the preview follows · decide below when it reads right"),
|
|
293
|
+
);
|
|
294
|
+
bar.appendChild(meta);
|
|
295
|
+
|
|
296
|
+
bar.appendChild(el("div", "sm-grow"));
|
|
297
|
+
|
|
298
|
+
previewBtn = mkButton("Preview", "ghost", "panel");
|
|
299
|
+
previewBtn.onclick = togglePreview;
|
|
300
|
+
bar.appendChild(previewBtn);
|
|
301
|
+
|
|
302
|
+
document.body.appendChild(bar);
|
|
303
|
+
}
|
|
304
|
+
function refreshChrome() {
|
|
305
|
+
if (editedBadge) editedBadge.style.display = planWasEdited() ? "" : "none";
|
|
306
|
+
|
|
307
|
+
var hasFeedback = commentText().length > 0;
|
|
308
|
+
if (sendBtn) {
|
|
309
|
+
sendBtn.classList.toggle("is-off", !hasFeedback);
|
|
310
|
+
}
|
|
311
|
+
if (approveBtn) {
|
|
312
|
+
var main = approveBtn.querySelector(".main span");
|
|
313
|
+
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 {
|
|
319
|
+
approveArmed = false;
|
|
320
|
+
approveBtn.classList.remove("is-armed");
|
|
321
|
+
if (main) main.textContent = "Approve plan";
|
|
322
|
+
if (sub) sub.style.display = "";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (previewBtn) {
|
|
326
|
+
previewBtn.classList.toggle("is-on", previewOpen);
|
|
327
|
+
btnLabel(previewBtn, previewOpen ? "Hide preview" : "Show preview");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------- workspace: editor + live preview + comment dock ----------
|
|
332
|
+
function buildWorkspace() {
|
|
333
|
+
wsEl = el("div", "sm-o sm-ws");
|
|
334
|
+
wsEl.setAttribute("data-sm-overlay", "1");
|
|
335
|
+
|
|
336
|
+
var panes = el("div", "sm-panes");
|
|
337
|
+
|
|
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();
|
|
344
|
+
refreshChrome();
|
|
345
|
+
});
|
|
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();
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
edPane.appendChild(editorEl);
|
|
359
|
+
panes.appendChild(edPane);
|
|
360
|
+
|
|
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);
|
|
367
|
+
|
|
368
|
+
wsEl.appendChild(panes);
|
|
369
|
+
document.body.appendChild(wsEl);
|
|
370
|
+
applyPreviewState(readPreviewPref());
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ---------- 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");
|
|
381
|
+
|
|
382
|
+
if (CFG.previousComment) {
|
|
383
|
+
var prev = el("div", "sm-prev-note");
|
|
384
|
+
prev.innerHTML = ICONS.ghost + '<span class="lbl">Previous round</span>';
|
|
385
|
+
var txt = el("span", "txt");
|
|
386
|
+
txt.textContent = CFG.previousComment;
|
|
387
|
+
prev.appendChild(txt);
|
|
388
|
+
composerEl.appendChild(prev);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
var row = el("div", "sm-composer-row");
|
|
392
|
+
commentEl = el("textarea", "sm-chat-ta");
|
|
393
|
+
commentEl.rows = 1;
|
|
394
|
+
commentEl.placeholder = "Tell the agent what to change — or edit the plan above yourself…";
|
|
395
|
+
commentEl.addEventListener("input", function () {
|
|
396
|
+
commentEl.style.height = "auto";
|
|
397
|
+
commentEl.style.height = Math.min(commentEl.scrollHeight, 150) + "px";
|
|
398
|
+
refreshChrome();
|
|
399
|
+
});
|
|
400
|
+
commentEl.addEventListener("keydown", function (e) {
|
|
401
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") requestChanges();
|
|
402
|
+
});
|
|
403
|
+
row.appendChild(commentEl);
|
|
404
|
+
|
|
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;
|
|
408
|
+
sendBtn.onclick = requestChanges;
|
|
409
|
+
row.appendChild(sendBtn);
|
|
410
|
+
|
|
411
|
+
row.appendChild(el("div", "sm-composer-sep"));
|
|
412
|
+
|
|
413
|
+
approveBtn = el("button", "sm-approve");
|
|
414
|
+
approveBtn.innerHTML =
|
|
415
|
+
'<span class="main">' +
|
|
416
|
+
ICONS.check +
|
|
417
|
+
"<span>Approve plan</span></span>" +
|
|
418
|
+
'<span class="sub">the agent builds the slides next</span>';
|
|
419
|
+
approveBtn.onclick = function () {
|
|
420
|
+
// Approving with unsent feedback in the input discards it — confirm inline.
|
|
421
|
+
if (commentText() && !approveArmed) {
|
|
422
|
+
approveArmed = true;
|
|
423
|
+
refreshChrome();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
submit("approve");
|
|
427
|
+
};
|
|
428
|
+
row.appendChild(approveBtn);
|
|
429
|
+
|
|
430
|
+
composerEl.appendChild(row);
|
|
431
|
+
wsEl.appendChild(composerEl);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ---------- 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).
|
|
438
|
+
function enterWaiting() {
|
|
439
|
+
if (editorEl) editorEl.readOnly = true;
|
|
440
|
+
if (wsEl) wsEl.classList.add("is-waiting");
|
|
441
|
+
document.title = WAITING_TITLE; // the tab label signals the state from the tab bar
|
|
442
|
+
if (composerEl) {
|
|
443
|
+
composerEl.innerHTML = "";
|
|
444
|
+
var w = el("div", "sm-wait");
|
|
445
|
+
w.appendChild(el("div", "sm-spin"));
|
|
446
|
+
waitTextEl = el(
|
|
447
|
+
"div",
|
|
448
|
+
null,
|
|
449
|
+
"<b>Feedback sent.</b> The agent is revising the plan — this tab refreshes " +
|
|
450
|
+
"automatically when the next round is ready. Feel free to keep reading, " +
|
|
451
|
+
"or come back later.",
|
|
452
|
+
);
|
|
453
|
+
w.appendChild(waitTextEl);
|
|
454
|
+
composerEl.appendChild(w);
|
|
455
|
+
}
|
|
456
|
+
// The permission prompt was raised at the send click (a user gesture, so the
|
|
457
|
+
// browser shows it); once granted, tell the user a notification will come.
|
|
458
|
+
(notifyPermPromise || Promise.resolve(notifyPermission())).then(function (perm) {
|
|
459
|
+
if (perm === "granted" && waitTextEl) {
|
|
460
|
+
waitTextEl.innerHTML += " You'll get a notification the moment it's ready.";
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ---------- 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
|
+
function notifyPermission() {
|
|
470
|
+
try {
|
|
471
|
+
return "Notification" in window ? Notification.permission : "unsupported";
|
|
472
|
+
} catch {
|
|
473
|
+
return "unsupported";
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function requestNotifyPermission() {
|
|
477
|
+
try {
|
|
478
|
+
if (!("Notification" in window)) return Promise.resolve("unsupported");
|
|
479
|
+
if (Notification.permission !== "default") return Promise.resolve(Notification.permission);
|
|
480
|
+
return Promise.resolve(Notification.requestPermission());
|
|
481
|
+
} catch {
|
|
482
|
+
return Promise.resolve("unsupported");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// The reload wipes this JS context, so the "announce the new round" wish is
|
|
486
|
+
// parked in sessionStorage and honoured by the NEXT page load.
|
|
487
|
+
function markRoundReadyAndReload() {
|
|
488
|
+
try {
|
|
489
|
+
sessionStorage.setItem(ROUND_READY_FLAG, "1");
|
|
490
|
+
} catch {
|
|
491
|
+
/* storage blocked — the reload still lands, just without the announce */
|
|
492
|
+
}
|
|
493
|
+
location.reload();
|
|
494
|
+
}
|
|
495
|
+
function announceNewRound() {
|
|
496
|
+
var flagged = false;
|
|
497
|
+
try {
|
|
498
|
+
flagged = sessionStorage.getItem(ROUND_READY_FLAG) === "1";
|
|
499
|
+
if (flagged) sessionStorage.removeItem(ROUND_READY_FLAG);
|
|
500
|
+
} catch {
|
|
501
|
+
/* no storage — nothing to announce */
|
|
502
|
+
}
|
|
503
|
+
if (!flagged) return;
|
|
504
|
+
try {
|
|
505
|
+
window.focus(); // best-effort; most browsers ignore it for background tabs
|
|
506
|
+
} catch {
|
|
507
|
+
/* ignore */
|
|
508
|
+
}
|
|
509
|
+
if (!document.hidden) return; // user is already looking at it
|
|
510
|
+
var origTitle = document.title;
|
|
511
|
+
var restore = function () {
|
|
512
|
+
document.title = origTitle;
|
|
513
|
+
document.removeEventListener("visibilitychange", onVis);
|
|
514
|
+
window.removeEventListener("focus", restore);
|
|
515
|
+
};
|
|
516
|
+
var onVis = function () {
|
|
517
|
+
if (!document.hidden) restore();
|
|
518
|
+
};
|
|
519
|
+
document.title = ATTENTION_TITLE;
|
|
520
|
+
document.addEventListener("visibilitychange", onVis);
|
|
521
|
+
window.addEventListener("focus", restore);
|
|
522
|
+
try {
|
|
523
|
+
if (notifyPermission() === "granted") {
|
|
524
|
+
var n = new Notification("slidemind — plan ready", {
|
|
525
|
+
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
|
|
527
|
+
});
|
|
528
|
+
n.onclick = function () {
|
|
529
|
+
try {
|
|
530
|
+
window.focus();
|
|
531
|
+
} catch {
|
|
532
|
+
/* ignore */
|
|
533
|
+
}
|
|
534
|
+
n.close();
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
/* notification construction can throw on some platforms — title marker stands */
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
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
|
+
// ---------- submit ----------
|
|
592
|
+
function requestChanges() {
|
|
593
|
+
if (!commentText()) return;
|
|
594
|
+
// Ask for notification permission HERE — the click is a user gesture, the
|
|
595
|
+
// only context where the browser will actually show the prompt.
|
|
596
|
+
notifyPermPromise = requestNotifyPermission();
|
|
597
|
+
submit("comments");
|
|
598
|
+
}
|
|
599
|
+
function submit(decision) {
|
|
600
|
+
if (submitted) return;
|
|
601
|
+
submitted = true;
|
|
602
|
+
var payload = {
|
|
603
|
+
decision: decision,
|
|
604
|
+
comment: decision === "comments" ? commentText() : null,
|
|
605
|
+
editedPlan: planWasEdited() ? editorEl.value : null,
|
|
606
|
+
};
|
|
607
|
+
fetch(CFG.endpoint, {
|
|
608
|
+
method: "POST",
|
|
609
|
+
headers: { "Content-Type": "application/json" },
|
|
610
|
+
body: JSON.stringify(payload),
|
|
611
|
+
})
|
|
612
|
+
.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
|
+
if (decision === "approve") {
|
|
618
|
+
showApproved();
|
|
619
|
+
} else {
|
|
620
|
+
enterWaiting();
|
|
621
|
+
startNextRoundPolling();
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
.catch(function () {
|
|
625
|
+
submitted = false;
|
|
626
|
+
alert("Could not reach the slidemind plan server — is the terminal command still running?");
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
function requestTabClose() {
|
|
630
|
+
setTimeout(function () {
|
|
631
|
+
try {
|
|
632
|
+
window.open("", "_self");
|
|
633
|
+
window.close();
|
|
634
|
+
} catch {
|
|
635
|
+
// Some browsers only allow script-opened tabs to close themselves.
|
|
636
|
+
}
|
|
637
|
+
}, 120);
|
|
638
|
+
}
|
|
639
|
+
// After a comments round the tab stays open and polls the heartbeat so the
|
|
640
|
+
// NEXT run (which binds the same deterministic port) reloads this tab instead
|
|
641
|
+
// of opening a fresh browser window.
|
|
642
|
+
function startNextRoundPolling() {
|
|
643
|
+
var startedAt = Date.now();
|
|
644
|
+
var pollMs = CFG.pollMs || 1000;
|
|
645
|
+
var ttl = CFG.pollTtlMs || 35 * 60 * 1000;
|
|
646
|
+
var hb = CFG.heartbeat || "/__plan/heartbeat";
|
|
647
|
+
var timer = setInterval(function () {
|
|
648
|
+
if (Date.now() - startedAt > ttl) {
|
|
649
|
+
clearInterval(timer);
|
|
650
|
+
showSessionEnded();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
fetch(hb, { cache: "no-store" })
|
|
654
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
655
|
+
.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
|
+
if (!data || data.service !== "slidemind-plan" || !data.round) return;
|
|
661
|
+
if (data.round !== CFG.round) {
|
|
662
|
+
clearInterval(timer);
|
|
663
|
+
markRoundReadyAndReload();
|
|
664
|
+
}
|
|
665
|
+
})
|
|
666
|
+
.catch(function () { /* server down between rounds — keep polling */ });
|
|
667
|
+
}, pollMs);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ---------- 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
|
+
function showVeil(statusHtml, subText, subPulse) {
|
|
675
|
+
if (bar) bar.style.display = "none";
|
|
676
|
+
if (wsEl) wsEl.style.display = "none";
|
|
677
|
+
var existing = document.querySelector(".sm-veil[data-sm-overlay]");
|
|
678
|
+
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
|
|
679
|
+
var veil = el("div", "sm-o sm-veil");
|
|
680
|
+
veil.setAttribute("data-sm-overlay", "1");
|
|
681
|
+
veil.appendChild(logoNode("sm-logo", "-v"));
|
|
682
|
+
veil.appendChild(el("div", "word", wordmarkHtml()));
|
|
683
|
+
veil.appendChild(el("div", "status", statusHtml));
|
|
684
|
+
veil.appendChild(el("div", "sub" + (subPulse ? " sm-pulse" : ""), subText));
|
|
685
|
+
document.body.appendChild(veil);
|
|
686
|
+
}
|
|
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
|
+
function showApproved() {
|
|
692
|
+
showVeil(
|
|
693
|
+
ICONS.check + "<span>Plan approved</span>",
|
|
694
|
+
"The agent is building your slides now — you can close this tab.",
|
|
695
|
+
true,
|
|
696
|
+
);
|
|
697
|
+
setTimeout(requestTabClose, APPROVE_CLOSE_DELAY_MS);
|
|
698
|
+
}
|
|
699
|
+
function showSessionEnded() {
|
|
700
|
+
showVeil("<span>Session ended</span>", "You can close this tab.");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ---------- assets ----------
|
|
704
|
+
function injectAssets() {
|
|
705
|
+
var style = el("style");
|
|
706
|
+
style.id = "sm-plan-style";
|
|
707
|
+
style.textContent = CSS;
|
|
708
|
+
document.head.appendChild(style);
|
|
709
|
+
|
|
710
|
+
var pre1 = el("link");
|
|
711
|
+
pre1.rel = "preconnect";
|
|
712
|
+
pre1.href = "https://fonts.googleapis.com";
|
|
713
|
+
var pre2 = el("link");
|
|
714
|
+
pre2.rel = "preconnect";
|
|
715
|
+
pre2.href = "https://fonts.gstatic.com";
|
|
716
|
+
pre2.crossOrigin = "anonymous";
|
|
717
|
+
var fonts = el("link");
|
|
718
|
+
fonts.rel = "stylesheet";
|
|
719
|
+
fonts.href =
|
|
720
|
+
"https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&family=Urbanist:wght@300;400&display=swap";
|
|
721
|
+
document.head.appendChild(pre1);
|
|
722
|
+
document.head.appendChild(pre2);
|
|
723
|
+
document.head.appendChild(fonts);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ---------- wire up ----------
|
|
727
|
+
function init() {
|
|
728
|
+
var root = planRoot();
|
|
729
|
+
if (!root) {
|
|
730
|
+
setTimeout(init, 50);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
injectAssets();
|
|
734
|
+
buildChrome();
|
|
735
|
+
buildWorkspace();
|
|
736
|
+
buildComposerBar();
|
|
737
|
+
refreshChrome();
|
|
738
|
+
editorEl.focus();
|
|
739
|
+
announceNewRound(); // notification + title marker when this load IS the next round
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (document.readyState === "loading") {
|
|
743
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
744
|
+
} else {
|
|
745
|
+
init();
|
|
746
|
+
}
|
|
747
|
+
})();
|