dsh-plugin-lookatstudy 0.26.0 → 0.26.2

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/lib/client.js CHANGED
@@ -1787,6 +1787,64 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
1787
1787
  }));
1788
1788
  }
1789
1789
  //#endregion
1790
+ //#region src/client/stage-session.ts
1791
+ /** The plugin's private retention label (never the view owner's 'mainView'). */
1792
+ const STAGE_SOURCE = "lookatstudy";
1793
+ /** Which staging generation the host speaks ('retain' wins when both exist). */
1794
+ function stagingMode(sessions) {
1795
+ return typeof sessions.retain === "function" ? "retain" : "open";
1796
+ }
1797
+ let held = null;
1798
+ /**
1799
+ * Stage `sessionId` for observation + addressability.
1800
+ * alpha: holds the plugin's reference. rc: `open()` navigation. Throws
1801
+ * whatever the host face throws — callers keep their existing catch policy.
1802
+ */
1803
+ function stageSession(sessions, sessionId) {
1804
+ if (stagingMode(sessions) === "retain") {
1805
+ if (held !== null && held.sessionId === sessionId) return;
1806
+ const previous = held;
1807
+ held = sessions.retain(sessionId, { source: STAGE_SOURCE });
1808
+ previous?.release();
1809
+ } else sessions.open?.(sessionId);
1810
+ }
1811
+ /** Release the held alpha reference (plugin dispose — across open/close
1812
+ * toggles the reference stays held, mirroring rc's sticky current). */
1813
+ function unstageSession() {
1814
+ held?.release();
1815
+ held = null;
1816
+ }
1817
+ /**
1818
+ * Hold a TRANSIENT alpha reference for an effect's lifetime so scope/binding
1819
+ * resolve for an un-staged session (E1/E6 projections, thread rename).
1820
+ * rc: a no-op closer — listed ids are addressable there without help, and
1821
+ * calling open() from inside an effect would trip the rc hand-back.
1822
+ * alpha: undefined only when the retain is refused (treat as feature-absent).
1823
+ */
1824
+ function acquireStageReference(sessions, sessionId) {
1825
+ if (sessions.retain === void 0) return () => {};
1826
+ let ref;
1827
+ try {
1828
+ ref = sessions.retain(sessionId, { source: STAGE_SOURCE });
1829
+ } catch {
1830
+ return;
1831
+ }
1832
+ return () => {
1833
+ ref?.release();
1834
+ ref = void 0;
1835
+ };
1836
+ }
1837
+ /**
1838
+ * Derive the host main-view selection from a list snapshot the way
1839
+ * ui-workspace does: alpha removed `list.current`, and the view owner's
1840
+ * `mainView` retention is the only public trace of the main selection.
1841
+ * Pure; rc snapshots carry no `retainedBy` and read as no selection.
1842
+ */
1843
+ function mainSelectionOf(byId) {
1844
+ if (byId === void 0) return void 0;
1845
+ for (const entry of Object.entries(byId)) if ((entry[1]?.retainedBy?.mainView ?? 0) > 0) return entry[0];
1846
+ }
1847
+ //#endregion
1790
1848
  //#region src/client/data.ts
