@cplieger/web-terminal-ui 7.0.1 → 7.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "7.0.1",
3
+ "version": "7.0.2",
4
4
  "type": "module",
5
5
  "description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
6
6
  "license": "MPL-2.0",
@@ -55,7 +55,7 @@
55
55
  "@vitest/coverage-v8": "4.1.11",
56
56
  "eslint": "10.8.1",
57
57
  "fast-check": "4.9.0",
58
- "happy-dom": "20.11.2",
58
+ "happy-dom": "20.11.6",
59
59
  "html-validate": "11.8.0",
60
60
  "knip": "6.32.2",
61
61
  "prettier": "3.9.6",
@@ -2873,13 +2873,20 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
2873
2873
  // still be), to the terminal otherwise. Recorded at entry because the exit
2874
2874
  // paths cannot tell them apart.
2875
2875
  let editFrom: "keyboard" | "pointer" = "pointer";
2876
- // Tab ids with a pinned-name request in flight. While an id is here, a status
2877
- // record carrying a DIFFERENT value is applied for display but does not
2878
- // supersede the request: SSE delivery order and REST mutation order are not
2879
- // one total order, so a record sampled BEFORE our PUT can arrive after it,
2880
- // and treating it as newer authority would suppress the request's own
2881
- // rollback and its failure toast.
2882
- const namesInFlight = new Set<string>();
2876
+ // Tab ids with pinned-name requests in flight, COUNTED per id. While a count
2877
+ // is held, a status record carrying a DIFFERENT value is applied for display
2878
+ // but does not supersede the request: SSE delivery order and REST mutation
2879
+ // order are not one total order, so a record sampled BEFORE our PUT can
2880
+ // arrive after it, and treating it as newer authority would suppress the
2881
+ // request's own rollback and its failure toast.
2882
+ // Counted rather than a Set of ids, because two renames of one tab can be out
2883
+ // at once and the invariant is "no request of OURS is in flight", not "no id
2884
+ // is marked": a bare Set let the first request's completion clear the marker
2885
+ // while the second's PUT was still open, and the second rename then failed
2886
+ // silently. A per-request token would carry more information, but the only
2887
+ // reader asks whether ANY request is out for this id, and the failure handler
2888
+ // already distinguishes its own supersession by (born, nameSeq).
2889
+ const namesInFlight = new Map<string, number>();
2883
2890
 
2884
2891
  /** restoreFocusAfterEdit sends focus where the entry path implies. The
2885
2892
  * terminal branch is gated exactly like switchTo's focus-on-switch: on a
@@ -2949,8 +2956,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
2949
2956
  const { text, fallback } = baseLabel(t);
2950
2957
  ctx.announce(fallback ? "Custom name removed" : `Using automatic name: ${text}`);
2951
2958
  }
2952
- namesInFlight.add(id);
2953
2959
  const request = name === "" ? api.clearPinnedTitle(id) : api.setPinnedTitle(id, name);
2960
+ // Counted only once the request exists: an api implementation that throws
2961
+ // instead of rejecting would otherwise leave a count behind that no
2962
+ // .finally ever releases, and the id would read as in flight forever.
2963
+ namesInFlight.set(id, (namesInFlight.get(id) ?? 0) + 1);
2954
2964
  void request
2955
2965
  .catch(() => {
2956
2966
  const cur = tabList.find((x) => x.id === id);
@@ -2962,7 +2972,12 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
2962
2972
  ctx.toast("Couldn't save the terminal name");
2963
2973
  })
2964
2974
  .finally(() => {
2965
- namesInFlight.delete(id);
2975
+ const left = (namesInFlight.get(id) ?? 0) - 1;
2976
+ if (left > 0) {
2977
+ namesInFlight.set(id, left);
2978
+ } else {
2979
+ namesInFlight.delete(id);
2980
+ }
2966
2981
  });
2967
2982
  return true;
2968
2983
  }
@@ -274,17 +274,21 @@ function buildTerminal(
274
274
  // Narrow = compact in EITHER dimension: skinny (a phone in portrait, a
275
275
  // narrow embedded panel) or short (a phone in landscape). The one
276
276
  // ResizeObserver below fires on both width and height changes.
277
+ //
278
+ // Constructed unconditionally: ResizeObserver is a hard requirement of this
279
+ // library, not an enhancement. viewport.init() and the tabs strip both
280
+ // construct one with no guard, and the same buildTerminal builds a WeakRef
281
+ // (whose browser floor is strictly higher), so a `typeof ResizeObserver ===
282
+ // "function"` guard here could only ever protect a browser that crashes
283
+ // moments later. It used to be here, and it was measured dead.
277
284
  const isNarrow = (): boolean =>
278
285
  root.clientWidth <= NARROW_MAX_PX || root.clientHeight <= SHORT_MAX_PX;
279
286
  function updateNarrow(): void {
280
287
  root.classList.toggle("wt-narrow", isNarrow());
281
288
  }
282
289
  updateNarrow();
283
- let narrowObserver: ResizeObserver | null = null;
284
- if (typeof ResizeObserver === "function") {
285
- narrowObserver = new ResizeObserver(updateNarrow);
286
- narrowObserver.observe(root);
287
- }
290
+ let narrowObserver: ResizeObserver | null = new ResizeObserver(updateNarrow);
291
+ narrowObserver.observe(root);
288
292
 
289
293
  // --- Document title (kernel-owned, composed from two inputs) ---
290
294
  //