@cplieger/web-terminal-ui 7.2.2 → 7.3.1
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/README.md +26 -13
- package/css/00-tokens.css +32 -1
- package/css/02-terminal.css +3 -1
- package/css/10-primitives.css +1 -0
- package/css/23-toolbar.css +1 -0
- package/css/30-tabs.css +61 -13
- package/css/page.css +59 -1
- package/package.json +1 -1
- package/src/features/activity-monitor.ts +37 -10
- package/src/features/animations.ts +23 -10
- package/src/features/clipboard.ts +26 -4
- package/src/features/connection-banner.ts +18 -5
- package/src/features/context-menu.ts +68 -48
- package/src/features/mobile-toolbar.ts +24 -7
- package/src/features/predictive-echo.ts +25 -6
- package/src/features/scroll-to-bottom.ts +19 -5
- package/src/features/tabs/attention.ts +4 -4
- package/src/features/tabs/index.ts +55 -16
- package/src/features/tabs/model.ts +7 -6
- package/src/kernel/kernel.ts +57 -5
- package/src/kernel/scrollback-storage.ts +12 -0
- package/src/kernel/startup-copy.ts +18 -16
- package/src/kernel/style-contract.ts +35 -33
- package/src/presets/agent-tabbed.ts +10 -7
- package/src/presets/single.ts +9 -5
- package/src/presets/tabbed.ts +12 -9
- package/src/presets/touch.ts +8 -4
- package/src/presets.ts +23 -17
|
@@ -1,51 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
/**
|
|
2
|
+
* contextMenu feature: the Copy / Select All / Paste menu for the terminal
|
|
3
|
+
* surface, rendered into the overlay region. Copy and Paste
|
|
4
|
+
* need the clipboard feature (ctx.use), and Paste routes through the kernel's
|
|
5
|
+
* sanitizing funnel. Escape-to-close goes through the kernel keydown intercept;
|
|
6
|
+
* outside-click is a document gesture.
|
|
7
|
+
*
|
|
8
|
+
* WHY THIS MENU EXISTS
|
|
9
|
+
* The terminal output is real DOM text, so the browser already does the hard
|
|
10
|
+
* part: drag-select with a mouse, long-press word-select plus the OS copy callout
|
|
11
|
+
* on touch. What no platform can offer is PASTE — the keyboard target is a 1x1
|
|
12
|
+
* pointer-events:none textarea, so there is no editable surface under the pointer
|
|
13
|
+
* for a native paste item to attach to. This menu is the paste path. It carries
|
|
14
|
+
* Copy and Select All because a menu appearing where the platform's would have
|
|
15
|
+
* appeared should not be missing them.
|
|
16
|
+
*
|
|
17
|
+
* THE MODEL: one owner per gesture, and nothing decided mid-gesture.
|
|
18
|
+
*
|
|
19
|
+
* Mouse / pen — `contextmenu` is an already-classified request for a menu:
|
|
20
|
+
* preventDefault and open at the pointer. No timers, no heuristics, no
|
|
21
|
+
* platform branches.
|
|
22
|
+
*
|
|
23
|
+
* Touch — the platform owns the press while the finger is down. We run no hold
|
|
24
|
+
* timer against it, open nothing before it finishes, and never cancel its
|
|
25
|
+
* gesture on WebKit. We classify ONCE, on `touchend`, when every fact is
|
|
26
|
+
* settled: a single-finger, stationary press held past the tap ceiling
|
|
27
|
+
* (kernel/gesture.ts, shared with the kernel's tap-to-focus so the two cannot
|
|
28
|
+
* both claim one press) that selected nothing and did not start on a link is a
|
|
29
|
+
* press the platform declined — so it is ours, and it is the paste path. A
|
|
30
|
+
* press that produced a selection belongs to the OS callout; a press on a link
|
|
31
|
+
* belongs to the platform's link preview; a shorter press is the kernel's tap.
|
|
32
|
+
*
|
|
33
|
+
* WHY AT RELEASE RATHER THAN DURING THE HOLD
|
|
34
|
+
* Opening from a ~550ms hold timer put this feature in a race it cannot win: the
|
|
35
|
+
* timer has to beat the platform's own long-press threshold and then GUESS
|
|
36
|
+
* whether a selection is still coming (iOS 26 registers a word selection well
|
|
37
|
+
* after 550ms). Four mechanisms existed only to referee that guess — a hit test
|
|
38
|
+
* for glyphs under the finger, a selectionchange watch to retract a menu that
|
|
39
|
+
* opened too early, a device sniff, and per-platform contextmenu branching,
|
|
40
|
+
* because WebKit reads preventDefault on a touch contextmenu as "cancel every
|
|
41
|
+
* remaining default of this gesture", the not-yet-registered selection included.
|
|
42
|
+
* Deciding at release deletes all four: the outcome is observed, not predicted.
|
|
43
|
+
*
|
|
44
|
+
* It also fixes the symptom that prompted the rewrite. A touch long-press emits a
|
|
45
|
+
* trailing click on release, and the swallow window that covers that click was
|
|
46
|
+
* armed when the menu OPENED — ~550ms into the press, expiring 350ms later — so
|
|
47
|
+
* holding a beat longer meant the release click landed as an outside click and
|
|
48
|
+
* dismissed the menu the instant the finger lifted. Opened BY the release, the
|
|
49
|
+
* window can only ever start at the release edge.
|
|
50
|
+
*
|
|
51
|
+
* @module
|
|
52
|
+
*/
|
|
49
53
|
|
|
50
54
|
import type { TerminalFeature } from "../kernel/types.js";
|
|
51
55
|
import { TAP_MAX_MS, TAP_MOVEMENT_PX, isLinkTarget } from "../kernel/gesture.js";
|
|
@@ -56,6 +60,12 @@ import { createClickSwallow, placeMenuAt } from "./menu-position.js";
|
|
|
56
60
|
// swallow all live in the shared point-anchored menu module (menu-position.ts),
|
|
57
61
|
// shared with the tab menu.
|
|
58
62
|
|
|
63
|
+
/** Options for the contextMenu feature.
|
|
64
|
+
*
|
|
65
|
+
* Pass the SAME clipboard feature value the composition includes, not a second
|
|
66
|
+
* `clipboard()` call: `ctx.use` resolves a value to the instance the kernel set
|
|
67
|
+
* up, so a value that is not in the feature list resolves to nothing and the
|
|
68
|
+
* menu silently loses Copy and Paste. `presetSingle` shows the shape. */
|
|
59
69
|
export interface ContextMenuOptions {
|
|
60
70
|
/** The clipboard feature value, so the menu can offer Copy/Paste through its
|
|
61
71
|
* API (ctx.use). Omitted: the menu shows only Select All. */
|
|
@@ -109,6 +119,16 @@ function selectionText(): string {
|
|
|
109
119
|
* so the selection test alone would not keep us out of its way. */
|
|
110
120
|
const onLink = isLinkTarget;
|
|
111
121
|
|
|
122
|
+
/** Build the contextMenu feature. Exposes no API — the menu is opened by the
|
|
123
|
+
* gestures described above, never programmatically by a peer.
|
|
124
|
+
*
|
|
125
|
+
* Hand it the clipboard value the composition also includes (see
|
|
126
|
+
* ContextMenuOptions) or it degrades to a Select All-only menu; its own position in
|
|
127
|
+
* the feature list is otherwise free, since nothing reads it through `ctx.use`. Its
|
|
128
|
+
* items act on the browser selection inside the terminal surface and on the
|
|
129
|
+
* kernel's paste funnel, so it holds no session state and survives a tab switch
|
|
130
|
+
* unchanged. Teardown removes the menu element and releases the keydown intercept
|
|
131
|
+
* along with every surface and document listener. */
|
|
112
132
|
export function contextMenu(opts: ContextMenuOptions = {}): TerminalFeature {
|
|
113
133
|
return {
|
|
114
134
|
name: "contextMenu",
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* mobileToolbar feature: the on-screen key toolbar (Tab/Esc/arrows/Enter + a
|
|
3
|
+
* collapse toggle and sticky-Ctrl) in the thumb-zone region.
|
|
4
|
+
* The engine's toolbar.bindMobileToolbar wires the buttons (DECCKM-aware
|
|
5
|
+
* arrows, sticky-Ctrl state; engine >= v3, where the widget moved out of the
|
|
6
|
+
* keyboard module); this feature builds the toolbar chrome, routes its
|
|
7
|
+
* output through the kernel funnel, and registers sticky-Ctrl as an input
|
|
8
|
+
* transform so a typed character is rewritten to its Ctrl byte when armed.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
8
12
|
|
|
9
13
|
import { toolbar } from "@cplieger/web-terminal-engine";
|
|
10
14
|
import type { TerminalFeature } from "../kernel/types.js";
|
|
@@ -52,6 +56,19 @@ const TOOLBAR_HTML = `
|
|
|
52
56
|
<button type="button" id="kb-right" class="kb-key kb-r2c4" aria-label="Right"><svg viewBox="0 0 24 24"><polyline points="9 6 15 12 9 18"/></svg></button>
|
|
53
57
|
</div>`;
|
|
54
58
|
|
|
59
|
+
/** Build the mobileToolbar feature.
|
|
60
|
+
*
|
|
61
|
+
* The chrome is always built; whether it is VISIBLE is a CSS decision — the keys
|
|
62
|
+
* and the toggle are `display: none` and only a coarse pointer restores them
|
|
63
|
+
* (`css/23-toolbar.css`) — so a desktop consumer including this feature pays the
|
|
64
|
+
* DOM and sees nothing. Order it before `tabs` when `externalToggle` is set, since
|
|
65
|
+
* tabs reads the returned API through `ctx.use` to drive the grid from the tab
|
|
66
|
+
* bar's keyboard button.
|
|
67
|
+
*
|
|
68
|
+
* Sticky-Ctrl is registered as a kernel INPUT TRANSFORM rather than handled in
|
|
69
|
+
* the toolbar's own click path, so it rewrites a character typed on the soft
|
|
70
|
+
* keyboard as well as one produced by these buttons. Teardown removes the
|
|
71
|
+
* toolbar, drops the transform, and clears every subscriber. */
|
|
55
72
|
export function mobileToolbar(opts: MobileToolbarOptions = {}): TerminalFeature<MobileToolbarApi> {
|
|
56
73
|
return {
|
|
57
74
|
name: "mobileToolbar",
|
|
@@ -1,13 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* predictiveEcho feature: Mosh-style local echo. Wires the
|
|
3
|
+
* predict.ts mini-VT to the kernel input funnel: it advances a predicted cursor
|
|
4
|
+
* on accepted input (an input observer) and owns the col-0 backspace brake (an
|
|
5
|
+
* input transform that drops a lone DEL at the true origin, since the predicted
|
|
6
|
+
* cursor is the authoritative position for that decision). It re-pushes the
|
|
7
|
+
* predicted-cursor overlay on prediction changes and after each render flush.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
7
11
|
|
|
8
12
|
import * as predict from "../predict.js";
|
|
9
13
|
import type { TerminalFeature } from "../kernel/types.js";
|
|
10
14
|
|
|
15
|
+
/** Build the predictiveEcho feature. Exposes no API — its output is the engine
|
|
16
|
+
* renderer's predicted-cursor overlay.
|
|
17
|
+
*
|
|
18
|
+
* The prediction state is MODULE-scoped (`../predict.js`), not per instance, so two
|
|
19
|
+
* terminals in one page that both include this feature would drive one predicted
|
|
20
|
+
* cursor between them. That is within the kernel's existing "call createTerminal
|
|
21
|
+
* exactly once" contract, but it is the reason this feature cannot be made
|
|
22
|
+
* per-instance by composing it twice.
|
|
23
|
+
*
|
|
24
|
+
* Predictions are advisory: `predict` suspends itself on any byte it cannot
|
|
25
|
+
* model, because a wrong prediction is worse than a missing one. The feature
|
|
26
|
+
* resets it on a server restart and on a tab switch (`onDetach`), so the outgoing
|
|
27
|
+
* session's ghost cursor never paints over the incoming session's screen; the
|
|
28
|
+
* next server frame re-arms it. Teardown drops every listener, hides the overlay,
|
|
29
|
+
* and clears the prediction. */
|
|
11
30
|
export function predictiveEcho(): TerminalFeature {
|
|
12
31
|
return {
|
|
13
32
|
name: "predictiveEcho",
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* scrollToBottom feature: a scroll-to-bottom control in the thumb-zone region,
|
|
3
|
+
* shown only while the user has scrolled up. The
|
|
4
|
+
* scroll-state signal comes from the kernel's scroll:state event (the kernel
|
|
5
|
+
* owns scroll.init); this feature just renders the affordance and drives
|
|
6
|
+
* scroll.scrollToBottom.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
6
10
|
|
|
7
11
|
import type { TerminalFeature } from "../kernel/types.js";
|
|
8
12
|
import { fromHTML, holdFocusOnPress } from "./dom.js";
|
|
@@ -12,6 +16,16 @@ const BUTTON_HTML = `
|
|
|
12
16
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" d="M7 13l5 5 5-5M7 6l5 5 5-5"/></svg>
|
|
13
17
|
</button>`;
|
|
14
18
|
|
|
19
|
+
/** Build the scrollToBottom feature. Exposes no API — the button is the whole
|
|
20
|
+
* surface, and a peer that wants the same effect calls `ctx.scroll.scrollToBottom`
|
|
21
|
+
* directly.
|
|
22
|
+
*
|
|
23
|
+
* Its job is resuming FOLLOW, not just moving the viewport, which is why it also
|
|
24
|
+
* acts when the reader is already at the bottom but holding (an ED3 clamp leaves
|
|
25
|
+
* exactly that state). The jump is smooth unless the user asked for reduced
|
|
26
|
+
* motion, and it is idempotent, so binding both `pointerdown` (touch presses feel
|
|
27
|
+
* immediate) and `click` (keyboard activation) is safe. Teardown unsubscribes and
|
|
28
|
+
* removes the button. */
|
|
15
29
|
export function scrollToBottom(): TerminalFeature {
|
|
16
30
|
return {
|
|
17
31
|
name: "scrollToBottom",
|
|
@@ -151,9 +151,9 @@ export function createAttention(env: AttentionEnv): AttentionSurfaces {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
/** iconVariantHref rewrites an icon URL to its variant, by the convention
|
|
155
|
-
*
|
|
156
|
-
*
|
|
154
|
+
/** iconVariantHref rewrites an icon URL to its variant, by the naming convention
|
|
155
|
+
* the variant assets must follow (they are the consuming app's own files, served
|
|
156
|
+
* alongside its icons rather than shipped by this library): the `favicon` token
|
|
157
157
|
* of the filename gains `-<variant>`, so `/favicon.svg` becomes
|
|
158
158
|
* `/favicon-input.svg` and `/favicon-32x32.png` becomes
|
|
159
159
|
* `/favicon-input-32x32.png`. The extension is preserved, so each link keeps
|
|
@@ -161,7 +161,7 @@ export function createAttention(env: AttentionEnv): AttentionSurfaces {
|
|
|
161
161
|
*
|
|
162
162
|
* A URL whose filename does not start with `favicon` is returned unchanged,
|
|
163
163
|
* which leaves that link alone rather than pointing it at a 404. Exported for
|
|
164
|
-
* the test that pins this against
|
|
164
|
+
* the test that pins this against that naming convention. */
|
|
165
165
|
export function iconVariantHref(href: string, variant: string): string | null {
|
|
166
166
|
const match = /(^|\/)favicon(?=[-.])/.exec(href);
|
|
167
167
|
if (!match) {
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
/**
|
|
2
|
+
* tabs feature: multiple independent terminals over the one kernel.
|
|
3
|
+
* It owns the session set (GET/POST/DELETE
|
|
4
|
+
* /api/sessions), a per-tab LineStore switching cache, the reconnect-on-switch
|
|
5
|
+
* swap, and the tab chrome on both form factors: the desktop top-bar strip and
|
|
6
|
+
* the mobile bottom-switcher + modal overview sheet. The kernel drives one
|
|
7
|
+
* active session; switching re-points the renderer at the next tab's cached
|
|
8
|
+
* store (ctx.render.bind) and asks the kernel to reconnect the terminal WS to it
|
|
9
|
+
* (ctx.notifySwitch), so the last-known screen paints instantly and the
|
|
10
|
+
* background delta arrives after.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
10
14
|
|
|
11
15
|
import { modes } from "@cplieger/web-terminal-engine";
|
|
12
16
|
import type { SessionRef, TerminalContext, TerminalFeature } from "../../kernel/types.js";
|
|
@@ -102,6 +106,20 @@ const TAB_DRAG_TYPE = "application/x-web-terminal-tab";
|
|
|
102
106
|
// the shared point-anchored positioner (menu-position.ts), shared with the
|
|
103
107
|
// terminal context menu (formerly two hand-synced copies of the same math).
|
|
104
108
|
|
|
109
|
+
/** The value a peer feature or a host reads through `ctx.use(tabs(...))`: the
|
|
110
|
+
* session set as commands, plus a snapshot of the strip.
|
|
111
|
+
*
|
|
112
|
+
* `create` and `close` are server round trips, and NEITHER rejects: a refusal is
|
|
113
|
+
* reported to the user as a toast and the promise resolves anyway, so awaiting one
|
|
114
|
+
* tells you the attempt finished and not that it succeeded (read `list` for that).
|
|
115
|
+
* Both are safe to call more than once per gesture — `create` shares an in-flight
|
|
116
|
+
* create, so the duplicate activation an iPad trackpad delivers opens one terminal
|
|
117
|
+
* rather than two. `switchTo` is local and synchronous, and ignores an unknown id.
|
|
118
|
+
*
|
|
119
|
+
* `list` is a snapshot, not a live view — it does not update, so re-read it rather
|
|
120
|
+
* than holding onto the array, and treat it as display data: the tab a user is
|
|
121
|
+
* looking at is this feature's own state and mutating it goes through the members
|
|
122
|
+
* above. */
|
|
105
123
|
export interface TabsApi {
|
|
106
124
|
/** Spawn a fresh session and switch to it. Calls made while a create is in
|
|
107
125
|
* flight share that create, so one gesture opens exactly one terminal. */
|
|
@@ -114,6 +132,14 @@ export interface TabsApi {
|
|
|
114
132
|
list(): readonly { id: string; title: string; active: boolean }[];
|
|
115
133
|
}
|
|
116
134
|
|
|
135
|
+
/** Options for the tabs feature.
|
|
136
|
+
*
|
|
137
|
+
* The two feature-valued members (`activityMonitor`, `keyboardToggle`) must be the
|
|
138
|
+
* SAME values the composition includes and must be ordered before tabs in the
|
|
139
|
+
* feature list, since `ctx.use` only resolves a peer the kernel has already set
|
|
140
|
+
* up. Both are optional and their absence degrades rather than fails: no monitor
|
|
141
|
+
* means polling instead of the status SSE, no toolbar means no keyboard button in
|
|
142
|
+
* the mobile bar. `buildTabbed` in presets/tabbed.ts is the worked example. */
|
|
117
143
|
export interface TabsOptions {
|
|
118
144
|
/** REST base for the session API (default "/api/sessions"). */
|
|
119
145
|
apiBase?: string;
|
|
@@ -149,10 +175,10 @@ export interface TabsOptions {
|
|
|
149
175
|
* `link[rel=icon]` whose filename starts with `favicon`, three variants must
|
|
150
176
|
* be served alongside it with `-input`, `-done` and `-alert` inserted after
|
|
151
177
|
* that token (`/favicon.svg` needs `/favicon-input.svg`; `/favicon-32x32.png`
|
|
152
|
-
* needs `/favicon-input-32x32.png`). Generate them with
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
178
|
+
* needs `/favicon-input-32x32.png`). Generate them with whatever tooling you
|
|
179
|
+
* like as long as it writes exactly those names, and assert their presence in
|
|
180
|
+
* the APP's own tests — the library cannot check a file it does not ship, and
|
|
181
|
+
* a missing variant is a blank tab icon.
|
|
156
182
|
*
|
|
157
183
|
* Not every platform honours it: Safari caches the first icon it fetched and
|
|
158
184
|
* ignores later changes, and an installed app has no tab icon at all. The
|
|
@@ -321,6 +347,20 @@ function looksLikeHardwareKey(ev: KeyboardEvent): boolean {
|
|
|
321
347
|
}
|
|
322
348
|
}
|
|
323
349
|
|
|
350
|
+
/** Build the tabs feature.
|
|
351
|
+
*
|
|
352
|
+
* Requires a server that speaks the session API — `GET`/`POST`/`DELETE` on
|
|
353
|
+
* `/api/sessions`, `?session=<id>` on the WebSocket, and ideally the status SSE.
|
|
354
|
+
* It registers itself as the kernel's `sessionOwner`, so it and not the kernel
|
|
355
|
+
* resolves the session the first connect attaches to. At most one feature may claim
|
|
356
|
+
* that role, and `createTerminal` throws before any DOM work when two do.
|
|
357
|
+
*
|
|
358
|
+
* The arrangement of the strip is SERVER state (the `order` field), while the
|
|
359
|
+
* active tab and the dismissed notification cues are per-viewer and live in
|
|
360
|
+
* `localStorage` — so a phone reordering tabs moves them on the desktop, and
|
|
361
|
+
* switching tabs on one does not move the other. Teardown removes all chrome,
|
|
362
|
+
* releases the per-tab caches, and leaves the sessions themselves running on the
|
|
363
|
+
* server; closing a tab is what kills a process. */
|
|
324
364
|
export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
325
365
|
const apiBase = opts.apiBase ?? DEFAULT_API_BASE;
|
|
326
366
|
const presumeReports = opts.presumeReports ?? false;
|
|
@@ -3544,9 +3584,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3544
3584
|
}
|
|
3545
3585
|
|
|
3546
3586
|
// --- Event wiring ---
|
|
3547
|
-
// No input observer. The ENGINE
|
|
3548
|
-
//
|
|
3549
|
-
// does per-keystroke title work in the browser, and the name is identical for
|
|
3587
|
+
// No input observer. The ENGINE owns a session's name, so no preset does
|
|
3588
|
+
// per-keystroke title work in the browser, and the name is identical for
|
|
3550
3589
|
// every client attached to the session — including one that attaches later.
|
|
3551
3590
|
// Observe (never consume) keydowns to detect a physical keyboard: a
|
|
3552
3591
|
// hardware-only key latches sawHardwareKey, which upgrades focus-on-switch
|
|
@@ -309,7 +309,8 @@ export function worseCue(a: CueStatus | "", b: CueStatus | ""): CueStatus | "" {
|
|
|
309
309
|
|
|
310
310
|
/** The icon variant a cue paints, which is NOT one per status: `crashed` and
|
|
311
311
|
* `failed` both render --status-failed, so they share one asset. An app ships
|
|
312
|
-
* one icon per name returned here (see .
|
|
312
|
+
* one icon per name returned here (see TabsOptions.attentionIcons for the
|
|
313
|
+
* naming contract those files must satisfy). */
|
|
313
314
|
export function cueIconName(status: CueStatus): "input" | "done" | "alert" {
|
|
314
315
|
if (status === "input" || status === "done") {
|
|
315
316
|
return status;
|
|
@@ -532,8 +533,8 @@ export interface Tab {
|
|
|
532
533
|
* session (the boot double-create bug). */
|
|
533
534
|
born: number;
|
|
534
535
|
/** The title the SERVER resolved for this session: its pinned name, else the
|
|
535
|
-
*
|
|
536
|
-
*
|
|
536
|
+
* program's OSC 0/2 window title, else a title the host pushed through
|
|
537
|
+
* SetSessionTitle, else the engine's own foreground-process/cwd inference.
|
|
537
538
|
* Possibly empty only against an engine that has none of those. The displayed
|
|
538
539
|
* label adds a numbered fallback and de-duplication (see relabelAll in
|
|
539
540
|
* index.ts). */
|
|
@@ -603,9 +604,9 @@ export interface Tab {
|
|
|
603
604
|
* there is one, otherwise the title the SERVER resolved.
|
|
604
605
|
*
|
|
605
606
|
* Only two rungs, because the engine now owns every automatic source and folds
|
|
606
|
-
* them into `title` in precedence order (pinned,
|
|
607
|
-
*
|
|
608
|
-
* inference). A client that re-implemented that ladder could only disagree with
|
|
607
|
+
* them into `title` in precedence order (pinned, the program's OSC window title,
|
|
608
|
+
* a title the host pushed through SetSessionTitle, then its own
|
|
609
|
+
* foreground-process/cwd inference). A client that re-implemented that ladder could only disagree with
|
|
609
610
|
* the server and with every other client.
|
|
610
611
|
*
|
|
611
612
|
* The pin is still checked here, even though the server also folds it into
|
package/src/kernel/kernel.ts
CHANGED
|
@@ -54,7 +54,23 @@ import type {
|
|
|
54
54
|
const { mapKeyboardEvent, bracketTextForPaste, prepareTextForTerminal } = keyboard;
|
|
55
55
|
|
|
56
56
|
const DEFAULT_WS_PATH = "/ws";
|
|
57
|
+
// The family that carries the CELL METRICS, alone: deliberately not --font-mono's
|
|
58
|
+
// list, which leads with the tiling overlay. WebKit resolves load() over a list as
|
|
59
|
+
// soon as the FIRST family whose descriptor covers the sample has loaded
|
|
60
|
+
// (CSSFontFaceSet::matchingFacesExcludingPreinstalledFonts, `if (found) break;`),
|
|
61
|
+
// so naming the overlay would settle the gate on its 11 KB while the companion is
|
|
62
|
+
// still loading — and the overlay copies the companion's metrics and carries no
|
|
63
|
+
// "M", the glyph render.ts measures, so waiting on it defends no number anyway.
|
|
57
64
|
const DEFAULT_FONT_READY = '14px "Monaspace Neon NF"';
|
|
65
|
+
// The deadline on the WHOLE gate below, because neither document.fonts.load nor
|
|
66
|
+
// document.fonts.ready carries one: a response held open leaves both pending for
|
|
67
|
+
// as long as that request lives, and a gate that never settles reports no size at
|
|
68
|
+
// all. The budget is the block period the faces themselves buy with font-display:
|
|
69
|
+
// block (css/page.css declares it on every one; 3s is what CSS Fonts 4 recommends
|
|
70
|
+
// for `block`) — how long an engine withholds glyphs before it paints the
|
|
71
|
+
// fallback. Once it has, the cell on screen IS the fallback cell, so a longer wait
|
|
72
|
+
// defends metrics nobody is looking at.
|
|
73
|
+
const FONT_READY_TIMEOUT_MS = 3000;
|
|
58
74
|
|
|
59
75
|
const TOAST_MS = 3000;
|
|
60
76
|
// A touch that focuses the input (opens the soft keyboard) must be a genuine
|
|
@@ -1383,13 +1399,49 @@ function buildTerminal(
|
|
|
1383
1399
|
});
|
|
1384
1400
|
};
|
|
1385
1401
|
try {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1402
|
+
// false once the fonts have landed, true when the deadline opened the gate
|
|
1403
|
+
// without them. The race covers the whole wait, not just the rejection arm: a
|
|
1404
|
+
// request that stalls with nothing rejecting leaves load() ITSELF pending, so a
|
|
1405
|
+
// deadline armed on the rejection would bound one of the ways a font can hang.
|
|
1406
|
+
const landed: Promise<boolean> = document.fonts.load(fontReady).then(
|
|
1407
|
+
() => false,
|
|
1408
|
+
(err: unknown) => {
|
|
1390
1409
|
console.warn(`web-terminal-ui: web font ${fontReady} failed to load`, err);
|
|
1391
|
-
|
|
1410
|
+
// One load() over a comma-separated stack rejects as a UNIT, so settling
|
|
1411
|
+
// here would let an unavailable OPTIONAL family abandon the gate for the
|
|
1412
|
+
// REQUIRED one and announce a size measured on fallback metrics. `ready`
|
|
1413
|
+
// settles the initial load INCLUDING a failed one (the same property
|
|
1414
|
+
// features/tabs/ink-centre.ts relies on), so the families that ARE served
|
|
1415
|
+
// still land their bytes before the first resize is measured.
|
|
1416
|
+
return document.fonts.ready.then(() => false);
|
|
1417
|
+
},
|
|
1418
|
+
);
|
|
1419
|
+
const deadline = new Promise<boolean>((resolve) => {
|
|
1420
|
+
window.setTimeout(() => {
|
|
1421
|
+
resolve(true);
|
|
1422
|
+
}, FONT_READY_TIMEOUT_MS);
|
|
1423
|
+
});
|
|
1424
|
+
// Whichever settles first opens the gate exactly once; the loser is ignored.
|
|
1425
|
+
void Promise.race([landed, deadline]).then((timedOut) => {
|
|
1426
|
+
onFontSettled();
|
|
1427
|
+
if (!timedOut) {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
// The gate opened on fallback metrics, and the swap period is infinite: bytes
|
|
1431
|
+
// that land after this change the cell width with fontsLoaded already true,
|
|
1432
|
+
// and nothing re-measures until the next viewport transition. So one
|
|
1433
|
+
// corrective announce, through the path the viewport settle uses.
|
|
1434
|
+
// sendResize deduplicates, so a font that never lands costs nothing.
|
|
1435
|
+
void document.fonts.ready.then(() => {
|
|
1436
|
+
// The bytes can arrive arbitrarily late, so this is the one arm of the gate
|
|
1437
|
+
// that outlives its terminal; destroy() wins, as it does for a feature's
|
|
1438
|
+
// un-cancelled async.
|
|
1439
|
+
if (isDestroyed() || measurableSize() === null) {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
connection.sendResize();
|
|
1392
1443
|
});
|
|
1444
|
+
}, onFontSettled);
|
|
1393
1445
|
} catch (err) {
|
|
1394
1446
|
console.warn(`web-terminal-ui: invalid fontReady ${fontReady}`, err);
|
|
1395
1447
|
onFontSettled();
|
|
@@ -54,6 +54,18 @@ const DEFAULT_PREFIX = "wt.scrollback.";
|
|
|
54
54
|
*/
|
|
55
55
|
const DEFAULT_MAX_BYTES = 512 * 1024;
|
|
56
56
|
|
|
57
|
+
/** Options for `localScrollbackStorage`.
|
|
58
|
+
*
|
|
59
|
+
* Four of the five are the kernel's policy rather than this store's: `maxAgeMs`,
|
|
60
|
+
* `lines` and `saveIntervalMs` are passed straight through on the returned
|
|
61
|
+
* `ScrollbackPersistence`, so the store's own sweep and the kernel's load-time age
|
|
62
|
+
* check cannot disagree about a number a caller set once. `maxBytes` is this
|
|
63
|
+
* store's alone, since only it can enumerate what it owns.
|
|
64
|
+
*
|
|
65
|
+
* Every value is validated as a positive integer and an invalid one falls back to
|
|
66
|
+
* the default rather than being clamped. Sizing `maxBytes` is a decision ABOUT THE
|
|
67
|
+
* HOST APPLICATION, not about the terminal: this competes for the origin's
|
|
68
|
+
* `localStorage` quota with everything else the page stores. */
|
|
57
69
|
export interface LocalScrollbackStorageOptions {
|
|
58
70
|
/** Key prefix (default `"wt.scrollback."`). */
|
|
59
71
|
prefix?: string;
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The copy of the "terminal did not start" recovery surface, as DATA.
|
|
3
|
+
*
|
|
4
|
+
* Why this is a module of its own rather than string literals inside the
|
|
5
|
+
* renderer: a full-page consumer needs the same words in a place no module can
|
|
6
|
+
* reach. Its HTML carries an inline bootstrap watchdog whose entire job is to
|
|
7
|
+
* report "the JS bundle never ran" -- a rung below `import`, by definition, or
|
|
8
|
+
* it could not detect its own failure case. That watchdog cannot import these
|
|
9
|
+
* strings at runtime, so a consumer's BUILD substitutes them into its HTML
|
|
10
|
+
* instead, and the words survive as one declaration rather than three
|
|
11
|
+
* hand-agreed copies.
|
|
12
|
+
*
|
|
13
|
+
* This file therefore imports nothing and touches no DOM, so a Node build script
|
|
14
|
+
* can read it without loading the kernel. It is exported at the package root and
|
|
15
|
+
* at the "./startup-copy" subpath for exactly that use.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
17
19
|
|
|
18
20
|
/** The words the recovery surface shows, whichever startup rung failed.
|
|
19
21
|
*
|