1791
1849
  /**
1792
1850
  * Shared client-side study store: one module-level poller over the host
@@ -8485,9 +8543,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8485
8543
  }
8486
8544
  const shell = PANEL_SHELL.current;
8487
8545
  if (shell !== null) shell.suppressHandBack(() => {
8488
- ctx.sessions.open(sessionId);
8546
+ stageSession(ctx.sessions, sessionId);
8489
8547
  });
8490
- else ctx.sessions.open(sessionId);
8548
+ else stageSession(ctx.sessions, sessionId);
8491
8549
  const actx = ctx.sessions.scope(sessionId);
8492
8550
  const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
8493
8551
  if (face === void 0) throw new Error("import session is not addressable yet");
@@ -8530,6 +8588,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8530
8588
  (0, react.useEffect)(() => {
8531
8589
  setPressure(null);
8532
8590
  if (boundId === null) return;
8591
+ const closeStage = acquireStageReference(ctx.sessions, boundId);
8533
8592
  let disposed = false;
8534
8593
  let off;
8535
8594
  try {
@@ -8555,22 +8614,27 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8555
8614
  }
8556
8615
  };
8557
8616
  read();
8558
- off = proj.subscribe(read);
8617
+ off = proj.subscribe(() => {
8618
+ queueMicrotask(read);
8619
+ });
8559
8620
  return () => {
8560
8621
  disposed = true;
8561
8622
  off?.();
8623
+ closeStage?.();
8562
8624
  };
8563
8625
  }
8564
8626
  } catch {}
8565
8627
  return () => {
8566
8628
  disposed = true;
8567
8629
  off?.();
8630
+ closeStage?.();
8568
8631
  };
8569
8632
  }, [boundId, ctx.sessions]);
8570
8633
  const [modelFace, setModelFace] = (0, react.useState)(null);
8571
8634
  (0, react.useEffect)(() => {
8572
8635
  setModelFace(null);
8573
8636
  if (boundId === null) return;
8637
+ const closeStage = acquireStageReference(ctx.sessions, boundId);
8574
8638
  let disposed = false;
8575
8639
  let off;
8576
8640
  try {
@@ -8582,16 +8646,20 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8582
8646
  if (!disposed && snap !== void 0 && typeof snap === "object" && snap.next !== void 0 && snap.next !== null) setModelFace(snap.next);
8583
8647
  };
8584
8648
  read();
8585
- off = proj.subscribe(read);
8649
+ off = proj.subscribe(() => {
8650
+ queueMicrotask(read);
8651
+ });
8586
8652
  return () => {
8587
8653
  disposed = true;
8588
8654
  off?.();
8655
+ closeStage?.();
8589
8656
  };
8590
8657
  }
8591
8658
  } catch {}
8592
8659
  return () => {
8593
8660
  disposed = true;
8594
8661
  off?.();
8662
+ closeStage?.();
8595
8663
  };
8596
8664
  }, [boundId, ctx.sessions]);
8597
8665
  (0, react.useEffect)(() => {
@@ -8620,7 +8688,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8620
8688
  };
8621
8689
  update();
8622
8690
  setFeedAttached(true);
8623
- offSource = source.subscribe(update);
8691
+ offSource = source.subscribe(() => {
8692
+ queueMicrotask(update);
8693
+ });
8624
8694
  return true;
8625
8695
  };
8626
8696
  const stage = () => {
@@ -8628,13 +8698,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8628
8698
  try {
8629
8699
  const shell = PANEL_SHELL.current;
8630
8700
  if (shell !== null) shell.suppressHandBack(() => {
8631
- ctx.sessions.open(boundId);
8701
+ stageSession(ctx.sessions, boundId);
8632
8702
  });
8633
- else ctx.sessions.open(boundId);
8703
+ else stageSession(ctx.sessions, boundId);
8634
8704
  } catch {}
8635
8705
  };
8636
8706
  const reconcile = () => {
8637
8707
  if (disposed || offSource !== void 0) return true;
8708
+ if (stagingMode(ctx.sessions) === "retain") {
8709
+ stage();
8710
+ return attach();
8711
+ }
8638
8712
  if (ctx.sessions.list?.getSnapshot().current !== boundId) {
8639
8713
  stage();
8640
8714
  return false;
@@ -8648,10 +8722,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8648
8722
  }, 150);
8649
8723
  const list = ctx.sessions.list;
8650
8724
  if (list !== void 0) offList = list.subscribe(() => {
8651
- if (disposed || reconcile()) {
8652
- offList?.();
8653
- offList = void 0;
8654
- }
8725
+ queueMicrotask(() => {
8726
+ if (disposed || reconcile()) {
8727
+ offList?.();
8728
+ offList = void 0;
8729
+ }
8730
+ });
8655
8731
  });
8656
8732
  return () => {
8657
8733
  disposed = true;
@@ -8716,9 +8792,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8716
8792
  }
8717
8793
  const shell = PANEL_SHELL.current;
8718
8794
  if (shell !== null) shell.suppressHandBack(() => {
8719
- ctx.sessions.open(sessionId);
8795
+ stageSession(ctx.sessions, sessionId);
8720
8796
  });
8721
- else ctx.sessions.open(sessionId);
8797
+ else stageSession(ctx.sessions, sessionId);
8722
8798
  const actx = ctx.sessions.scope(sessionId);
8723
8799
  const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
8724
8800
  if (face === void 0) throw new Error("lesson session is not addressable yet");
@@ -8758,9 +8834,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8758
8834
  bindLessonSession(lesson.lessonId, sessionId);
8759
8835
  const shell = PANEL_SHELL.current;
8760
8836
  if (shell !== null) shell.suppressHandBack(() => {
8761
- ctx.sessions.open(sessionId);
8837
+ stageSession(ctx.sessions, sessionId);
8762
8838
  });
8763
- else ctx.sessions.open(sessionId);
8839
+ else stageSession(ctx.sessions, sessionId);
8764
8840
  };
8765
8841
  const startNewThread = () => {
8766
8842
  if (lesson === null) return;
@@ -8773,8 +8849,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8773
8849
  const renameLessonThreadUi = (sessionId, title) => {
8774
8850
  if (lesson === null) return;
8775
8851
  lessonThreadOp(lesson.lessonId, sessionId, "rename", { title }).then(() => {
8852
+ const closeRef = acquireStageReference(ctx.sessions, sessionId);
8853
+ if (closeRef === void 0) return;
8776
8854
  const actx = ctx.sessions.scope(sessionId);
8777
- (actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx))?.rename?.(title).catch(() => {});
8855
+ (actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx))?.rename?.(title).catch(() => {}).finally(() => {
8856
+ closeRef();
8857
+ });
8778
8858
  }, (err) => {
8779
8859
  setSendError(friendlyError(err, "action"));
8780
8860
  });
@@ -11127,8 +11207,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
11127
11207
  * event.
11128
11208
  *
11129
11209
  * Differences from stardeck's original: our own attribute namespace, a
11130
- * hand-back SUPPRESSION hook (the panel itself calls sessions.open to stage
11131
- * the focus lesson's thread — an internal navigation must not close the
11210
+ * hand-back SUPPRESSION hook (the panel itself stages the focus lesson's
11211
+ * thread through stage-session.ts — an internal navigation must not close the
11132
11212
  * panel; only USER session navigation hands the column back), and dsw-token
11133
11213
  * styling via the injected stylesheet.
11134
11214
  * @module dsh-plugin-lookatstudy/client/shell-entry
@@ -11287,13 +11367,21 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
11287
11367
  doc.addEventListener(ACTIVATE_EVENT, onOtherActivate);
11288
11368
  let unsubscribeSessions;
11289
11369
  if (sessions?.list !== void 0) {
11290
- let lastCurrent = sessions.list.getSnapshot().current;
11370
+ const readSelection = () => {
11371
+ const snap = sessions.list.getSnapshot();
11372
+ if (snap.current !== void 0) return snap.current;
11373
+ return mainSelectionOf(snap.byId);
11374
+ };
11375
+ let lastCurrent = readSelection();
11291
11376
  unsubscribeSessions = sessions.list.subscribe(() => {
11292
- const next = sessions.list.getSnapshot().current;
11293
- if (next === lastCurrent) return;
11294
- const wasHydration = lastCurrent === void 0;
11295
- lastCurrent = next;
11296
- if (shouldHandBack(suppressed, wasHydration)) state.setOpen(false);
11377
+ const wasSuppressed = suppressed;
11378
+ queueMicrotask(() => {
11379
+ const next = readSelection();
11380
+ if (next === lastCurrent) return;
11381
+ const wasHydration = lastCurrent === void 0;
11382
+ lastCurrent = next;
11383
+ if (shouldHandBack(wasSuppressed, wasHydration)) state.setOpen(false);
11384
+ });
11297
11385
  });
11298
11386
  }
11299
11387
  const unsubscribe = state.subscribe(applyActive);
@@ -11318,6 +11406,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
11318
11406
  doc.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
11319
11407
  unsubscribeSessions?.();
11320
11408
  unsubscribe();
11409
+ unstageSession();
11321
11410
  root?.unmount();
11322
11411
  container?.remove();
11323
11412
  row.remove();