slidemind 0.3.20 → 0.4.1

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.
Files changed (187) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +592 -16
  4. package/LICENSE +4 -0
  5. package/README.md +203 -8
  6. package/THIRD-PARTY-NOTICES.md +201 -0
  7. package/bin/slidemind.mjs +50 -2
  8. package/package.json +5 -3
  9. package/skills/create-slides/SKILL.md +484 -886
  10. package/skills/search/SKILL.md +46 -23
  11. package/src/bridge/build-and-push.mjs +548 -73
  12. package/src/bridge/bundle.mjs +100 -31
  13. package/src/bridge/capture.mjs +14 -17
  14. package/src/bridge/client/brands.mjs +157 -18
  15. package/src/bridge/client/engine-routing.mjs +48 -13
  16. package/src/bridge/client/engine-search.mjs +24 -1
  17. package/src/bridge/client/render.mjs +27 -1
  18. package/src/bridge/client/transport.mjs +499 -23
  19. package/src/bridge/client.mjs +10 -0
  20. package/src/bridge/config.mjs +67 -6
  21. package/src/bridge/engine-download.mjs +0 -10
  22. package/src/bridge/graph-auth.mjs +55 -3
  23. package/src/bridge/image-intent-envelope.mjs +239 -0
  24. package/src/bridge/package-preview.mjs +67 -0
  25. package/src/bridge/sharepoint.mjs +914 -0
  26. package/src/builders/asset-fetch.mjs +203 -0
  27. package/src/builders/block-dividers.mjs +83 -0
  28. package/src/builders/build.mjs +171 -87
  29. package/src/builders/card-height.mjs +294 -0
  30. package/src/builders/chart-hole.mjs +183 -0
  31. package/src/builders/chart-jsx.mjs +410 -0
  32. package/src/builders/chart-labels.mjs +326 -0
  33. package/src/builders/concept-geometry.mjs +370 -0
  34. package/src/builders/concept-intent.mjs +200 -0
  35. package/src/builders/concept-layout.mjs +79 -0
  36. package/src/builders/concept-lines.mjs +509 -0
  37. package/src/builders/concept-lists.mjs +270 -0
  38. package/src/builders/concept-marks.mjs +84 -0
  39. package/src/builders/concept-relationships.mjs +12 -0
  40. package/src/builders/concept-spec.mjs +645 -0
  41. package/src/builders/concept-text.mjs +140 -0
  42. package/src/builders/concept-waves.mjs +57 -0
  43. package/src/builders/copy-fit.mjs +139 -0
  44. package/src/builders/design-tokens.mjs +347 -0
  45. package/src/builders/draft-check.mjs +552 -340
  46. package/src/builders/draft-lines.mjs +369 -0
  47. package/src/builders/edit-route.mjs +29 -0
  48. package/src/builders/figure-sketch.mjs +375 -0
  49. package/src/builders/fold-sets.mjs +639 -0
  50. package/src/builders/gate-server.mjs +9 -4
  51. package/src/builders/gate-session.js +273 -12
  52. package/src/builders/gate-shell.js +1494 -0
  53. package/src/builders/image-intent-check.mjs +308 -0
  54. package/src/builders/image-use-reconcile.mjs +107 -0
  55. package/src/builders/kit-helpers.mjs +121 -0
  56. package/src/builders/layout-names.mjs +32 -0
  57. package/src/builders/legacy-reference-uses.mjs +179 -0
  58. package/src/builders/line-fit.mjs +224 -0
  59. package/src/builders/line-native.mjs +895 -0
  60. package/src/builders/line-relations.mjs +230 -0
  61. package/src/builders/lint-fix.mjs +117 -9
  62. package/src/builders/lint.mjs +25 -20
  63. package/src/builders/list-jsx.mjs +88 -0
  64. package/src/builders/placement.mjs +133 -0
  65. package/src/builders/plan-checks.mjs +135 -0
  66. package/src/builders/plan-comments.mjs +19 -0
  67. package/src/builders/plan-handlers.mjs +95 -0
  68. package/src/builders/plan-markdown-edit.mjs +3 -3
  69. package/src/builders/plan-overlay.js +1530 -1608
  70. package/src/builders/plan.mjs +689 -186
  71. package/src/builders/preset-definitions.json +1 -0
  72. package/src/builders/preset-eval.mjs +244 -0
  73. package/src/builders/preview.html +51 -120
  74. package/src/builders/red-thread-backbone.mjs +135 -36
  75. package/src/builders/repair.mjs +205 -0
  76. package/src/builders/repeat-groups.mjs +540 -0
  77. package/src/builders/resolve-skill.mjs +5 -4
  78. package/src/builders/review-overlay.js +296 -293
  79. package/src/builders/review.mjs +305 -84
  80. package/src/builders/rule-shift.mjs +96 -0
  81. package/src/builders/seat.mjs +354 -0
  82. package/src/builders/settle-driver.mjs +236 -0
  83. package/src/builders/settle.mjs +1448 -0
  84. package/src/builders/slide-prep.mjs +67 -0
  85. package/src/builders/spec-assets.mjs +236 -0
  86. package/src/builders/spec-match.mjs +56 -0
  87. package/src/builders/stack.mjs +413 -0
  88. package/src/builders/storyboard-markdown.mjs +70 -27
  89. package/src/builders/storyboard-schema.mjs +54 -114
  90. package/src/builders/text-blocks.mjs +212 -0
  91. package/src/builders/text-card-fit.mjs +552 -0
  92. package/src/builders/text-stack.mjs +276 -0
  93. package/src/builders/transcribe.mjs +1069 -0
  94. package/src/cli/assets.mjs +164 -0
  95. package/src/cli/common.mjs +173 -12
  96. package/src/cli/concept-spec-support.mjs +76 -0
  97. package/src/cli/concepts.mjs +1002 -103
  98. package/src/cli/deck.mjs +401 -65
  99. package/src/cli/diagnose.mjs +4 -2
  100. package/src/cli/engine-helpers.mjs +17 -10
  101. package/src/cli/engine-results.mjs +76 -3
  102. package/src/cli/help.mjs +232 -28
  103. package/src/cli/install.mjs +9 -0
  104. package/src/cli/maintenance.mjs +104 -0
  105. package/src/cli/read.mjs +2 -1
  106. package/src/cli/ready.mjs +6 -3
  107. package/src/cli/record.mjs +522 -0
  108. package/src/cli/remote.mjs +818 -0
  109. package/src/cli/revision.mjs +686 -0
  110. package/src/cli/search.mjs +30 -11
  111. package/src/cli/session.mjs +56 -13
  112. package/src/cli/setup.mjs +46 -2
  113. package/src/cli/status.mjs +142 -33
  114. package/src/cli/tasks.mjs +246 -0
  115. package/src/cli/transcribe.mjs +854 -0
  116. package/src/cli/workspace-ui.mjs +260 -0
  117. package/src/cli/workspace.mjs +212 -34
  118. package/src/install-channel.mjs +64 -7
  119. package/src/lib/adopt.mjs +346 -25
  120. package/src/lib/concept-spec-binding.mjs +96 -0
  121. package/src/lib/conversions.mjs +70 -6
  122. package/src/lib/convert-folded.mjs +107 -0
  123. package/src/lib/deck-paths.mjs +418 -0
  124. package/src/lib/durable-ids.mjs +47 -0
  125. package/src/lib/edit-operations.mjs +316 -0
  126. package/src/lib/edit-state.mjs +1244 -0
  127. package/src/lib/edit-transitions.mjs +120 -0
  128. package/src/lib/extensions.mjs +501 -0
  129. package/src/lib/image-assets.mjs +465 -0
  130. package/src/lib/image-derivatives.mjs +234 -0
  131. package/src/lib/image-intent-fingerprint.mjs +70 -0
  132. package/src/lib/image-intent-tags.mjs +176 -0
  133. package/src/lib/image-intent.mjs +802 -0
  134. package/src/lib/image-use-overrides.mjs +279 -0
  135. package/src/lib/inline-components.mjs +613 -0
  136. package/src/lib/lint-converted.mjs +68 -4
  137. package/src/lib/merge-json.mjs +163 -0
  138. package/src/lib/overwrite-guard.mjs +3 -3
  139. package/src/lib/presentation-context.mjs +531 -0
  140. package/src/lib/preservation-tags.mjs +157 -0
  141. package/src/lib/preservation-verdict.mjs +1342 -0
  142. package/src/lib/publish.mjs +1557 -0
  143. package/src/lib/rebase.mjs +945 -0
  144. package/src/lib/reconcile-ack.mjs +4 -6
  145. package/src/lib/reconcile.mjs +62 -24
  146. package/src/lib/record-client.mjs +175 -0
  147. package/src/lib/record-state.mjs +151 -0
  148. package/src/lib/remote.mjs +661 -0
  149. package/src/lib/revision.mjs +1210 -0
  150. package/src/lib/slide-guids.mjs +4 -6
  151. package/src/lib/slide-preservation.mjs +423 -0
  152. package/src/lib/slide-selection.mjs +9 -0
  153. package/src/lib/workspace-transaction.mjs +117 -0
  154. package/src/pptx/content-digest.mjs +334 -0
  155. package/src/pptx/deck-assemble.mjs +1278 -0
  156. package/src/pptx/fill/engine.mjs +2 -3
  157. package/src/pptx/fill/freeform.mjs +6 -3
  158. package/src/pptx/ooxml.mjs +20 -0
  159. package/src/pptx/package-evidence.mjs +383 -0
  160. package/src/pptx/tags.mjs +163 -11
  161. package/src/update-check.mjs +22 -4
  162. package/src/workspace/adapters.mjs +222 -0
  163. package/src/workspace/auth.mjs +256 -0
  164. package/src/workspace/autostart.mjs +204 -0
  165. package/src/workspace/daemon.mjs +360 -0
  166. package/src/workspace/discovery.mjs +180 -0
  167. package/src/workspace/edit.mjs +167 -0
  168. package/src/workspace/events.mjs +52 -0
  169. package/src/workspace/gate-client.mjs +290 -0
  170. package/src/workspace/handback.mjs +73 -0
  171. package/src/workspace/lifecycle.mjs +635 -0
  172. package/src/workspace/operations.mjs +200 -0
  173. package/src/workspace/pick-folder.mjs +152 -0
  174. package/src/workspace/registry.mjs +359 -0
  175. package/src/workspace/remote-links.mjs +397 -0
  176. package/src/workspace/remote-status.mjs +359 -0
  177. package/src/workspace/routes.mjs +1331 -0
  178. package/src/workspace/server.mjs +159 -0
  179. package/src/workspace/sync.mjs +1119 -0
  180. package/src/workspace/task-actions.mjs +215 -0
  181. package/src/workspace/tasks.mjs +507 -0
  182. package/src/workspace/ui/app.js +1886 -0
  183. package/src/workspace/ui/index.html +141 -0
  184. package/src/workspace/ui/plan-bridge.js +120 -0
  185. package/src/workspace/ui/sharepoint.svg +59 -0
  186. package/src/workspace/ui/styles.css +423 -0
  187. package/src/builders/visual-families.mjs +0 -46
