slidemind 0.3.5 → 0.3.12

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.
@@ -9,34 +9,32 @@
9
9
  // - Click anywhere on the stage -> resolve the innermost element under the
10
10
  // click via closest('[data-sm-loc]'), drop a pin, capture a note.
11
11
  // Slide-level notes (empty-area clicks) carry no element.
12
- // - Saving a pin SENDS it (there is no batch "submit" button): the gate
13
- // resolves at once and the `slidemind review` command returns with it.
12
+ // - Saving a pin keeps it as an unsent DRAFT in this tab (editable, deletable).
13
+ // "Send N comments" in the header submits the whole batch: the gate resolves
14
+ // and the `slidemind review` command returns with it (the add-in's
15
+ // "Submit N comments").
14
16
  // - Approve -> POST to the local server, which unblocks the command with the
15
- // sign-off. No copy-paste.
17
+ // sign-off. Refused while drafts exist — send or delete them first, exactly
18
+ // the add-in's rule.
16
19
  //
17
20
  // TWO MODES, not "reviewing vs. veiled over":
18
- // - PARKED (the command is blocking on this tab): saving a pin resolves the
19
- // gate right now, carrying any queued stragglers with it; Approve is live
20
- // while nothing is outstanding.
21
- // - WORKING (the agent is fixing the slides): the preview stays FULLY
22
- // browsable and pinnable — saved pins QUEUE in sessionStorage (ledger
23
- // status "queued") instead of hitting the server — and only Approve is off
24
- // (the displayed render is already superseded). A status strip says so.
25
- //
26
- // Queued pins are never lost: the tab polls the heartbeat, and when the agent's
27
- // next `slidemind review` run claims it the reload's boot POSTs the whole
28
- // backlog to /review/backlog — which resolves that run immediately as a drain
29
- // round, so the tab simply stays in working mode with the ledger flipped to
30
- // "sent". A tab superseded by a fresh window flushes before retiring, likewise.
21
+ // - PARKED (the command is blocking on this tab): pinning, sending and
22
+ // approving are live.
23
+ // - WORKING (the agent is fixing the slides): the preview stays browsable and
24
+ // the ledger stays visible, but no new pins are taken and Approve is off —
25
+ // the displayed render is already superseded. A status strip says so. The
26
+ // tab polls the heartbeat and reloads itself when the agent's next
27
+ // `slidemind review` run presents the new round. Nothing is queued behind
28
+ // the agent's back (add-in parity: rounds are synchronous).
31
29
  //
32
30
  // ONE LEDGER, three statuses. Every pin is {id, round, jsxPath, resolvedTarget,
33
31
  // x, y, text, status} (x/y are full-precision 0..1 fractions of the slide):
34
- // queued (typed here, not yet delivered) -> sent (the agent has it) ->
35
- // addressed (a newer gate run served this page — the server's round nonce
36
- // changed). Addressed pins are the read-only GHOST pins of previous rounds —
37
- // there is no separate previous-round channel. A tab with no session of its own
38
- // seeds the ledger from CFG.ledgerSeed (the deck's persisted review.json) as
39
- // SENT rows: the agent has just been handed them, nothing has answered them.
32
+ // draft (saved here, not yet sent) -> sent (the agent has it) -> addressed (a
33
+ // newer gate run served this page — the server's round nonce changed).
34
+ // Addressed pins are the read-only GHOST pins of previous rounds — there is no
35
+ // separate previous-round channel. A tab with no session of its own seeds the
36
+ // ledger from CFG.ledgerSeed (the deck's persisted review.json) as SENT rows:
37
+ // the agent has just been handed them, nothing has answered them.
40
38
  //
41
39
  // Branding (lifted from the add-in's product identity):
42
40
  // - The embossed "raised slide" logo (public/logo.svg).
@@ -54,15 +52,15 @@
54
52
  var SIDEBAR_W = 316; // panel + gaps reserved on the right when comments exist
55
53
  // The ledger IS the state: one flat list of this session's pins. Pins, sidebar
56
54
  // rows and the approve guard are all derived from it, so a comment keeps its
57
- // identity across queued -> sent -> addressed (and across the round reload).
55
+ // identity across draft -> sent -> addressed (and across the round reload).
58
56
  var ledger = [];
59
57
  var roundNo = 1; // this tab's round counter (sessionStorage, not server state)
60
58
  var currentStem = null;
