@cplieger/web-terminal-ui 7.0.0 → 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 +2 -2
- package/src/features/context-menu.ts +0 -3
- package/src/features/tabs/index.ts +31 -11
- package/src/kernel/kernel.ts +27 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "7.0.
|
|
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.
|
|
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",
|
|
@@ -188,9 +188,6 @@ export function contextMenu(opts: ContextMenuOptions = {}): TerminalFeature {
|
|
|
188
188
|
clipboard.paste();
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
|
-
if (menu.childElementCount === 0) {
|
|
192
|
-
return; // nothing to offer
|
|
193
|
-
}
|
|
194
191
|
// Make visible (so it has measurable size), then place it within the
|
|
195
192
|
// visible viewport via the shared point-anchored positioner (clamp to the
|
|
196
193
|
// visual viewport; flip above the finger near the bottom edge).
|
|
@@ -244,7 +244,12 @@ async function createSessionHonouringRetry(
|
|
|
244
244
|
isTornDown: () => boolean,
|
|
245
245
|
): Promise<SessionInfo> {
|
|
246
246
|
const startedAt = Date.now();
|
|
247
|
-
|
|
247
|
+
// The elapsed reading of the last announcement, or null when nothing has been
|
|
248
|
+
// announced yet. NOT a 0 sentinel: elapsed is legitimately 0 when the server
|
|
249
|
+
// refuses inside the first millisecond (a host on the same machine), and a 0
|
|
250
|
+
// sentinel read that as "never spoken" and re-announced on the next refusal —
|
|
251
|
+
// the one thing the throttle below exists to prevent.
|
|
252
|
+
let lastAnnouncedAt: number | null = null;
|
|
248
253
|
for (;;) {
|
|
249
254
|
try {
|
|
250
255
|
return await api.create();
|
|
@@ -266,7 +271,7 @@ async function createSessionHonouringRetry(
|
|
|
266
271
|
// only said "not yet" -- and the first announcement fires before any retry
|
|
267
272
|
// has happened at all, so a user two seconds into a boot was told
|
|
268
273
|
// "retrying" about a request that had been made exactly once.
|
|
269
|
-
if (lastAnnouncedAt ===
|
|
274
|
+
if (lastAnnouncedAt === null || elapsed - lastAnnouncedAt >= CREATE_RETRY_REANNOUNCE_MS) {
|
|
270
275
|
lastAnnouncedAt = elapsed;
|
|
271
276
|
const reason = err.serverMessage ?? "Server is not ready yet";
|
|
272
277
|
ctx.toast(`${reason}; waiting…`, 8000);
|
|
@@ -2868,13 +2873,20 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2868
2873
|
// still be), to the terminal otherwise. Recorded at entry because the exit
|
|
2869
2874
|
// paths cannot tell them apart.
|
|
2870
2875
|
let editFrom: "keyboard" | "pointer" = "pointer";
|
|
2871
|
-
// Tab ids with
|
|
2872
|
-
// record carrying a DIFFERENT value is applied for display
|
|
2873
|
-
// supersede the request: SSE delivery order and REST mutation
|
|
2874
|
-
// one total order, so a record sampled BEFORE our PUT can
|
|
2875
|
-
// and treating it as newer authority would suppress the
|
|
2876
|
-
// rollback and its failure toast.
|
|
2877
|
-
|
|
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>();
|
|
2878
2890
|
|
|
2879
2891
|
/** restoreFocusAfterEdit sends focus where the entry path implies. The
|
|
2880
2892
|
* terminal branch is gated exactly like switchTo's focus-on-switch: on a
|
|
@@ -2944,8 +2956,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2944
2956
|
const { text, fallback } = baseLabel(t);
|
|
2945
2957
|
ctx.announce(fallback ? "Custom name removed" : `Using automatic name: ${text}`);
|
|
2946
2958
|
}
|
|
2947
|
-
namesInFlight.add(id);
|
|
2948
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);
|
|
2949
2964
|
void request
|
|
2950
2965
|
.catch(() => {
|
|
2951
2966
|
const cur = tabList.find((x) => x.id === id);
|
|
@@ -2957,7 +2972,12 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2957
2972
|
ctx.toast("Couldn't save the terminal name");
|
|
2958
2973
|
})
|
|
2959
2974
|
.finally(() => {
|
|
2960
|
-
namesInFlight.
|
|
2975
|
+
const left = (namesInFlight.get(id) ?? 0) - 1;
|
|
2976
|
+
if (left > 0) {
|
|
2977
|
+
namesInFlight.set(id, left);
|
|
2978
|
+
} else {
|
|
2979
|
+
namesInFlight.delete(id);
|
|
2980
|
+
}
|
|
2961
2981
|
});
|
|
2962
2982
|
return true;
|
|
2963
2983
|
}
|
package/src/kernel/kernel.ts
CHANGED
|
@@ -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 =
|
|
284
|
-
|
|
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
|
//
|
|
@@ -790,29 +794,29 @@ function buildTerminal(
|
|
|
790
794
|
);
|
|
791
795
|
|
|
792
796
|
/**
|
|
793
|
-
* Applies
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
* coming
|
|
797
|
+
* Applies a `mapKeyboardEvent` result. BOTH keydown paths — the focused textarea
|
|
798
|
+
* below, and the document-level type-to-focus fallback in the focus-strategy
|
|
799
|
+
* section — handle every arm identically, including "ignore": the textarea lets
|
|
800
|
+
* its own `input` event deliver the character, and the fallback has already
|
|
801
|
+
* resolved that case before it gets here, computing the typed character itself
|
|
802
|
+
* because no `input` event is coming for a key that targeted the body.
|
|
799
803
|
*
|
|
800
|
-
* Switching over the whole `KeyboardResult` union in one place is what keeps
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
+
* Switching over the whole `KeyboardResult` union in one place is what keeps the
|
|
805
|
+
* two paths from drifting — handling only "send" in the fallback silently dropped
|
|
806
|
+
* Shift+PageUp, which is exactly what a reader who just selected some scrollback
|
|
807
|
+
* presses next.
|
|
804
808
|
*/
|
|
805
|
-
function applyMappedKey(ev: KeyboardEvent, result: keyboard.KeyboardResult):
|
|
809
|
+
function applyMappedKey(ev: KeyboardEvent, result: keyboard.KeyboardResult): void {
|
|
806
810
|
switch (result.kind) {
|
|
807
811
|
case "send":
|
|
808
812
|
ev.preventDefault();
|
|
809
813
|
sendText(result.bytes);
|
|
810
|
-
return
|
|
814
|
+
return;
|
|
811
815
|
case "scroll-up": {
|
|
812
816
|
ev.preventDefault();
|
|
813
817
|
const h = termWrap.clientHeight;
|
|
814
818
|
termWrap.scrollTop = Math.max(0, termWrap.scrollTop - h);
|
|
815
|
-
return
|
|
819
|
+
return;
|
|
816
820
|
}
|
|
817
821
|
case "scroll-down": {
|
|
818
822
|
ev.preventDefault();
|
|
@@ -823,10 +827,10 @@ function buildTerminal(
|
|
|
823
827
|
// reasoning as the engine's bottom writes.
|
|
824
828
|
const max = Math.max(0, termWrap.scrollHeight - h);
|
|
825
829
|
termWrap.scrollTop = Math.min(max, termWrap.scrollTop + h);
|
|
826
|
-
return
|
|
830
|
+
return;
|
|
827
831
|
}
|
|
828
832
|
case "ignore":
|
|
829
|
-
return
|
|
833
|
+
return;
|
|
830
834
|
}
|
|
831
835
|
}
|
|
832
836
|
|
|
@@ -842,9 +846,9 @@ function buildTerminal(
|
|
|
842
846
|
return;
|
|
843
847
|
}
|
|
844
848
|
}
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
849
|
+
// Nothing more is needed for an "ignore": the `input` listener above delivers
|
|
850
|
+
// the character, which is also how IME and dead-key composition output
|
|
851
|
+
// reaches the terminal.
|
|
848
852
|
applyMappedKey(ev, mapKeyboardEvent(ev, modes));
|
|
849
853
|
},
|
|
850
854
|
{ signal },
|