@@ -16,6 +16,10 @@
16
16
  // - Approve -> POST to the local server, which unblocks the command with the
17
17
  // sign-off. Refused while drafts exist — send or delete them first, exactly
18
18
  // the add-in's rule.
19
+ // - Every decision goes through the shell's decision wire (SHELL.decide): one
20
+ // attempt at a time, an operationId reused on every retry, and each refusal
21
+ // (a busy deck, a newer version, a sign-out, a stopped workspace) handled on
22
+ // its own — what the reviewer wrote survives every one of them.
19
23
  //
20
24
  // TWO MODES, not "reviewing vs. veiled over":
21
25
  // - PARKED (the command is blocking on this tab): pinning, sending and
@@ -49,6 +53,7 @@
49
53
 
50
54
  var STAGE_SEL = "#stage";
51
55
  var HEADER_H = 62;
56
+ var chromeH = HEADER_H; // HEADER_H + any banner stacked under the bar
52
57
  var SIDEBAR_W = 316; // panel + gaps reserved on the right when comments exist
53
58
  // The ledger IS the state: one flat list of this session's pins. Pins, sidebar
54
59
  // rows and the approve guard are all derived from it, so a comment keeps its
@@ -70,13 +75,20 @@
70
75
  // as window.__SLIDEMIND_GATE__): the deck-keyed sessionStorage ledger, the
71
76
  // server-nonce round transition, the round-ready announce and the heartbeat
72
77
  // poll — one implementation, shared with the plan gate.