61
59
  var pendingStem = null; // the slide to re-anchor on after a round reload
62
60
  var draftPopover = null;
63
61
  var approved = false; // approve latch — the only thing that ends the session
64
- var working = false; // the agent is fixing → pins queue instead of resolving
65
- var workingNote = null; // extra line for the working strip (queued / superseded)
62
+ var working = false; // the agent is fixing → no new pins, no approve
63
+ var workingNote = null; // extra line for the working strip (superseded)
66
64
  var sidebarEl = null;
67
65
  var sidebarOpen = false;
68
66
  var hoverBox = null;
@@ -70,8 +68,8 @@
70
68
 
71
69
  // The shared gate session (src/builders/gate-session.js, inlined into the page
72
70
  // as window.__SLIDEMIND_GATE__): the deck-keyed sessionStorage ledger, the
73
- // backlog flush, the server-nonce round transition, the round-ready announce
74
- // and the heartbeat poll — one implementation, shared with the plan gate.
71
+ // server-nonce round transition, the round-ready announce and the heartbeat
72
+ // poll — one implementation, shared with the plan gate.
75
73
  var SES = window.__SLIDEMIND_GATE__.createGateSession({
76
74
  name: "review",
77
75
  deck: CFG.deck || "",
@@ -82,7 +80,6 @@
82
80
  round: CFG.round || "",
83
81
  service: "slidemind-review",
84
82
  heartbeat: CFG.heartbeat || "/__review/heartbeat",
85
- backlogEndpoint: CFG.backlogEndpoint || "/review/backlog",
86
83
  pollMs: CFG.pollMs,
87
84
  pollTtlMs: CFG.pollTtlMs,
88
85
  roundInSentLabel: true, // "sent · round N" — a pin names the render it sits on
@@ -195,6 +192,8 @@
195
192
  ".sm-note{max-width:min(560px,38vw);font-size:10.5px;line-height:1.35;color:#c7c7cf;" +
196
193
  "border-left:2px solid var(--sm-accent);padding-left:10px;}" +
197
194
  ".sm-note b{font-weight:600;color:#f4f4f5;}" +
195
+ ".sm-chart-note{display:none;border-left-color:#fbbf24;}" +
196
+ ".sm-chart-note.on{display:block;}" +
198
197
  ".sm-bar~.deck__rail,.deck__rail{top:var(--deck-chrome-h,62px);}" +
199
198
  ".sm-grow{flex:1;}" +
200
199
  ".sm-count{font-size:12px;color:var(--sm-dim);font-variant-numeric:tabular-nums;}" +
@@ -226,7 +225,7 @@
226
225
  "font:600 12px var(--sm-font);cursor:pointer;display:flex;align-items:center;justify-content:center;" +
227
226
  "pointer-events:auto;animation:sm-pin-in .26s cubic-bezier(.2,1.5,.4,1);transition:filter .12s;}" +
228
227
  ".sm-pin:hover{filter:brightness(1.12);}" +
229
- /* queued (typed here, not delivered yet) / sent (the agent has it) /
228
+ /* draft (saved here, not sent yet) / sent (the agent has it) /
230
229
  addressed (answered by a newer render — the read-only ghost) */
231
230
  ".sm-pin-active{background:#161618;color:#fff;border:2px solid rgba(255,255,255,.92);box-shadow:0 2px 9px rgba(0,0,0,.55);}" +
232
231
  ".sm-pin-sent{background:#161618;color:var(--sm-accent);border:2px dashed var(--sm-accent);box-shadow:0 2px 9px rgba(0,0,0,.45);}" +
@@ -287,7 +286,7 @@
287
286
  /* ledger status chip — where each pin stands, per row */
288
287
  ".sm-side-state{display:inline-block;margin-top:4px;padding:1px 7px;border-radius:999px;font-size:9.5px;font-weight:600;" +
289
288
  "letter-spacing:.05em;text-transform:uppercase;border:1px solid var(--sm-border);color:var(--sm-dim);}" +
290
- '.sm-side-state[data-state="queued"]{border-color:color-mix(in srgb,var(--sm-accent) 45%,transparent);color:var(--sm-accent);}' +
289
+ '.sm-side-state[data-state="draft"]{border-color:color-mix(in srgb,var(--sm-accent) 45%,transparent);color:var(--sm-accent);}' +
291
290
  '.sm-side-state[data-state="sent"]{border-color:color-mix(in srgb,var(--sm-accent) 30%,transparent);color:#c6d4f5;}';
292
291
 
293
292
  // ---------- helpers ----------
@@ -322,12 +321,17 @@
322
321
  function activeComments() {
323
322
  return commentsForStem(currentStem);
324
323
  }
325
- // Outstanding = written but not yet answered by a render. This is the approve
326
- // guard: signing off while something is outstanding would approve slides the
327
- // agent has been told to change.
324
+ // Outstanding = written but not yet answered by a render (the header count).
328
325
  function openComments() {
329
326
  return ledger.filter(function (c) {
330
- return c.status === "queued" || c.status === "sent";
327
+ return c.status === "draft" || c.status === "sent";
328
+ });
329
+ }
330
+ // Saved here, not sent: the batch "Send comments" submits, and the reason
331
+ // Approve is off while any exist.
332
+ function draftComments() {
333
+ return ledger.filter(function (c) {
334
+ return c.status === "draft";
331
335
  });
332
336
  }
333
337
  function el(tag, cls, html) {
@@ -393,15 +397,10 @@
393
397
  SES.saveLedger(ledger);
394
398
  }
395
399
  // The context the NEXT round restores from: the server nonce (the round
396
- // marker, added by the shared session) plus which slide the reviewer was on.
400
+ // marker, added by the shared session), which slide the reviewer was on, and
401
+ // whether this round was already sent (so a plain refresh stays in WORKING).
397
402
  function saveCtx() {
398
- SES.saveCtx({ round: roundNo, stem: currentStem });
399
- }
400
- function queuedComments() {
401
- return SES.queued(ledger);
402
- }
403
- function flushBacklog() {
404
- return SES.flushBacklog(ledger);
403
+ SES.saveCtx({ round: roundNo, stem: currentStem, working: working });
405
404
  }
406
405
 
407
406
  // Restore this tab's session and work out whether THIS load is a new round.
@@ -418,7 +417,7 @@
418
417
  }
419
418
 
420
419
  // ---------- chrome (top bar) ----------
421
- var bar, approveBtn, countLabel, statusEl;
420
+ var bar, approveBtn, sendBtn, countLabel, statusEl, chartNoteEl;
422
421
  function buildChrome() {
423
422
  bar = el("div", "sm-o sm-bar");
424
423
  bar.setAttribute("data-sm-overlay", "1");
@@ -446,11 +445,27 @@
446
445
  );
447
446
  bar.appendChild(note);
448
447
 
448
+ // Add-in parity: a slide holding a <Chart> shows a stand-in, never
449
+ // PowerPoint's render — warn on exactly those slides.
450
+ chartNoteEl = el(
451
+ "div",
452
+ "sm-note sm-chart-note",
453
+ "<b>Preview only</b> — the chart may look different in PowerPoint.",
454
+ );
455
+ bar.appendChild(chartNoteEl);
456
+
449
457
  bar.appendChild(el("div", "sm-grow"));
450
458
 
451
459
  countLabel = el("div", "sm-count");
452
460
  bar.appendChild(countLabel);
453
461
 
462
+ // The add-in's footer: "Submit N comments" appears once drafts exist and is
463
+ // the ONLY way forward while they do; Approve is the exclusive alternative.
464
+ sendBtn = mkButton("Send comments", "ghost", "arrow");
465
+ sendBtn.onclick = sendComments;
466
+ sendBtn.style.display = "none";
467
+ bar.appendChild(sendBtn);
468
+
454
469
  approveBtn = mkButton("Approve & continue", "primary", "check");
455
470
  approveBtn.onclick = approve;
456
471
  bar.appendChild(approveBtn);
@@ -459,45 +474,38 @@
459
474
  refreshChrome();
460
475
  }
