@cplieger/web-terminal-ui 5.4.3 → 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.3",
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",
@@ -233,11 +233,17 @@ async function createSessionHonouringRetry(
233
233
  // First refusal always speaks; after that only every
234
234
  // CREATE_RETRY_REANNOUNCE_MS, so a multi-minute wait is neither silent nor
235
235
  // a toast storm.
236
+ //
237
+ // The suffix says the page is WAITING, not recovering from a failure. It
238
+ // used to say "retrying", which read as an error loop when the server had
239
+ // only said "not yet" -- and the first announcement fires before any retry
240
+ // has happened at all, so a user two seconds into a boot was told
241
+ // "retrying" about a request that had been made exactly once.
236
242
  if (lastAnnouncedAt === 0 || elapsed - lastAnnouncedAt >= CREATE_RETRY_REANNOUNCE_MS) {
237
243
  lastAnnouncedAt = elapsed;
238
- const waiting = err.serverMessage ?? "Server is not ready yet";
239
- ctx.toast(`${waiting}; retrying`, 8000);
240
- ctx.announce(`${waiting}; retrying`);
244
+ const reason = err.serverMessage ?? "Server is not ready yet";
245
+ ctx.toast(`${reason}; waiting…`, 8000);
246
+ ctx.announce(`${reason}; waiting…`);
241
247
  }
242
248
  // Separate from the throttled pair above, and unthrottled: this writes the
243
249
  // reason onto the loading OVERLAY, which is the only surface a user can
@@ -246,7 +252,7 @@ async function createSessionHonouringRetry(
246
252
  // stacking notifications, so there is no storm to throttle, and repeating
247
253
  // the same string is idempotent. This is what turns the black screen of a
248
254
  // twenty-minute tools install into a screen that says why it is waiting.
249
- ctx.loadingReason(`${err.serverMessage ?? "Server is not ready yet"} — retrying…`);
255
+ ctx.loadingReason(`${err.serverMessage ?? "Server is not ready yet"}; waiting…`);
250
256
  await new Promise((resolve) => {
251
257
  window.setTimeout(resolve, err.retryAfterMs ?? CREATE_RETRY_FALLBACK_MS);
252
258
  });
@@ -738,26 +744,58 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
738
744
  // name, `list()` reports the real name, and clearing the progress needs no
739
745
  // cleanup — the next paint simply stops adding it.
740
746
  function relabelAll(): void {
741
- // Count only real (non-fallback) labels, so multiple untitled tabs all
742
- // read "New tab" without a numeric suffix.
743
- 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[]>();
744
770
  for (const t of tabList) {
745
771
  const { text, fallback } = baseLabel(t);
746
- if (!fallback) {
747
- 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]);
748
782
  }
749
783
  }
750
- const seen = new Map<string, number>();
751
- for (const t of tabList) {
752
- const { text, fallback } = baseLabel(t);
753
- let display = text;
754
- if (!fallback && (counts.get(text) ?? 0) > 1) {
755
- const k = (seen.get(text) ?? 0) + 1;
756
- seen.set(text, k);
757
- if (k > 1) {
758
- display = `${text} (${String(k)})`;
759
- }
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
760
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;
761
799
  t.display = display;
762
800
  t.label.textContent = display;
763
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