78
+ // The storage namespace: inside the workspace the registry id (two decks may
79
+ // share a name, and one tab's sessionStorage serves every deck it frames);
80
+ // standalone the deck name, as before.
81
+ var DECK_KEY = CFG.embedded && CFG.deckId ? "id:" + CFG.deckId : CFG.deck || "";
73
82
  var SES = window.__SLIDEMIND_GATE__.createGateSession({
74
83
  name: "review",
75
- deck: CFG.deck || "",
84
+ deck: DECK_KEY,
76
85
  storage: sessionStorage,
77
86
  fetch: function (u, o) {
78
87
  return fetch(u, o);
79
88
  },
89
+ endpoint: CFG.endpoint || "/review",
90
+ embedded: !!CFG.embedded,
91
+ subject: CFG.subjectHash || null, // the content version this page shows (rides in ctx)
80
92
  round: CFG.round || "",
81
93
  service: "slidemind-review",
82
94
  heartbeat: CFG.heartbeat || "/__review/heartbeat",
@@ -97,128 +109,94 @@
97
109
  // the sidebar jump to any slide by synthesising the deck's arrow-key nav.
98
110
  var slideOrder = [];
99
111
 
100
- // ---------- brand assets ----------
101
- // The add-in's embossed "raised slide" mark (src/ui/public/logo.svg), inlined.
102
- var LOGO_SVG =
103
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="160 160 960 540" preserveAspectRatio="none">' +
104
- '<defs>' +
105
- '<linearGradient id="rimTop" gradientUnits="userSpaceOnUse" x1="160" y1="0" x2="1120" y2="0">' +
106
- '<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.85"/>' +
107
- '<stop offset="35%" stop-color="#fff" stop-opacity="0.5"/>' +
108
- '<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
109
- '<linearGradient id="rimLeft" gradientUnits="userSpaceOnUse" x1="0" y1="160" x2="0" y2="700">' +
110
- '<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.6"/>' +
111
- '<stop offset="40%" stop-color="#fff" stop-opacity="0.28"/>' +
112
- '<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
113
- '<linearGradient id="rimBottom" gradientUnits="userSpaceOnUse" x1="1120" y1="0" x2="160" y2="0">' +
114
- '<stop offset="0%" stop-color="#000" stop-opacity="0.55"/>' +
115
- '<stop offset="60%" stop-color="#000" stop-opacity="0.2"/>' +
116
- '<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
117
- '<linearGradient id="rimRight" gradientUnits="userSpaceOnUse" x1="0" y1="700" x2="0" y2="160">' +
118
- '<stop offset="0%" stop-color="#000" stop-opacity="0.45"/>' +
119
- '<stop offset="60%" stop-color="#000" stop-opacity="0.16"/>' +
120
- '<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
121
- '<linearGradient id="faceFill" gradientUnits="userSpaceOnUse" x1="160" y1="160" x2="1120" y2="700">' +
122
- '<stop offset="0%" stop-color="#fff" stop-opacity="0.08"/>' +
123
- '<stop offset="50%" stop-color="#808080" stop-opacity="0.02"/>' +
124
- '<stop offset="100%" stop-color="#000" stop-opacity="0.10"/></linearGradient>' +
125
- '</defs>' +
126
- '<rect x="160" y="160" width="960" height="540" rx="40" ry="40" fill="url(#faceFill)"/>' +
127
- '<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"/>' +
128
- '<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"/>' +
129
- '<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"/>' +
130
- '<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"/>' +
131
- '<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"/>' +
132
- "</svg>";
133
-
134
- // Give a copy of the logo unique filter/gradient ids so two instances (header
135
- // + done veil) can coexist without id collisions.
136
- function logoWithIds(suffix) {
137
- return LOGO_SVG.replace(/id="([^"]+)"/g, 'id="$1' + suffix + '"').replace(
138
- /url\(#([^)]+)\)/g,
139
- "url(#$1" + suffix + ")",
140
- );
141
- }
112
+ // The shared chrome: bar, strips, hand-off dialog, veil, buttons, icons.
113
+ var SHELL = window.__SLIDEMIND_SHELL__.createGateShell({
114
+ CFG: CFG,
115
+ session: SES,
116
+ service: "slidemind-review",
117
+ what: { revising: "the slides", approved: "Slides" },
118
+ // The agent changes slides only while it works on an answer, and then asks
119
+ // again with a new preview. Approved once and not asked again: a later change
120
+ // never comes back here, so there is nothing to say — the "Approved" badge's
121
+ // tooltip already says later changes go straight into the PowerPoint file.
122
+ staleLine: CFG.embedded && CFG.approved && !CFG.asked
123
+ ? null
124
+ : "The slides changed after this preview was made — your agent will show you the new version.",
125
+ isWorking: function () {
126
+ return working;
127
+ },
128
+ workingNote: function () {
129
+ return workingNote;
130
+ },
131
+ onOffset: function (total) {
132
+ // A strip under the bar makes the chrome taller: the rail follows the
133
+ // CSS var (set by the shell), the stage is refit under it.
134
+ chromeH = total;
135
+ applyStageFit();
136
+ },
137
+ onSaveCtx: function () {
138
+ saveCtx();
139
+ },
140
+ onChange: function () {
141
+ renderAll();
142
+ },
143
+ onDecided: function (decision, ack, body) {
144
+ decided(decision, body);
145
+ },
146
+ onDecisionFailed: function () {
147
+ renderAll();
148
+ },
149
+ onAlreadyAnswered: function (a) {
150
+ enterWorking(a && a.action === "approve" ? "Already approved." : a && a.action ? "Comments already sent." : "Already answered in another window.");
151
+ },
152
+ onApprovalPickedUp: function () {
153
+ showDone();
154
+ },
155
+ onLost: function () {
156
+ startNextRoundPolling(); // a re-run of the review reloads this tab, drafts and all
157
+ },
158
+ // A pin being written is not saved anywhere yet: a newer version waits for it.
159
+ reloadBlocker: function () {
160
+ var ta = draftPopover && draftPopover.querySelector("textarea");
161
+ return ta && ta.value.trim() ? "typing" : null;
162
+ },
163
+ reloadIntoNewRound: function () {
164
+ saveLedger();
165
+ saveCtx();
166
+ SES.markRoundReadyAndReload();
167
+ },
168
+ });
169
+ var el = SHELL.el;
170
+ var ICONS = SHELL.ICONS;
171
+ var mkButton = SHELL.mkButton;
142
172
 
143
- // Lucide icons (MIT) — the same family the add-in's review UI uses.
144
- var ICONS = {
145
- check:
146
- '<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>',
147
- message:
148
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M7 9h10"/><path d="M7 13h6"/></svg>',
149
- trash:
150
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>',
151
- close:
152
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="M6 6l12 12"/></svg>',
153
- pin:
154
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 4.4-8 12-8 12s-8-7.6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>',
155
- arrow:
156
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>',
157
- };
173
+ // The stage cursor while pinning: a comment bubble (the add-in's affordance);
174
+ // `crosshair` when a browser refuses the data-URL cursor.
175
+ var COMMENT_CURSOR =
176
+ "url(\"data:image/svg+xml," +
177
+ encodeURIComponent(
178
+ '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">' +
179
+ '<path d="M5 23.5V9.5a5 5 0 0 1 5-5h9a5 5 0 0 1 5 5v5a5 5 0 0 1-5 5h-8.6z" fill="#161618" fill-opacity=".96" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/>' +
180
+ '<path d="M14.5 9.2v5.6M11.7 12h5.6" stroke="#a9c2ff" stroke-width="1.7" stroke-linecap="round"/>' +
181
+ "</svg>",
182
+ ) +
183
+ '") 5 24, crosshair';
158
184
 
159
- // ---------- styles ----------
185
+ // ---------- styles (review-specific; the chrome comes from the shell) ----------
160
186
  var CSS =
161
- "@keyframes sm-drift-a{0%,100%{transform:translateX(-5px)}50%{transform:translateX(5px)}}" +
162
- "@keyframes sm-drift-b{0%,100%{transform:translateX(5px)}50%{transform:translateX(-5px)}}" +
163
- "@keyframes sm-bar-in{from{transform:translateY(-100%);opacity:0}to{transform:none;opacity:1}}" +
164
- "@keyframes sm-pop-in{from{opacity:0;transform:translateY(5px) scale(.98)}to{opacity:1;transform:none}}" +
165
187
  "@keyframes sm-pin-in{from{transform:translate(-50%,-50%) scale(0)}to{transform:translate(-50%,-50%) scale(1)}}" +
166
- "@keyframes sm-veil-in{from{opacity:0}to{opacity:1}}" +
167
- "@keyframes sm-rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}" +
168
- "@keyframes sm-pulse{0%,100%{opacity:.45}50%{opacity:.85}}" +
169
- ":root{--sm-bg:var(--deck-chrome-bg,#111113);--sm-surface:var(--deck-chrome-bg-2,#161618);" +
170
- "--sm-border:var(--deck-chrome-border,#2b2b30);--sm-fg:var(--deck-chrome-fg,#ededed);" +
171
- "--sm-dim:var(--deck-chrome-dim,#9a9aa0);--sm-accent:#a9c2ff;--sm-success:#2ecc71;--sm-danger:#f87171;" +
172
- "--sm-font:'Poppins',ui-sans-serif,system-ui,sans-serif;--sm-word:'Urbanist',var(--sm-font);}" +
173
- ".sm-o,.sm-o *{box-sizing:border-box;}" +
174
- /* header */
175
- ".sm-bar{position:fixed;top:0;left:0;right:0;z-index:2147483600;display:flex;align-items:center;" +
176
- "gap:16px;height:var(--deck-chrome-h,62px);padding:0 18px;color:var(--sm-fg);font-family:var(--sm-font);font-size:13px;" +
177
- "background:var(--sm-bg);border-bottom:1px solid var(--sm-border);" +
178
- "user-select:none;animation:sm-bar-in .5s cubic-bezier(.2,.7,.2,1);}" +
179
- ".sm-brand{display:flex;align-items:center;gap:11px;}" +
180
- ".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);}" +
181
- ".sm-logo-svg{position:absolute;inset:0;z-index:1;}.sm-logo-svg svg{width:100%;height:100%;display:block;}" +
182
- ".sm-logo-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;}" +
183
- ".sm-word{display:flex;flex-direction:column;line-height:.98;font-family:var(--sm-word);" +
184
- "font-weight:300;font-size:13px;letter-spacing:.46em;color:var(--sm-fg);}" +
185
- ".sm-word b{font-weight:300;display:block;will-change:transform;}" +
186
- ".sm-word .top{animation:sm-drift-a 20s ease-in-out infinite;}" +
187
- ".sm-word .bot{margin-left:.18em;animation:sm-drift-b 20s ease-in-out infinite;}" +
188
- ".sm-sep{width:1px;height:28px;background:var(--sm-border);}" +
189
- ".sm-meta{display:flex;flex-direction:column;gap:2px;}" +
190
- ".sm-meta .t{font-weight:600;font-size:13px;}" +
191
- ".sm-meta .h{font-size:11.5px;color:var(--sm-dim);}" +
192
- ".sm-note{max-width:min(560px,38vw);font-size:10.5px;line-height:1.35;color:#c7c7cf;" +
193
- "border-left:2px solid var(--sm-accent);padding-left:10px;}" +
194
- ".sm-note b{font-weight:600;color:#f4f4f5;}" +
195
188
  ".sm-chart-note{display:none;border-left-color:#fbbf24;}" +
196
- ".sm-chart-note.on{display:block;}" +
189
+ ".sm-chart-note.on{display:-webkit-box;}" +
197
190
  ".sm-bar~.deck__rail,.deck__rail{top:var(--deck-chrome-h,62px);}" +
198
- ".sm-grow{flex:1;}" +
199
- ".sm-count{font-size:12px;color:var(--sm-dim);font-variant-numeric:tabular-nums;}" +
191
+ "@media (max-width:760px){.sm-bar~.deck__rail,.deck__rail{top:auto;}}" +
200
192
  /* working strip: the agent is fixing — the preview stays live behind it */
201
193
  ".sm-status{display:none;align-items:center;gap:9px;max-width:min(420px,32vw);padding:7px 12px;border-radius:10px;" +
202
194
  "border:1px solid color-mix(in srgb,var(--sm-accent) 32%,transparent);" +
203
195
  "background:color-mix(in srgb,var(--sm-accent) 10%,transparent);font-size:11.5px;line-height:1.4;color:#d7d7de;}" +
204
- ".sm-status.on{display:flex;}" +
196
+ ".sm-status.on{display:flex;flex-shrink:0;max-height:52px;overflow:hidden;}" +
197
+ /* the working chip outranks the notes beside it: one of them fits a 62px bar */
198
+ ".sm-status.on~.sm-note{display:none;}" +
205
199
  ".sm-status b{display:block;font-weight:600;font-size:12px;color:#f4f4f5;}" +
206
- ".sm-spin{width:11px;height:11px;flex-shrink:0;border-radius:50%;border:2px solid var(--sm-accent);" +
207
- "border-top-color:transparent;animation:sm-spin 0.9s linear infinite;}" +
208
- "@keyframes sm-spin{to{transform:rotate(360deg)}}" +
209
- /* buttons */
210
- ".sm-btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:9px;" +
211
- "padding:8px 14px;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;" +
212
- "transition:transform .14s ease,background .14s ease,box-shadow .14s ease,border-color .14s ease;white-space:nowrap;}" +
213
- ".sm-btn svg{width:15px;height:15px;flex-shrink:0;}" +
214
- ".sm-btn-primary{background:#f4f4f5;color:#0f0f0f;}" +
215
- ".sm-btn-primary:hover{background:#fff;transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,.45);}" +
216
- ".sm-btn-primary svg{color:var(--sm-success);}" +
217
- ".sm-btn-ghost{background:transparent;border-color:var(--sm-border);color:var(--sm-fg);}" +
218
- ".sm-btn-ghost:hover{background:rgba(255,255,255,.06);border-color:#3a3a40;}" +
219
- ".sm-btn-danger{background:transparent;border-color:rgba(248,113,113,.4);color:var(--sm-danger);}" +
220
- ".sm-btn-danger:hover{background:rgba(248,113,113,.12);}" +
221
- ".sm-btn.is-off{opacity:.4;cursor:not-allowed;pointer-events:none;}" +
222
200
  /* pins */
223
201
  ".sm-pinlayer{position:fixed;inset:0;z-index:2147483500;pointer-events:none;}" +
224
202
  ".sm-pin{position:fixed;transform:translate(-50%,-50%);width:26px;height:26px;border-radius:50%;" +
@@ -237,35 +215,20 @@
237
215
  ".sm-pop-head{display:flex;align-items:center;gap:6px;font-weight:600;font-size:11.5px;" +
238
216
  "color:var(--sm-accent);margin-bottom:9px;word-break:break-all;}" +
239
217
  ".sm-pop-head svg{width:13px;height:13px;flex-shrink:0;}" +
240
- ".sm-pop-head .mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}" +
218
+ ".sm-pop-head .mono{font-family:var(--sm-mono);}" +
241
219
  ".sm-ta{width:100%;min-height:74px;resize:vertical;background:#0c0c0e;color:var(--sm-fg);" +
242
220
  "border:1px solid var(--sm-border);border-radius:8px;padding:9px;font:inherit;outline:none;transition:border-color .15s;}" +
243
221
  ".sm-ta:focus{border-color:var(--sm-accent);}.sm-ta::placeholder{color:#6b6b72;}" +
244
222
  ".sm-row{display:flex;gap:8px;margin-top:10px;}.sm-row.end{justify-content:flex-end;}.sm-row.split{justify-content:space-between;}" +
245
223
  ".sm-pop .sm-btn{padding:6px 12px;font-size:12px;}" +
246
224
  ".sm-ghost-body{white-space:pre-wrap;color:#d4d4d8;font-size:12.5px;line-height:1.5;}" +
247
- /* done veil */
248
- ".sm-veil{position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;" +
249
- "justify-content:center;gap:30px;font-family:var(--sm-font);color:var(--sm-fg);" +
250
- "background:radial-gradient(ellipse at center,rgba(16,16,18,.93),rgba(5,5,7,.98));" +
251
- "-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);animation:sm-veil-in .4s ease;}" +
252
- ".sm-veil .sm-logo{width:188px;height:106px;border-radius:14px;animation:sm-rise .6s cubic-bezier(.2,.7,.2,1) both;}" +
253
- ".sm-veil .word{display:flex;flex-direction:column;gap:6px;font-family:var(--sm-word);font-weight:300;" +
254
- "font-size:36px;letter-spacing:.6em;text-align:center;animation:sm-rise .6s .06s cubic-bezier(.2,.7,.2,1) both;}" +
255
- ".sm-veil .word b{font-weight:300;}" +
256
- ".sm-veil .word .top{animation:sm-drift-a 18s ease-in-out infinite;}" +
257
- ".sm-veil .word .bot{margin-left:.3em;animation:sm-drift-b 18s ease-in-out infinite;}" +
258
- ".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;}" +
259
- ".sm-veil .status svg{width:24px;height:24px;color:var(--sm-success);}" +
260
- ".sm-veil .sub{color:var(--sm-dim);font-size:13.5px;animation:sm-rise .6s .18s cubic-bezier(.2,.7,.2,1) both;}" +
261
- ".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;}" +
262
225
  /* hover box */
263
226
  ".sm-hover{position:fixed;z-index:2147483450;pointer-events:none;border-radius:5px;border:2px solid var(--sm-accent);" +
264
227
  "box-shadow:0 0 0 1px rgba(0,0,0,.3),0 6px 22px rgba(0,0,0,.28);opacity:0;" +
265
228
  "transition:left .07s ease,top .07s ease,width .07s ease,height .07s ease,opacity .12s ease;}" +
266
229
  /* sidebar */
267
230
  "@keyframes sm-side-in{from{opacity:0;transform:translateX(16px)}to{opacity:1;transform:none}}" +
268
- ".sm-side{position:fixed;top:74px;right:14px;bottom:14px;width:288px;z-index:2147483520;display:flex;flex-direction:column;" +
231
+ ".sm-side{position:fixed;top:calc(var(--deck-chrome-h,62px) + 12px);right:14px;bottom:14px;width:288px;z-index:2147483520;display:flex;flex-direction:column;" +
269
232
  "background:rgba(18,18,20,.93);-webkit-backdrop-filter:blur(13px);backdrop-filter:blur(13px);border:1px solid var(--sm-border);" +
270
233
  "border-radius:14px;box-shadow:0 18px 54px rgba(0,0,0,.5);font-family:var(--sm-font);color:var(--sm-fg);overflow:hidden;" +
271
234
  "animation:sm-side-in .28s cubic-bezier(.2,.7,.2,1);}" +
@@ -274,7 +237,7 @@
274
237
  ".sm-side-head .n{margin-left:auto;color:var(--sm-dim);font-weight:500;font-variant-numeric:tabular-nums;}" +
275
238
  ".sm-side-body{flex:1;overflow-y:auto;padding:8px 8px 12px;}" +
276
239
  ".sm-side-grp{margin-top:9px;}.sm-side-grp:first-child{margin-top:2px;}" +
277
- ".sm-side-grp-h{font-size:11px;font-weight:600;color:var(--sm-dim);padding:6px 8px 4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}" +
240
+ ".sm-side-grp-h{font-size:11px;font-weight:600;color:var(--sm-dim);padding:6px 8px 4px;font-family:var(--sm-mono);}" +
278
241
  ".sm-side-sub{font-size:10px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--sm-dim);padding:7px 8px 2px;}" +
279
242
  ".sm-side-row{display:flex;align-items:flex-start;gap:9px;padding:7px 8px;border-radius:9px;cursor:pointer;transition:background .12s;}" +
280
243
  ".sm-side-row:hover{background:rgba(255,255,255,.05);}" +
@@ -282,12 +245,14 @@
282
245
  "font-size:10px;font-weight:600;margin-top:1px;background:#161618;color:#fff;border:1.5px solid rgba(255,255,255,.85);}" +
283
246
  ".sm-side-chip.ghost{background:rgba(20,20,22,.6);color:#a1a1aa;border:1.5px dashed #71717a;}" +
284
247
  ".sm-side-txt{font-size:12.5px;line-height:1.4;color:#e4e4e7;word-break:break-word;}" +
285
- ".sm-side-loc{font-size:10.5px;color:var(--sm-dim);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;margin-top:2px;word-break:break-all;}" +
248
+ ".sm-side-loc{font-size:10.5px;color:var(--sm-dim);font-family:var(--sm-mono);margin-top:2px;word-break:break-all;}" +
286
249
  /* ledger status chip — where each pin stands, per row */
287
250
  ".sm-side-state{display:inline-block;margin-top:4px;padding:1px 7px;border-radius:999px;font-size:9.5px;font-weight:600;" +
288
251
  "letter-spacing:.05em;text-transform:uppercase;border:1px solid var(--sm-border);color:var(--sm-dim);}" +
289
252
  '.sm-side-state[data-state="draft"]{border-color:color-mix(in srgb,var(--sm-accent) 45%,transparent);color:var(--sm-accent);}' +
290
- '.sm-side-state[data-state="sent"]{border-color:color-mix(in srgb,var(--sm-accent) 30%,transparent);color:#c6d4f5;}';
253
+ '.sm-side-state[data-state="sent"]{border-color:color-mix(in srgb,var(--sm-accent) 30%,transparent);color:#c6d4f5;}' +
254
+ /* small screens: the comment rail docks under the stage instead of over it */
255
+ "@media (max-width:760px){.sm-side{top:auto;right:8px;left:8px;bottom:8px;width:auto;max-height:38vh;}.sm-note{display:none;}}";
291
256
 
292
257
  // ---------- helpers ----------
293
258
  function stemOf(jsxPath) {
@@ -334,36 +299,6 @@
334
299
  return c.status === "draft";
335
300
  });