461
476
  // Approve is refused while the agent is fixing (the render on screen is
462
- // superseded) or while pins are QUEUED here — sending them is the reviewer's
463
- // own next action, and approving would drop them. It is NOT refused for
464
- // already-delivered ("sent") pins: the same rule the plan gate uses. Guarding
465
- // on those deadlocked the gate — a sent pin only clears on a round transition,
466
- // so a round that touched no file left Approve off for the rest of the session.
477
+ // superseded) or while DRAFTS exist — the add-in's rule: submit or delete them
478
+ // first. It is NOT refused for already-delivered ("sent") pins: a sent pin
479
+ // only clears on a round transition, so guarding on it deadlocked a round
480
+ // that touched no file.
467
481
  function refreshChrome() {
468
482
  if (!countLabel) return;
469
483
  var open = openComments().length;
470
- var q = queuedComments().length;
484
+ var d = draftComments().length;
471
485
  countLabel.textContent =
472
486
  ledger.length === 0
473
487
  ? "no comments yet"
474
488
  : open + " open · " + ledger.length + " total";
475
- var off = working || q > 0;
489
+ 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;
476
493
  approveBtn.disabled = off;
477
494
  approveBtn.setAttribute("aria-disabled", off ? "true" : "false");
478
495
  approveBtn.classList.toggle("is-off", off);
479
496
  approveBtn.title = working
480
497
  ? "The agent is fixing the slides — approve once the next round arrives."
481
- : q > 0
482
- ? "Send your queued comments first."
498
+ : d > 0
499
+ ? "Send or delete your comments before approving."
483
500
  : "Approve and continue.";
484
501
  statusEl.classList.toggle("on", working);
485
502
  if (working) {
486
503
  statusEl.innerHTML = '<div class="sm-spin"></div>';
487
504
  var txt = el("div");
488
- txt.appendChild(el("div", null, "<b>The agent is fixing the slides</b>"));
489
- txt.appendChild(
490
- el(
491
- "div",
492
- null,
493
- "",
494
- ),
495
- ).textContent =
505
+ txt.appendChild(el("div", null, "<b>The agent is working through your comments</b>"));
506
+ txt.appendChild(el("div", null, "")).textContent =
496
507
  (workingNote ? workingNote + " " : "") +
497
- (q
498
- ? q + (q === 1 ? " comment is" : " comments are") +
499
- " queued — they're delivered with the next round."
500
- : "Keep browsing and keep commenting — new pins are picked up automatically.");
508
+ "This tab refreshes with the next round — comment again then.";
501
509
  statusEl.appendChild(txt);
502
510
  }
503
511
  }
