@cplieger/web-terminal-ui 5.4.4 → 5.4.5

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": "5.4.4",
3
+ "version": "5.4.5",
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",
@@ -744,26 +744,58 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
744
744
  // name, `list()` reports the real name, and clearing the progress needs no
745
745
  // cleanup — the next paint simply stops adding it.
746
746
  function relabelAll(): void {
747
- // Count only real (non-fallback) labels, so multiple untitled tabs all
748
- // read "New tab" without a numeric suffix.
749
- const counts = new Map<string, number>();
747
+ // De-duplicate identical labels with a "(k)" suffix, numbered by the session's
748
+ // AGE — never by its position in the strip.
749
+ //
750
+ // The suffix used to be assigned by encounter order while walking tabList, which
751
+ // made it a property of the SLOT: two tabs both called "workspace" always read
752
+ // "workspace" then "workspace (2)" whichever way round they sat, so the label
753
+ // text stayed put while the sessions moved underneath it. Dragging one of them
754
+ // changed nothing on screen, and the reorder — which had worked correctly all
755
+ // along, server included — was invisible. It also meant dragging tab A could
756
+ // renumber tab B, which is the clearest sign the number belonged to the wrong
757
+ // thing.
758
+ //
759
+ // A label answers "which session is this", so it has to be a property of the
760
+ // session: stable while other tabs move, and travelling with its own tab. Age is
761
+ // that key. createdAt (the server's, tie-broken by id) rather than the local
762
+ // `born` counter, so every device and every reload agrees on which "workspace"
763
+ // is (2) — `born` is adoption order, which races.
764
+ //
765
+ // Numbering is recomputed over the LIVE group each time, so it stays contiguous:
766
+ // closing "(2)" of three renumbers the survivor rather than leaving a hole. That
767
+ // is a renumber with a visible cause the user just performed, which is the one
768
+ // kind that reads as sensible.
769
+ const groups = new Map<string, Tab[]>();
750
770
  for (const t of tabList) {
751
771
  const { text, fallback } = baseLabel(t);
752
- if (!fallback) {
753
- counts.set(text, (counts.get(text) ?? 0) + 1);
772
+ // Fallback labels are not real names: several untitled tabs all read
773
+ // "New tab" with no numbering, which is the pre-existing behaviour.
774
+ if (fallback) {
775
+ continue;
776
+ }
777
+ const group = groups.get(text);
778
+ if (group) {
779
+ group.push(t);
780
+ } else {
781
+ groups.set(text, [t]);
754
782
  }
755
783
  }
756
- const seen = new Map<string, number>();
757
- for (const t of tabList) {
758
- const { text, fallback } = baseLabel(t);
759
- let display = text;
760
- if (!fallback && (counts.get(text) ?? 0) > 1) {
761
- const k = (seen.get(text) ?? 0) + 1;
762
- seen.set(text, k);
763
- if (k > 1) {
764
- display = `${text} (${String(k)})`;
765
- }
784
+ const ordinals = new Map<Tab, number>();
785
+ for (const group of groups.values()) {
786
+ if (group.length < 2) {
787
+ continue; // nothing to disambiguate from, so no suffix at all
766
788
  }
789
+ [...group]
790
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id))
791
+ .forEach((t, i) => {
792
+ ordinals.set(t, i + 1);
793
+ });
794
+ }
795
+ for (const t of tabList) {
796
+ const { text } = baseLabel(t);
797
+ const k = ordinals.get(t);
798
+ const display = k !== undefined && k > 1 ? `${text} (${String(k)})` : text;
767
799
  t.display = display;
768
800
  t.label.textContent = display;
769
801
  // The accessible name carries the state and the percentage as well as
@@ -31,7 +31,7 @@ import { statusPhrase, statusRevealsDot } from "./model.js";
31
31
  * pointer announces itself; this only filters the coincidence where one event of a
32
32
  * sweep happens to land within REORDER_MOVE_EPS_PX of the previous one (a direction
33
33
  * reversal, or a frame where the motion was almost entirely vertical). */
34
- export const REORDER_STILL_MS = 90;
34
+ export const REORDER_STILL_MS = 50;
35
35
 
36
36
  /** Fallback guard (ms): commit the pending slot this long after the last MOVEMENT, for
37
37
  * the case where `dragover` stops arriving altogether and no stationary event ever