336
301
  }
337
- function el(tag, cls, html) {
338
- var e = document.createElement(tag);
339
- if (cls) e.className = cls;
340
- if (html != null) e.innerHTML = html;
341
- return e;
342
- }
343
- function wordmarkHtml() {
344
- return '<b class="top">slide</b><b class="bot">mind</b>';
345
- }
346
- // A logo mark. Genuine path: the add-in's per-brand orb PNG (CFG.logoMark),
347
- // shipped on the bundle and shown verbatim. Fallback (no PNG materialized
348
- // yet): a plain embossed slide glyph — no CLI-side orb re-creation.
349
- function logoNode(cls, idSuffix) {
350
- var box = el("div", cls);
351
- if (CFG.logoMark) {
352
- var img = el("img", "sm-logo-img");
353
- img.src = CFG.logoMark;
354
- img.alt = "slidemind";
355
- box.appendChild(img);
356
- return box;
357
- }
358
- box.appendChild(el("div", "sm-logo-svg", idSuffix ? logoWithIds(idSuffix) : LOGO_SVG));
359
- return box;
360
- }
361
- function mkButton(label, variant, iconName) {
362
- var b = el("button", "sm-btn sm-btn-" + variant);
363
- b.innerHTML = (iconName ? ICONS[iconName] : "") + "<span>" + label + "</span>";
364
- return b;
365
- }
366
-
367
302
  // Build the resolvedTarget HitRegion for an element, in the same shape the