@@ -570,11 +578,11 @@
570
578
  y: sr.top + fy(c) * sr.height,
571
579
  };
572
580
  }
573
- // Only a QUEUED pin is still the reviewer's to change; once it is with the
574
- // agent it is read-only history.
581
+ // Only a DRAFT is still the reviewer's to change; once it is with the agent
582
+ // it is read-only history.
575
583
  function openCommentAt(c) {
576
584
  var p = commentScreenPoint(c);
577
- if (c.status === "queued") openEditor(c, p.x, p.y);
585
+ if (c.status === "draft") openEditor(c, p.x, p.y);
578
586
  else openGhost(c, p.x, p.y);
579
587
  }
580
588
  function statusLabel(c) {
@@ -691,7 +699,7 @@
691
699
  });
692
700
  }
693
701
  function pinVariant(c) {
694
- if (c.status === "queued") return "sm-pin-active";
702
+ if (c.status === "draft") return "sm-pin-active";
695
703
  if (c.status === "sent") return "sm-pin-sent";
696
704
  return "sm-pin-ghost";
697
705
  }
@@ -757,9 +765,8 @@
757
765
  });
758
766
  }
759
767
 
760
- // Saving a pin SENDS it — there is no batch submit. The button label says so
761
- // in each mode, because the consequence differs: parked resolves the gate now,
762
- // working queues for the agent's next run.
768
+ // Saving a pin keeps it as a DRAFT; the header's "Send comments" submits the
769
+ // batch (the add-in's "Save comment" → "Submit N comments").
763
770
  function openDraft(point, region, px, py) {
764
771
  var pop = makePopover(px, py);
765
772
  pop.appendChild(head(targetLabel(region), !!region));
@@ -768,12 +775,12 @@
768
775
  var row = el("div", "sm-row end");
769
776
  var cancel = mkButton("Cancel", "ghost", "close");
770
777
  cancel.onclick = closePopover;
771
- var save = mkButton(working ? "Queue comment" : "Send comment", "primary", "check");
778
+ var save = mkButton("Save comment", "primary", "check");
772
779
  function doSave() {
773
780
  var text = ta.value.trim();
774
781
  if (!text) return;
775
782
  closePopover();
776
- sendComment({
783
+ addDraft({
777
784
  id: genId(),
778
785
  round: roundNo,
779
786
  jsxPath: "slides/" + currentStem + ".jsx",
@@ -781,7 +788,7 @@
781
788
  x: clamp01(point.x),
782
789
  y: clamp01(point.y),
783
790
  text: text,
784
- status: "queued",
791
+ status: "draft",
785
792
  at: new Date().toISOString(),
786
793
  });
787
794
  }
@@ -795,8 +802,8 @@
795
802
  bindTextareaKeys(ta, doSave);
796
803
  }
797
804
 
798
- // Only reachable for a QUEUED pin (openCommentAt routes the rest to the
799
- // read-only view): until it is delivered, the reviewer can still fix or drop it.
805
+ // Only reachable for a DRAFT (openCommentAt routes the rest to the read-only
806
+ // view): until it is sent, the reviewer can still fix or drop it.
800
807
  function openEditor(comment, px, py) {
801
808
  var pop = makePopover(px, py);
802
809
  pop.appendChild(head(targetLabel(comment.resolvedTarget), !!comment.resolvedTarget));
@@ -849,18 +856,19 @@
849
856
  var WAITING_TITLE = "⏳ Agent revising slides — slidemind";
850
857
 
851
858
  // ---------- submit ----------
852
- // A saved pin. WORKING → it queues locally (the agent is mid-round; it rides
853
- // along on the next run). PARKED → it resolves the gate right now, carrying
854
- // any queued stragglers from a flush that couldn't reach the server, and the
855
- // tab enters working mode instead of veiling over.
856
- function sendComment(c) {
859
+ function addDraft(c) {
857
860
  ledger.push(c);
858
861
  saveLedger();
859
862
  renderAll();
860
- if (working) return;
861
- var batch = queuedComments();
862
- // Ask for notification permission HERE — the save click is a user gesture,
863
- // the only context where the browser will show the prompt.
863
+ }
864
+ // "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.
866
+ function sendComments() {
867
+ if (approved || working) return;
868
+ var batch = draftComments();
869
+ if (!batch.length) return;
870
+ // Ask for notification permission HERE — the click is a user gesture, the
871
+ // only context where the browser will show the prompt.
864
872
  SES.requestNotifyPermission();
865
873
  fetch(CFG.endpoint, {
866
874
  method: "POST",
@@ -869,7 +877,8 @@
869
877
  })
870
878
  .then(function (res) {
871
879
  if (res.status === 409) {
872
- // The gate already drained a backlog this run — this round is stale.
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.
873
882
  enterWorking("Round superseded.");
874
883
  return;
875
884
  }
@@ -881,13 +890,12 @@
881
890
  enterWorking();
882
891
  })
883
892
  .catch(function () {
884
- // The command is gone (killed / timed out). The pin stays QUEUED and is
885
- // delivered by the next run — the reviewer's words are never dropped.
886
- enterWorking("Couldn't reach the terminal command.");
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?");
887
895
  });
888
896
  }
889
897
  function approve() {
890
- if (approved || working || queuedComments().length) return;
898
+ if (approved || working || draftComments().length) return;
891
899
  approved = true;
892
900
  fetch(CFG.endpoint, {
893
901
  method: "POST",
@@ -896,7 +904,7 @@
896
904
  })
897
905
  .then(function (res) {
898
906
  if (res.status === 409) {
899
- // A backlog drained this run: the render being approved is superseded.
907
+ // Another tab already decided this run: the render is superseded.
900
908
  approved = false;
901
909
  enterWorking("Round superseded.");
902
910
  return;
@@ -910,15 +918,17 @@
910
918
  });
911
919
  }
912
920
  // ---------- working mode (the agent is fixing) ----------
913
- // The tab neither veils over nor freezes: the stage stays browsable and
914
- // pinnable, the ledger stays visible, and only Approve goes away. Heartbeat
915
- // polling waits for the agent's next `slidemind review` run, which reloads
916
- // this same tab with the new render.
921
+ // The tab neither veils over nor freezes: the stage stays browsable and the
922
+ // ledger stays visible; pinning and Approve go away until the next round.
923
+ // Heartbeat polling waits for the agent's next `slidemind review` run, which
924
+ // reloads this same tab with the new render.
917
925
  function enterWorking(note) {
918
926
  working = true;
919
927
  workingNote = note || null;
920
928
  document.title = WAITING_TITLE; // the tab label signals the state from the tab bar
921
929
  closePopover();
930
+ hideHoverBox();
931
+ saveCtx(); // a plain refresh of this round stays in working mode
922
932
  renderAll();
923
933
  startNextRoundPolling();
924
934
  }
@@ -932,15 +942,16 @@
932
942
  }
933
943
  }, 120);
934
944
  }
935
- // The shared heartbeat poll (gate-session.js) with this gate's two endings:
936
- // a retired tab hands its backlog over FIRST — the fresh window that replaces
937
- // it starts with an empty sessionStorage, so anything still queued here would
938
- // otherwise be lost (the handover also drains the new run immediately).
945
+ // The shared heartbeat poll (gate-session.js) with this gate's two endings.
939
946
  function startNextRoundPolling() {
940
947
  SES.startNextRoundPolling({
941
948
  onEnded: showSessionEnded,
949
+ // A retired tab still holding unsent drafts must not discard them: the
950
+ // fresh window that replaced it has its own (empty) sessionStorage, so
951
+ // this tab reloads into the new round instead and flags itself.
942
952
  onSuperseded: function () {
943
- flushBacklog().then(showSuperseded, showSuperseded);
953
+ if (draftComments().length) SES.markRoundReadyAndReload();
954
+ else showSuperseded();
944
955
  },
945
956
  });
946
957
  }
@@ -989,6 +1000,14 @@
989
1000
  renderPins();
990
1001
  saveCtx(); // the slide the reviewer is on survives the round reload
991
1002
  }