368
303
  // add-in's preview-review uses, so the harness-side formatter ports verbatim.
369
304
  function describeTarget(elx) {
@@ -399,19 +334,33 @@
399
334
  // The context the NEXT round restores from: the server nonce (the round
400
335
  // marker, added by the shared session), which slide the reviewer was on, and
401
336
  // whether this round was already sent (so a plain refresh stays in WORKING).
337
+ function notAskedYet() {
338
+ return SHELL.notAskedYet();
339
+ }
340
+ var arrivedAfterRevising = false; // this load is the round the reviewer was waiting for
341
+ var askedAgainUnchanged = false; // …and the agent asked again without changing anything
342
+
402
343
  function saveCtx() {
403
- SES.saveCtx({ round: roundNo, stem: currentStem, working: working });
344
+ var f = { round: roundNo, stem: currentStem, working: working };
345
+ var shell = SHELL.ctxFields(); // the hand-off dialog and the revising strip
346
+ for (var k in shell) f[k] = shell[k];
347
+ SES.saveCtx(f);
404
348
  }
405
349
 
406
350
  // Restore this tab's session and work out whether THIS load is a new round.
407
351
  // Runs before the first render so the chrome is built in the right mode.
408
352
  function restoreSession() {
409
- var st = SES.restore(CFG.ledgerSeed);
353
+ // A changed nonce while the agent still revises this page's comments is not
354
+ // the next round (SHELL.holdsRound) — nothing it was handed is answered yet.
355
+ var st = SES.restore(CFG.ledgerSeed, { hold: SHELL.holdsRound });
410
356
  ledger = st.ledger;
411
357
  roundNo = st.roundNo;
412
358
  working = st.working;
413
359
  // Reviewer context survives the round: come back to the same slide.
414
360
  if (st.ctx && typeof st.ctx.stem === "string" && st.ctx.stem) pendingStem = st.ctx.stem;
361
+ arrivedAfterRevising = SHELL.restoreCtx(st.ctx, working, st.newRound);
362
+ // Same bytes, asked again: the earlier pins stay "sent", never "worked in".
363
+ askedAgainUnchanged = !!(st.unchanged && st.hadSent);
415
364
  saveLedger();
416
365
  saveCtx();
417
366
  }
@@ -419,31 +368,20 @@
419
368
  // ---------- chrome (top bar) ----------
420
369
  var bar, approveBtn, sendBtn, countLabel, statusEl, chartNoteEl;
421
370
  function buildChrome() {
422
- bar = el("div", "sm-o sm-bar");
423
- bar.setAttribute("data-sm-overlay", "1");
424
-
425
- var brand = el("div", "sm-brand");
426
- brand.appendChild(logoNode("sm-logo"));
427
- brand.appendChild(el("div", "sm-word", wordmarkHtml()));
428
- bar.appendChild(brand);
429
- bar.appendChild(el("div", "sm-sep"));
430
-
431
- var meta = el("div", "sm-meta");
432
- meta.appendChild(el("div", "t", "Review"));
433
- meta.appendChild(el("div", "h", "Click any element to comment · ← → to switch slide"));
434
- bar.appendChild(meta);
435
-
371
+ var built = SHELL.buildBar("Review", "Click any element to comment · ← → to switch slide");
372
+ bar = built.bar;
373
+ var grow = built.grow;
436
374
  // The working strip: shown instead of the old full-screen waiting veil, so
437
375
  // the preview behind it stays readable and pinnable while the agent fixes.
438
376
  statusEl = el("div", "sm-o sm-status");
439
- bar.appendChild(statusEl);
377
+ grow.appendChild(statusEl);
440
378
 
441
379
  var note = el(
442
380
  "div",
443
381
  "sm-note",
444
- "<b>Ask for slide changes here.</b> Direct PowerPoint edits after approval are not tracked and may be overwritten. Preview may differ slightly from final PowerPoint.",
382
+ "<b>Ask for slide changes here.</b> You approve once: your agent then builds the deck and makes later changes directly in PowerPoint. Preview may differ slightly from final PowerPoint.",
445
383
  );
446
- bar.appendChild(note);
384
+ grow.appendChild(note);
447
385
 
448
386
  // Add-in parity: a slide holding a <Chart> shows a stand-in, never
449
387
  // PowerPoint's render — warn on exactly those slides.
@@ -452,9 +390,11 @@
452
390
  "sm-note sm-chart-note",
453
391
  "<b>Preview only</b> — the chart may look different in PowerPoint.",
454
392
  );
455
- bar.appendChild(chartNoteEl);
456
-
457
- bar.appendChild(el("div", "sm-grow"));
393
+ grow.appendChild(chartNoteEl);
394
+ grow.style.display = "flex";
395
+ grow.style.alignItems = "center";
396
+ grow.style.gap = "16px";
397
+ SHELL.fitBar(); // with no room in the middle, the notes fold behind the (i)
458
398
 
459
399
  countLabel = el("div", "sm-count");
460
400
  bar.appendChild(countLabel);
@@ -470,7 +410,6 @@
470
410
  approveBtn.onclick = approve;
471
411
  bar.appendChild(approveBtn);
472
412
 
473
- document.body.appendChild(bar);
474
413
  refreshChrome();
475
414
  }
476
415
  // Approve is refused while the agent is fixing (the render on screen is
@@ -482,30 +421,63 @@
482
421
  if (!countLabel) return;
483
422
  var open = openComments().length;
484
423
  var d = draftComments().length;
424
+ // the in-flight latch: one answer at a time, a double click cannot send twice
425
+ var pending = SHELL.decisionPending();
426
+ var sending = pending && SHELL.state.pending;
485
427
  countLabel.textContent =
486
428
  ledger.length === 0
487
429
  ? "no comments yet"
488
430
  : open + " open · " + ledger.length + " total";
489
431
  sendBtn.style.display = !working && d > 0 ? "" : "none";
490
- sendBtn.querySelector("span").textContent =
491
- "Send " + d + (d === 1 ? " comment" : " comments");
492
- var off = working || d > 0;
432
+ sendBtn.disabled = pending;
433
+ sendBtn.classList.toggle("is-off", pending);
434
+ // in the workspace the answer also copies the message the user pastes to
435
+ // their agent; the dialog after the click says so, the button stays short
436
+ sendBtn.querySelector("span").textContent = sending && sending.decision === "comments" ? "Sending…" : "Send " + d + (d === 1 ? " comment" : " comments");
437
+ var already = SHELL.alreadyApproved();
438
+ var notAsked = notAskedYet();
439
+ // an approval would cover a preview older than the slides (comments stay open)
440
+ var stale = SHELL.isPreviewStale() && !working;
441
+ var off = working || d > 0 || already || notAsked || pending || stale;
493
442
  approveBtn.disabled = off;
494
443
  approveBtn.setAttribute("aria-disabled", off ? "true" : "false");
495
444
  approveBtn.classList.toggle("is-off", off);
496
- approveBtn.title = working
497
- ? "The agent is fixing the slides — approve once the next round arrives."
445
+ approveBtn.querySelector("span").textContent = already
446
+ ? SHELL.reviewNotNeeded() ? "No review needed" : "Approved"
447
+ : notAsked
448
+ ? "Approve when your agent asks"
449
+ : sending && sending.decision === "approve"
450
+ ? "Approving…"
451
+ : CFG.embedded
452
+ ? "Approve slides"
453
+ : "Approve & continue";
454
+ approveBtn.title = already
455
+ ? SHELL.reviewNotNeeded()
456
+ ? "This deck came from your PowerPoint file, so there is nothing to approve. Pin a comment if something must change."
457
+ : "These slides are approved. Pin a comment if something must still change."
458
+ : notAsked
459
+ ? "Your agent is still working on the slides. It asks for your review when they are ready."
460
+ : pending
461
+ ? "Sending your answer…"
462
+ : stale
463
+ ? "Your agent changed the slides after this preview was made — approve the new version when it shows it to you."
464
+ : working
465
+ ? (CFG.embedded ? "Your answer is saved — approve the next version when your agent asks." : "The agent is fixing the slides — approve once the next round arrives.")
498
466
  : d > 0
499
467
  ? "Send or delete your comments before approving."
500
- : "Approve and continue.";
501
- statusEl.classList.toggle("on", working);
502
- if (working) {
468
+ : CFG.embedded
469
+ ? "Approve these slides. You then paste a message into your agent chat."
470
+ : "Approve and continue.";
471
+ // Embedded, the working state is the full-width strip under the bar
472
+ // (renderRevising); in the terminal gate the bar chip carries it.
473
+ var chip = working && !CFG.embedded;
474
+ statusEl.classList.toggle("on", chip);
475
+ if (chip) {
503
476
  statusEl.innerHTML = '<div class="sm-spin"></div>';
504
477
  var txt = el("div");
505
478
  txt.appendChild(el("div", null, "<b>The agent is working through your comments</b>"));
506
479
  txt.appendChild(el("div", null, "")).textContent =
507
- (workingNote ? workingNote + " " : "") +
508
- "This tab refreshes with the next round — comment again then.";
480
+ (workingNote ? workingNote + " " : "") + "This tab refreshes with the next round.";
509
481
  statusEl.appendChild(txt);
510
482
  }
511
483
  }
@@ -559,13 +531,19 @@
559
531
  var st = stage();
560
532
  if (!st) return;
561
533
  var rail = document.getElementById("slide-rail");
562
- var railW = rail && getComputedStyle(rail).display !== "none" ? rail.offsetWidth : 0;
563
- var reserve = sidebarOpen ? SIDEBAR_W : 0;
534
+ // The rail sits left (its width) or, on small screens, along the bottom
535
+ // (its height) — see preview.html's media query.
536
+ var railShown = rail && getComputedStyle(rail).display !== "none";
537
+ var railBottom = railShown && rail.offsetWidth >= window.innerWidth - 1;
538
+ var railW = railShown && !railBottom ? rail.offsetWidth : 0;
539
+ var railH = railBottom ? rail.offsetHeight : 0;
540
+ var narrow = window.innerWidth <= 760;
541
+ var reserve = sidebarOpen && !narrow ? SIDEBAR_W : 0;
564
542
  var availW = window.innerWidth - railW - reserve;
565
- var availH = window.innerHeight - HEADER_H - 8;
543
+ var availH = window.innerHeight - chromeH - railH - 8;
566
544
  var s = Math.min(availW / 1280, availH / 720) * 0.94;