1003
+ refreshChartNote();
1004
+ }
1005
+ // The chart stand-in stamps its spec on its root as data-sm-chart.
1006
+ function refreshChartNote() {
1007
+ if (!chartNoteEl) return;
1008
+ var st = stage();
1009
+ var hasChart = !!(st && st.querySelector("[data-sm-chart]"));
1010
+ chartNoteEl.classList.toggle("on", hasChart);
992
1011
  }
993
1012
 
994
1013
  // ---------- wire up ----------
@@ -1020,8 +1039,8 @@
1020
1039
  setTimeout(init, 50);
1021
1040
  return;
1022
1041
  }
1023
- // Session first: it decides the MODE this page renders in (a queued backlog
1024
- // means the agent is still working) and the round-transition state.
1042
+ // Session first: it decides the MODE this page renders in (a refresh of an
1043
+ // already-sent round stays in working mode) and the round-transition state.
1025
1044
  var flagged = SES.consumeRoundFlag();
1026
1045
  restoreSession();
1027
1046
 
@@ -1035,10 +1054,10 @@
1035
1054
  buildPinLayer();
1036
1055
  buildHoverBox();
1037
1056
 
1038
- // Pinnable in BOTH modes — a pin dropped while the agent is fixing queues
1039
- // instead of resolving the gate. Only an approved session stops taking pins.
1057
+ // Pinnable only while PARKED: while the agent is fixing, the render on
1058
+ // screen is superseded and the strip says to comment on the next round.
1040
1059
  st.addEventListener("click", function (e) {
1041
- if (approved) return;
1060
+ if (approved || working) return;
1042
1061
  if (e.target.closest("[data-sm-overlay]")) return;
1043
1062
  var sr = st.getBoundingClientRect();
1044
1063
  var x = (e.clientX - sr.left) / sr.width;
@@ -1050,7 +1069,7 @@
1050
1069
 
1051
1070
  // Hover-box: outline the element under the cursor (the add-in's affordance).
1052
1071
  st.addEventListener("pointermove", function (e) {
1053
- if (approved || locked) return;
1072
+ if (approved || working || locked) return;
1054
1073
  if (e.target.closest("[data-sm-overlay]")) {
1055
1074
  hideHoverBox();
1056
1075
  return;
@@ -1083,11 +1102,9 @@
1083
1102
  renderAll();
1084
1103
 
1085
1104
  if (working) {
1086
- // Drain round: hand the backlog over, stay in working mode, stay silent —
1087
- // the reviewer already knows the agent is mid-fix.
1105
+ // A refresh of a round the reviewer already sent: keep waiting quietly.
1088
1106
  document.title = WAITING_TITLE;
1089
1107
  startNextRoundPolling();
1090
- flushBacklog().then(renderAll); // rows flip queued → sent
1091
1108
  } else {
1092
1109
  // Parked round: the agent is waiting on the reviewer — announce it.
1093
1110
  SES.announceNewRound(flagged);