567
545
  var offX = -reserve / 2;
568
- var offY = HEADER_H / 2;
546
+ var offY = (chromeH - railH) / 2;
569
547
  st.style.transform = "translate(" + offX + "px," + offY + "px) scale(" + s + ")";
570
548
  renderPins();
571
549
  }
@@ -579,10 +557,10 @@
579
557
  };
580
558
  }
581
559
  // Only a DRAFT is still the reviewer's to change; once it is with the agent
582
- // it is read-only history.
560
+ // — or on its way there (SHELL.inFlight) — it is read-only history.
583
561
  function openCommentAt(c) {
584
562
  var p = commentScreenPoint(c);
585
- if (c.status === "draft") openEditor(c, p.x, p.y);
563
+ if (c.status === "draft" && !SHELL.inFlight(c.id)) openEditor(c, p.x, p.y);
586
564
  else openGhost(c, p.x, p.y);
587
565
  }
588
566
  function statusLabel(c) {
@@ -712,6 +690,49 @@
712
690
  var py = sr.top + fy(c) * sr.height;
713
691
  pin.style.left = px + "px";
714
692
  pin.style.top = py + "px";
693
+ if (c.status === "draft" && !SHELL.inFlight(c.id)) {
694
+ // A draft pin can be dragged onto the right spot: the comment keeps its
695
+ // text, its slide fraction (and the element under it) are re-read on drop.
696
+ pin.style.cursor = "grab";
697
+ pin.addEventListener("pointerdown", function (e) {
698
+ if (e.button !== 0) return;
699
+ var startX = e.clientX, startY = e.clientY, moved = false;
700
+ pin.setPointerCapture(e.pointerId);
701
+ function onMove(ev) {
702
+ if (!moved && Math.hypot(ev.clientX - startX, ev.clientY - startY) < 4) return;
703
+ moved = true;
704
+ pin.style.cursor = "grabbing";
705
+ pin.style.left = ev.clientX + "px";
706
+ pin.style.top = ev.clientY + "px";
707
+ }
708
+ function onUp(ev) {
709
+ pin.removeEventListener("pointermove", onMove);
710
+ pin.removeEventListener("pointerup", onUp);
711
+ pin.removeEventListener("pointercancel", onUp);
712
+ pin.style.cursor = "grab";
713
+ if (!moved) return; // a click: the onclick below opens the editor
714
+ var r = stage() && stage().getBoundingClientRect();
715
+ if (!r) return;
716
+ c.x = clamp01((ev.clientX - r.left) / r.width);
717
+ c.y = clamp01((ev.clientY - r.top) / r.height);
718
+ pin.style.pointerEvents = "none";
719
+ var under = document.elementFromPoint(ev.clientX, ev.clientY);
720
+ pin.style.pointerEvents = "";
721
+ c.resolvedTarget = under && stage().contains(under) && under !== stage() ? describeTarget(under) : null;
722
+ saveLedger();
723
+ renderAll();
724
+ }
725
+ pin.addEventListener("pointermove", onMove);
726
+ pin.addEventListener("pointerup", onUp);
727
+ pin.addEventListener("pointercancel", onUp);
728
+ });
729
+ pin.addEventListener("click", function (e) {
730
+ e.stopPropagation();
731
+ if (pin.style.cursor === "grabbing") return;
732
+ openCommentAt(c);
733
+ });
734
+ return pin;
735
+ }
715
736
  pin.onclick = function (e) {
716
737
  e.stopPropagation();
717
738
  openCommentAt(c);
@@ -862,61 +883,47 @@
862
883
  renderAll();
863
884
  }
864
885
  // "Send N comments": the whole draft batch resolves the gate as one comments
865
- // round, the drafts flip to "sent" and the tab enters working mode.
886
+ // round. The shell carries it to the server (retries a busy deck, waits out a
887
+ // stopped workspace) and calls decided() once it is saved — only then do the
888
+ // drafts flip to "sent" and the tab enter working mode.
866
889
  function sendComments() {
867
- if (approved || working) return;
890
+ if (approved || working || SHELL.decisionPending()) return;
868
891
  var batch = draftComments();
869
892
  if (!batch.length) return;
870
893
  // Ask for notification permission HERE — the click is a user gesture, the
871
- // only context where the browser will show the prompt.
872
- SES.requestNotifyPermission();
873
- fetch(CFG.endpoint, {
874
- method: "POST",
875
- headers: { "Content-Type": "application/json" },
876
- body: JSON.stringify({ decision: "comments", comments: batch.map(SES.wire) }),
877
- })
878
- .then(function (res) {
879
- if (res.status === 409) {
880
- // Another tab already decided this run. The drafts stay drafts — the
881
- // next round reloads this tab and the reviewer can send them then.
882
- enterWorking("Round superseded.");
883
- return;
884
- }
885
- if (!res.ok) throw new Error("gate refused the decision");
886
- batch.forEach(function (x) {
887
- x.status = "sent";
888
- });
889
- saveLedger();
890
- enterWorking();
891
- })
892
- .catch(function () {
893
- // The command is gone (killed / timed out). The drafts stay in the tab.
894
- alert("Could not reach the slidemind review server — is the terminal command still running?");
895
- });
894
+ // only context where the browser will show the prompt. Standalone only: in
895
+ // the workspace nothing waits in the background, and a permission prompt
896
+ // there is friction on the click that copies the message.
897
+ if (!CFG.embedded) SES.requestNotifyPermission();
898
+ SHELL.decide({ decision: "comments", comments: batch.map(SES.wire) });
899
+ renderAll();
896
900
  }
897
901
  function approve() {
898
902
  if (approved || working || draftComments().length) return;
899
- approved = true;
900
- fetch(CFG.endpoint, {
901
- method: "POST",
902
- headers: { "Content-Type": "application/json" },
903
- body: JSON.stringify({ decision: "approve", comments: [] }),
904
- })
905
- .then(function (res) {
906
- if (res.status === 409) {
907
- // Another tab already decided this run: the render is superseded.
908
- approved = false;
909
- enterWorking("Round superseded.");
910
- return;
911
- }
912
- showDone();
913
- requestTabClose();
914
- })
915
- .catch(function () {
916
- approved = false;
917
- alert("Could not reach the slidemind review server — is the terminal command still running?");
918
- });
903
+ if (SHELL.decisionPending() || SHELL.alreadyApproved() || notAskedYet() || SHELL.isPreviewStale()) return;
904
+ SHELL.decide({ decision: "approve", comments: [] });
905
+ renderAll();
919
906
  }
907
+ // The server saved the answer (never before: a refused POST leaves every
908
+ // draft a draft). The rows the request carried are the ones that are sent.
909
+ function decided(decision, body) {
910
+ if (decision === "approve") {
911
+ approved = true;
912
+ showDone();
913
+ requestTabClose();
914
+ return;
915
+ }
916
+ var sentIds = {};
917
+ ((body && body.comments) || []).forEach(function (c) {
918
+ sentIds[c.id] = true;
919
+ });
920
+ ledger.forEach(function (x) {
921
+ if (x.status === "draft" && sentIds[x.id]) x.status = "sent";
922
+ });
923
+ saveLedger();
924
+ enterWorking(CFG.embedded ? "Comments saved." : null);
925
+ }
926
+
920
927
  // ---------- working mode (the agent is fixing) ----------
921
928
  // The tab neither veils over nor freezes: the stage stays browsable and the
922
929
  // ledger stays visible; pinning and Approve go away until the next round.
@@ -930,20 +937,18 @@
930
937
  hideHoverBox();
931
938
  saveCtx(); // a plain refresh of this round stays in working mode
932
939
  renderAll();
940
+ SHELL.renderRevising();
933
941
  startNextRoundPolling();
934
942
  }
935
943
  function requestTabClose() {
936
- setTimeout(function () {
937
- try {
938
- window.open("", "_self");
939
- window.close();
940
- } catch {
941
- // Some browsers only allow script-opened tabs to close themselves.
942
- }
943
- }, 120);
944
+ if (CFG.embedded) return; // the workspace page stays; nothing to close
945
+ SHELL.requestTabClose();
944
946
  }
945
- // The shared heartbeat poll (gate-session.js) with this gate's two endings.
947
+ // The shared heartbeat poll (gate-session.js) with this gate's two endings —
948
+ // standalone only: in the workspace page the shell's monitor watches the round
949
+ // for the page's whole life, and the page never "ends".
946
950
  function startNextRoundPolling() {
951
+ if (CFG.embedded) return;
947
952
  SES.startNextRoundPolling({
948
953
  onEnded: showSessionEnded,
949
954
  // A retired tab still holding unsent drafts must not discard them: the
@@ -955,27 +960,33 @@
955
960
  },
956
961
  });
957
962
  }
958
- // Shared full-screen veil (logo + wordmark + status + sub). Replaces any veil
959
- // already shown rather than stacking a second one, so a comments round's
960
- // "waiting" veil can be swapped for "session ended" in place.
963
+ // The shared veil, with this gate's own nodes taken off screen behind it.
961
964
  function showVeil(statusHtml, subText, subPulse) {
962
965
  closePopover();
963
- if (pinLayer) pinLayer.style.display = "none";
964
- if (bar) bar.style.display = "none";
965
- var existing = document.querySelector(".sm-veil[data-sm-overlay]");
966
- if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
967
- var veil = el("div", "sm-o sm-veil");
968
- veil.setAttribute("data-sm-overlay", "1");
969
- veil.appendChild(logoNode("sm-logo", "-v"));
970
- veil.appendChild(el("div", "word", wordmarkHtml()));
971
- veil.appendChild(el("div", "status", statusHtml));
972
- veil.appendChild(el("div", "sub" + (subPulse ? " sm-pulse" : ""), subText));
973
- document.body.appendChild(veil);
966
+ return SHELL.showVeil(statusHtml, subText, subPulse, [pinLayer, sidebarEl, hoverBox]);
974
967
  }
975
968
  // The veil is now ONLY for states where the review is over (approved, session
976
969
  // expired, superseded). A comments round no longer veils: see enterWorking.
970
+ // Embedded, the veil says the agent HAS the approval — only once the wire
971
+ // proved it (delivered, or picked up later). Until then the approval is saved
972
+ // and the strip + hand-off say what is still missing.
977
973
  function showDone() {
978
- showVeil(ICONS.check + "<span>Approved</span>", "Returned to your terminal.");
974
+ if (CFG.embedded && SHELL.state.phase !== "approved") {
975
+ enterWorking("Approved.");
976
+ return;
977
+ }
978
+ // working, so a reload (the approval itself may move the round) restores
979
+ // this state instead of a fresh "approved — nothing to do" page
980
+ if (CFG.embedded && !working) enterWorking("Approved.");
981
+ var picked = SHELL.state.pickedUpAt;
982
+ showVeil(
983
+ ICONS.check + "<span>Approved</span>",
984
+ !CFG.embedded
985
+ ? "Returned to your terminal."
986
+ : picked
987
+ ? "Your agent picked it up at " + SHELL.hhmm(picked) + " and is building the PowerPoint now."
988
+ : "Your agent has it and is building the PowerPoint now.",
989
+ );
979
990
  }
980
991
  function showSessionEnded() {
981
992
  showVeil("<span>Session ended</span>", "You can close this tab.");
@@ -1008,29 +1019,12 @@
1008
1019
  var st = stage();
1009
1020
  var hasChart = !!(st && st.querySelector("[data-sm-chart]"));
1010
1021
  chartNoteEl.classList.toggle("on", hasChart);
1022
+ SHELL.fitBar();
1011
1023
  }
1012
1024
 
1013
1025
  // ---------- wire up ----------
1014
1026
  function injectAssets() {
1015
- var style = el("style");
1016
- style.id = "sm-review-style";
1017
- style.textContent = CSS;
1018
- document.head.appendChild(style);
1019
-
1020
- var pre1 = el("link");
1021
- pre1.rel = "preconnect";
1022
- pre1.href = "https://fonts.googleapis.com";
1023
- var pre2 = el("link");
1024
- pre2.rel = "preconnect";
1025
- pre2.href = "https://fonts.gstatic.com";
1026
- pre2.crossOrigin = "anonymous";
1027
- var fonts = el("link");
1028
- fonts.rel = "stylesheet";
1029
- fonts.href =
1030
- "https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&family=Urbanist:wght@300;400&display=swap";
1031
- document.head.appendChild(pre1);
1032
- document.head.appendChild(pre2);
1033
- document.head.appendChild(fonts);
1027
+ SHELL.injectAssets(CSS);
1034
1028
  }
1035
1029
 
1036
1030
  function init() {
@@ -1045,12 +1039,19 @@
1045
1039
  restoreSession();
1046
1040
 
1047
1041
  injectAssets();
1048
- st.style.cursor = "crosshair";
1042
+ st.style.cursor = COMMENT_CURSOR;
1049
1043
  // Slide play order, for the sidebar's jump-to-slide.
1050
1044
  document.querySelectorAll('script[data-kind="slide"]').forEach(function (s) {
1051
1045
  slideOrder[parseInt(s.dataset.index, 10)] = s.dataset.name;
1052
1046
  });
1053
1047
  buildChrome();
1048
+ if (CFG.embedded && working) setTimeout(SHELL.renderRevising, 0); // a refresh mid-round keeps the strip
1049
+ if (CFG.embedded && working && SHELL.state.phase === "approved") setTimeout(showDone, 0); // the agent has the approval
1050
+ if (CFG.embedded && !working && askedAgainUnchanged && CFG.asked) setTimeout(SHELL.showAskedAgainUnchanged, 0);
1051
+ else if (CFG.embedded && !working && arrivedAfterRevising && CFG.asked) setTimeout(function () { SHELL.showArrived("It worked through your comments. Have another look."); }, 0);
1052
+ if (SHELL.alreadyApproved()) setTimeout(function () { SHELL.showApprovedBanner(SHELL.reviewNotNeeded() ? "This deck came from your PowerPoint file, so your agent makes changes directly in it." : "Later changes go straight into the PowerPoint file — no new review needed."); }, 0);
1053
+ if (SHELL.askedAgain()) setTimeout(function () { SHELL.showApprovedBanner("Approve again to let it continue, or pin a comment if something must still change.", "Your agent asks you to confirm the slides again"); }, 0);
1054
+ if (notAskedYet()) setTimeout(function () { SHELL.showNotAskedBanner("Your agent is still working on the slides. Look around if you like — this page updates by itself when it asks you."); }, 0);
1054
1055
  buildPinLayer();
1055
1056
  buildHoverBox();
1056
1057
 
@@ -1084,7 +1085,7 @@
1084
1085
 
1085
1086
  window.addEventListener("resize", function () {
1086
1087
  closePopover();
1087
- applyStageFit();
1088
+ SHELL.offsetSurface(); // a banner rewraps at a new width; it refits the stage
1088
1089
  });
1089
1090
 
1090
1091
  var nav = document.getElementById("nav");
@@ -1109,6 +1110,8 @@
1109
1110
  // Parked round: the agent is waiting on the reviewer — announce it.
1110
1111
  SES.announceNewRound(flagged);
1111
1112
  }
1113
+ // The hand-over in progress, the next-load note, an answer on its way.
1114
+ setTimeout(SHELL.resume, 0);
1112
1115
  }
1113
1116
 
1114
1117
  if (document.readyState === "loading") {