@cplieger/web-terminal-ui 6.1.2 → 6.1.4
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 +42 -42
- package/css/30-tabs.css +2 -2
- package/package.json +3 -3
- package/src/features/tabs/index.ts +42 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ terminal:
|
|
|
28
28
|
- a **context menu** (Copy / Select All / Paste), on a right-click and on a
|
|
29
29
|
touch long-press. Paste is the reason it exists: the keyboard target is a
|
|
30
30
|
hidden `<textarea>`, so no platform can offer a native paste over the output.
|
|
31
|
-
On touch the platform keeps its long-press
|
|
31
|
+
On touch the platform keeps its long-press: word selection and the OS copy
|
|
32
32
|
callout run untouched, and the menu appears on release only when the press
|
|
33
33
|
selected nothing
|
|
34
34
|
- **predictive local echo**, **IME / composition** (CJK, dictation,
|
|
@@ -109,7 +109,7 @@ entry modules beside it:
|
|
|
109
109
|
plus every feature's chrome) inside the target element itself. `target` is a CSS
|
|
110
110
|
selector or an element: pass a **selector** from a page (`"#terminal"`) and pass
|
|
111
111
|
an **element** only when you already hold one you created yourself. The
|
|
112
|
-
difference matters
|
|
112
|
+
difference matters; see "Startup failures" below. There is no element-id
|
|
113
113
|
contract for the host page to reproduce, and every style and CSS custom
|
|
114
114
|
property is scoped to the `wt-root` class it stamps on your element (removed
|
|
115
115
|
again by `destroy()`). Call it exactly once; the engine's
|
|
@@ -164,18 +164,18 @@ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
|
|
|
164
164
|
|
|
165
165
|
### Options
|
|
166
166
|
|
|
167
|
-
| Option | Default | Purpose
|
|
168
|
-
| ------------------- | ---------------------------- |
|
|
169
|
-
| `features` | _(none; bare kernel)_ | A FUNCTION returning the feature list. Omitted builds only the terminal (no chrome). Pass a preset by name (`features: presetTabbed`) or a factory of your own; it is called inside the startup-failure boundary, so a preset that throws is handled rather than escaping.
|
|
170
|
-
| `layout` | `"viewport"` | How the terminal claims space. `"viewport"`: the root becomes a fixed full-viewport box (the full-page product). `"container"`: the root fills your container element, which becomes the styling and positioning boundary (the embedded case).
|
|
171
|
-
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to.
|
|
172
|
-
| `fontReady` | `'14px "Monaspace Neon NF"'` | CSS font shorthand awaited before the first resize, so the server is sized against the real web font's cell metrics rather than a fallback.
|
|
173
|
-
| `scrollbackLines` | _(engine default)_ | Retained scrollback lines per terminal
|
|
174
|
-
| `persistScrollback` | _(off)_ | Persist each session's scrollback across a page discard, through storage YOU supply. A page that is discarded and reloaded otherwise resumes holding nothing, so it asks the server for everything and refills its whole buffer over the wire
|
|
175
|
-
| `loading` | _(none)_ | A pre-JS loading overlay element (kept in your served HTML so it paints before this module loads); it is faded out and removed once the first frame renders.
|
|
176
|
-
| `onFatalError` | _(built-in recovery)_ | Called with a `TerminalStartupFailure` after a fatal startup failure, in either phase (`feature-setup` or `kernel-init`); behavior below.
|
|
177
|
-
| `onSessionEnded` | _(none)_ | Called when the active session's process has ended and nothing is retrying
|
|
178
|
-
| `theme` | _(none)_ | Theme overrides (CSS custom properties on the terminal root): `--accent`, `--tab-bg`, `--tab-hover-bg`, `--tab-active-bg`, `--tab-active-fg`, `--tab-active-border`, plus the activity-dot palette `--status-working`, `--status-done`, `--status-input`, `--status-warning`, `--status-failed`. The library ships neutral defaults. If you retheme the animated trio (`--status-working` / `--status-warning` / `--status-failed`), keep their LIGHTNESS spread: they differ only in hue, so equal-lightness replacements collapse into one another in greyscale and under deuteranopia.
|
|
167
|
+
| Option | Default | Purpose |
|
|
168
|
+
| ------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
169
|
+
| `features` | _(none; bare kernel)_ | A FUNCTION returning the feature list. Omitted builds only the terminal (no chrome). Pass a preset by name (`features: presetTabbed`) or a factory of your own; it is called inside the startup-failure boundary, so a preset that throws is handled rather than escaping. |
|
|
170
|
+
| `layout` | `"viewport"` | How the terminal claims space. `"viewport"`: the root becomes a fixed full-viewport box (the full-page product). `"container"`: the root fills your container element, which becomes the styling and positioning boundary (the embedded case). |
|
|
171
|
+
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
|
|
172
|
+
| `fontReady` | `'14px "Monaspace Neon NF"'` | CSS font shorthand awaited before the first resize, so the server is sized against the real web font's cell metrics rather than a fallback. |
|
|
173
|
+
| `scrollbackLines` | _(engine default)_ | Retained scrollback lines per terminal, and per tab under the tabs feature (every per-tab cache is built through the same budget). This is the page's dominant memory dial: it bounds the styled-run arrays each session retains AND the DOM rows the renderer keeps (one row element per retained line), so a memory-constrained host (iOS Safari, where the content process is reclaimed under system pressure) passes a smaller budget at the price of a shorter scroll-back. A history budget floored at the live screen: the engine never evicts the current window, so a value at or below the terminal height keeps the full screen with no scrollback, but choose a value comfortably above the largest expected terminal height (a few multiples), since near or below it the batched eviction degrades to per-line churn. Non-integer or non-positive values are ignored. Left unset, the engine decides, and it decides twice: 5000 against a server that cannot serve history back, dropping to a 1500-line resident tail plus an on-demand cache once a server declares demand-paged scrollback. Prefer leaving it unset: the depth then lives on the server and the phone holds a working set. An explicit value opts OUT of that flip: it is an explicit memory decision and holds in both states. |
|
|
174
|
+
| `persistScrollback` | _(off)_ | Persist each session's scrollback across a page discard, through storage YOU supply. A page that is discarded and reloaded otherwise resumes holding nothing, so it asks the server for everything and refills its whole buffer over the wire (the normal case on iOS, where Safari evicts backgrounded tabs under memory pressure and returning to one re-runs the page). With a snapshot restored, the resume asks only for what was printed while the tab was gone. It helps the FRESH-LOAD case only: a warm reconnect and an in-page tab switch already replay nothing. Off by default because `localStorage` is a shared, origin-wide, quota-limited resource and this library is embedded in applications that keep their own state there: an application decides durability for its own users, a library does not decide it for an embedder. Applies to a single terminal and to every tab alike. See below. |
|
|
175
|
+
| `loading` | _(none)_ | A pre-JS loading overlay element (kept in your served HTML so it paints before this module loads); it is faded out and removed once the first frame renders. |
|
|
176
|
+
| `onFatalError` | _(built-in recovery)_ | Called with a `TerminalStartupFailure` after a fatal startup failure, in either phase (`feature-setup` or `kernel-init`); behavior below. |
|
|
177
|
+
| `onSessionEnded` | _(none)_ | Called when the active session's process has ended and nothing is retrying: the fact the banner renders as "Session ended". Wire it only if you can act on it: the recovery move is `handle.reattach()`, and whether that helps depends on what your endpoint does on the next connect (see "When a session ends"). Observation only; everything the kernel does about the end happens first and happens regardless, and a handler that throws is logged rather than allowed to take the banner down with it. |
|
|
178
|
+
| `theme` | _(none)_ | Theme overrides (CSS custom properties on the terminal root): `--accent`, `--tab-bg`, `--tab-hover-bg`, `--tab-active-bg`, `--tab-active-fg`, `--tab-active-border`, plus the activity-dot palette `--status-working`, `--status-done`, `--status-input`, `--status-warning`, `--status-failed`. The library ships neutral defaults. If you retheme the animated trio (`--status-working` / `--status-warning` / `--status-failed`), keep their LIGHTNESS spread: they differ only in hue, so equal-lightness replacements collapse into one another in greyscale and under deuteranopia. |
|
|
179
179
|
|
|
180
180
|
`createTerminal()` returns a handle: `focus()` re-focuses the terminal input
|
|
181
181
|
(and opens the soft keyboard on touch); `send(bytes)` sends bytes to the active
|
|
@@ -191,15 +191,15 @@ serves now, for a session that has ended (see "When a session ends"); and
|
|
|
191
191
|
`persistScrollback` takes the storage, not a switch, because storage is a
|
|
192
192
|
decision this library should not make for you. Chrome's page-lifecycle guidance
|
|
193
193
|
is to close IndexedDB connections on freeze, since a held connection costs
|
|
194
|
-
bfcache eligibility
|
|
194
|
+
bfcache eligibility, and this kernel depends on bfcache, so it must not own a
|
|
195
195
|
database. `localStorage` is also a shared, origin-wide, ~5 MB resource, and this
|
|
196
196
|
package is embedded in applications that keep their own state there: consuming it
|
|
197
197
|
uninvited would risk the host's writes failing, and how that degrades is theirs to
|
|
198
|
-
design. Ours degrades gracefully
|
|
198
|
+
design. Ours degrades gracefully: nothing restored, terminal unaffected.
|
|
199
199
|
|
|
200
200
|
So the option is off until you pass storage, and `localScrollbackStorage()` is the
|
|
201
|
-
ready-made answer for consumers who want the ordinary one
|
|
202
|
-
handles the quota sweep, the age bound and the `localStorage`-unavailable case:
|
|
201
|
+
ready-made answer for consumers who want the ordinary one (one import, and it
|
|
202
|
+
handles the quota sweep, the age bound and the `localStorage`-unavailable case):
|
|
203
203
|
|
|
204
204
|
```ts
|
|
205
205
|
import { createTerminal, localScrollbackStorage, presetTabbed } from "@cplieger/web-terminal-ui";
|
|
@@ -214,7 +214,7 @@ All three reference apps (web-terminal-kiro, web-terminal-server, vibekit's shel
|
|
|
214
214
|
panel) enable it that way; web-terminal-server additionally exposes an operator
|
|
215
215
|
opt-out, because it runs a command its operator chooses.
|
|
216
216
|
|
|
217
|
-
Supply your own storage instead when the snapshots belong somewhere else
|
|
217
|
+
Supply your own storage instead when the snapshots belong somewhere else: a shared
|
|
218
218
|
IndexedDB, a server round-trip, an in-memory cache for a test. The rest of this
|
|
219
219
|
section is that case:
|
|
220
220
|
|
|
@@ -245,11 +245,11 @@ createTerminal("#terminal", {
|
|
|
245
245
|
than working around: a restore has to be in place before the resume announces
|
|
246
246
|
what the client already holds, and a resume cannot be taken back. A store
|
|
247
247
|
hydrated after that has already lost the argument. So an asynchronous store is
|
|
248
|
-
read into memory first
|
|
248
|
+
read into memory first, which is also the shape that holds no database
|
|
249
249
|
connection open.
|
|
250
250
|
|
|
251
251
|
`sessionId` is a real session id, never a key of your invention: the id the tabs
|
|
252
|
-
feature uses, or
|
|
252
|
+
feature uses, or (for a single terminal) the engine's own per-tab id. That is
|
|
253
253
|
load-bearing rather than tidy, because the same id is what the persisted server
|
|
254
254
|
boot epoch is checked against. Prefix it however you like inside your storage.
|
|
255
255
|
|
|
@@ -257,7 +257,7 @@ Knobs, all optional. On the option itself: `lines` (default 200) bounds how much
|
|
|
257
257
|
each session's tail is written, `maxAgeMs` (default 7 days) bounds how long an entry
|
|
258
258
|
may be used, and `saveIntervalMs` (default 10s) sets the background save cadence.
|
|
259
259
|
`localScrollbackStorage` adds `prefix` and `maxBytes` (default 512 KiB of
|
|
260
|
-
characters, roughly 1 MiB of quota since `localStorage` is accounted in UTF-16
|
|
260
|
+
characters, roughly 1 MiB of quota since `localStorage` is accounted in UTF-16;
|
|
261
261
|
about a fifth of the ~5 MiB floor, leaving the rest to the host application).
|
|
262
262
|
|
|
263
263
|
The 200-line default is a measurement rather than a guess: VS Code's own
|
|
@@ -268,13 +268,13 @@ timer while output advances, so that cost is paid repeatedly on the device this
|
|
|
268
268
|
exists for.
|
|
269
269
|
|
|
270
270
|
The library writes on `visibilitychange` to hidden, on `pagehide`, on `destroy()`,
|
|
271
|
-
and on that timer
|
|
271
|
+
and on that timer; in every case only for a session whose content has advanced
|
|
272
272
|
since its last recorded save. That predicate is not an optimisation: two pages of
|
|
273
273
|
the same app hold the same session ids, and an unconditional write let a background
|
|
274
274
|
page roll a foreground page's newer entry back. `pagehide` is documented as not
|
|
275
275
|
guaranteed, which is why the timer exists.
|
|
276
276
|
|
|
277
|
-
Every callback may throw or return nonsense without consequence
|
|
277
|
+
Every callback may throw or return nonsense without consequence: a failure
|
|
278
278
|
degrades to "nothing was restored", exactly as if the option were absent. An
|
|
279
279
|
entry is also discarded, and dropped, when it is too old, unreadable, or from a
|
|
280
280
|
different server process: absolute line indices only mean anything within one
|
|
@@ -284,7 +284,7 @@ terminal that is confidently wrong.
|
|
|
284
284
|
|
|
285
285
|
No permission prompt is involved: `localStorage` is not in the Permissions API, so
|
|
286
286
|
there is nothing for a user to grant or refuse. (`navigator.storage.persist()` is
|
|
287
|
-
the call that can prompt, and this library never makes it
|
|
287
|
+
the call that can prompt, and this library never makes it; these snapshots are
|
|
288
288
|
disposable by design, so asking a user to exempt them from eviction would be asking
|
|
289
289
|
for the wrong thing.) Storage can still be _unavailable_ with no prompt either way,
|
|
290
290
|
and the failures differ: a browser set to block site data makes
|
|
@@ -300,28 +300,28 @@ happen.
|
|
|
300
300
|
|
|
301
301
|
Cleanup, since a store that only ever writes fills up. Four mechanisms, and the
|
|
302
302
|
first is the one that matters day to day: the kernel calls `drop` when a session
|
|
303
|
-
closes, so a tab the user closes
|
|
303
|
+
closes, so a tab the user closes (or one the server reaps while the page is open)
|
|
304
304
|
takes its entry with it. On top of that, an entry past `maxAgeMs` is deleted when it
|
|
305
305
|
is next read; `localScrollbackStorage` sweeps both at construction and after every
|
|
306
306
|
write (expired entries first, then the oldest until the byte budget fits); and a
|
|
307
307
|
rejected entry is always dropped rather than left to be re-read. What no mechanism
|
|
308
|
-
can catch promptly is a session that disappeared while the page was CLOSED
|
|
309
|
-
runs then, and there is no reliable "this tab is closing for good" event to hook
|
|
308
|
+
can catch promptly is a session that disappeared while the page was CLOSED. Nothing
|
|
309
|
+
runs then, and there is no reliable "this tab is closing for good" event to hook, so
|
|
310
310
|
that case is the sweep's, which is why a budget exists rather than the age bound
|
|
311
311
|
alone.
|
|
312
312
|
|
|
313
313
|
The budget is bytes rather than an entry count deliberately, because a count bounds
|
|
314
314
|
the wrong thing. Measured on the real stored value at the 200-line default, a plain
|
|
315
315
|
80-column session is ~24 K characters and a wide coloured one ~112 K, so a 20-entry
|
|
316
|
-
allowance would have permitted over 4 MiB of a ~5 MiB quota
|
|
316
|
+
allowance would have permitted over 4 MiB of a ~5 MiB quota; the cap would have
|
|
317
317
|
guaranteed the refusal it was meant to prevent. A single snapshot larger than the
|
|
318
318
|
whole budget is refused outright rather than evicting everything for something that
|
|
319
319
|
still would not fit.
|
|
320
320
|
|
|
321
321
|
### Themes that provably apply
|
|
322
322
|
|
|
323
|
-
`theme` is an open `Record<string, string>`
|
|
324
|
-
terminal root verbatim
|
|
323
|
+
`theme` is an open `Record<string, string>` (the kernel sets every key on the
|
|
324
|
+
terminal root verbatim), so a key this library renamed or retired becomes a live
|
|
325
325
|
declaration nothing reads: no error, just the library's neutral defaults where
|
|
326
326
|
your brand should be. The supported keys are therefore published as data:
|
|
327
327
|
|
|
@@ -348,23 +348,23 @@ that markup usually lives in a static HTML file no compiler reads.
|
|
|
348
348
|
### Startup failures
|
|
349
349
|
|
|
350
350
|
You do not need your own startup-failure UI. Every way starting up can fail ends
|
|
351
|
-
at one recovery surface this package owns
|
|
352
|
-
with a Reload button
|
|
351
|
+
at one recovery surface this package owns (a "Terminal failed to start" panel
|
|
352
|
+
with a Reload button), and your loading overlay is lowered so that panel is
|
|
353
353
|
visible.
|
|
354
354
|
|
|
355
355
|
Two phases can fail. If a feature's setup throws or rejects
|
|
356
356
|
(`phase: "feature-setup"`), the kernel stops the connection, tears down every
|
|
357
357
|
completed feature and core listener, clears the broken subtree, and shows the
|
|
358
|
-
panel. If `createTerminal` itself throws (`phase: "kernel-init"
|
|
358
|
+
panel. If `createTerminal` itself throws (`phase: "kernel-init"`: an
|
|
359
359
|
unresolvable mount selector, a preset that throws, an invalid feature list, a DOM
|
|
360
360
|
invariant), it shows the same panel and then rethrows, so a caller with its own
|
|
361
361
|
error handling still sees the error. Either way the panel is modal when the
|
|
362
362
|
terminal owns the viewport and non-modal when it fills an embedded container.
|
|
363
363
|
|
|
364
364
|
**This is why `target` takes a selector and `features` takes a function.** Both
|
|
365
|
-
are resolved INSIDE that boundary. Written the other way round
|
|
366
|
-
`createTerminal(document.getElementById("terminal"), { features: presetTabbed() })`
|
|
367
|
-
|
|
365
|
+
are resolved INSIDE that boundary. Written the other way round
|
|
366
|
+
(`createTerminal(document.getElementById("terminal"), { features: presetTabbed() })`),
|
|
367
|
+
the lookup and the preset call both happen at your call site, before
|
|
368
368
|
`createTerminal` is entered, so a missing element or a throwing preset escapes
|
|
369
369
|
the library entirely and leaves the page spinning under your overlay with nothing
|
|
370
370
|
but a console error. Those were the two failures every consumer used to
|
|
@@ -382,8 +382,8 @@ feature composition never began. `surface` names the element the built-in panel
|
|
|
382
382
|
would fill, and is the element to render into if you claim it. Return `true` only
|
|
383
383
|
when you have rendered replacement recovery UI there.
|
|
384
384
|
|
|
385
|
-
If your page also carries an inline bootstrap watchdog
|
|
386
|
-
"the JS bundle never loaded at all", a rung below `import`
|
|
385
|
+
If your page also carries an inline bootstrap watchdog (a script that reports
|
|
386
|
+
"the JS bundle never loaded at all", a rung below `import`), it cannot import
|
|
387
387
|
anything by definition. Take its wording from `STARTUP_FAILURE_COPY` (exported at
|
|
388
388
|
the package root and at `@cplieger/web-terminal-ui/startup-copy`, which imports
|
|
389
389
|
nothing and touches no DOM so a build script can read it) and substitute the
|
|
@@ -398,8 +398,8 @@ endpoint, reconnecting could only collect the same close again, which is an
|
|
|
398
398
|
endless "Reconnecting…" flap over a screen that will never change.
|
|
399
399
|
|
|
400
400
|
Some hosts are the other shape. If your endpoint hands out a NEW session on the
|
|
401
|
-
next connect
|
|
402
|
-
that spawns on attach
|
|
401
|
+
next connect (one shared PTY the server replaces once it is spent, an endpoint
|
|
402
|
+
that spawns on attach), then the connection the engine declines to make is exactly
|
|
403
403
|
what would produce a working terminal, and you are the only party who knows that.
|
|
404
404
|
So the library gives you the fact and the move, and keeps the policy out of it:
|
|
405
405
|
|
|
@@ -427,7 +427,7 @@ drops history the server may have evicted since.
|
|
|
427
427
|
|
|
428
428
|
**Bound your own retries.** A shell that dies as fast as it is spawned will end
|
|
429
429
|
again the moment it is replaced, so a handler that reattaches unconditionally is a
|
|
430
|
-
hot loop. Count consecutive ends, back off, and stop after a few
|
|
430
|
+
hot loop. Count consecutive ends, back off, and stop after a few: the banner is
|
|
431
431
|
the honest outcome when a session cannot stay up. The library will not do this for
|
|
432
432
|
you, because how many attempts is worth making, and whether any are, is a fact
|
|
433
433
|
about your server rather than about this terminal.
|
package/css/30-tabs.css
CHANGED
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
invisible. It is NOT free: every backdrop-filter surface holds an offscreen
|
|
57
57
|
copy of what is behind it plus the blurred result, re-derived when that
|
|
58
58
|
content changes — a standing compositor allocation on exactly the
|
|
59
|
-
memory-constrained devices this UI targets
|
|
60
|
-
|
|
59
|
+
memory-constrained devices this UI targets. An EMBEDDED root (wt-container)
|
|
60
|
+
keeps the frosting: the
|
|
61
61
|
host's own page can sit behind the bar there, which is what the blur is
|
|
62
62
|
for. */
|
|
63
63
|
:where(.wt-root.wt-container) .wt-tab-bar {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.4",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"verify": "bash scripts/verify.sh"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@cplieger/web-terminal-engine": "^3.10.0 || ^4.0.0"
|
|
43
|
+
"@cplieger/web-terminal-engine": "^3.10.0 || ^4.0.0 || ^5.0.0"
|
|
44
44
|
},
|
|
45
45
|
"overrides": {
|
|
46
46
|
"qs": "^6.15.2"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"eslint": "10.8.1",
|
|
57
57
|
"fast-check": "4.9.0",
|
|
58
58
|
"happy-dom": "20.11.2",
|
|
59
|
-
"html-validate": "11.
|
|
59
|
+
"html-validate": "11.8.0",
|
|
60
60
|
"knip": "6.32.2",
|
|
61
61
|
"prettier": "3.9.6",
|
|
62
62
|
"stylelint": "17.14.1",
|
|
@@ -152,7 +152,13 @@ export interface TabsOptions {
|
|
|
152
152
|
*
|
|
153
153
|
* Not every platform honours it: Safari caches the first icon it fetched and
|
|
154
154
|
* ignores later changes, and an installed app has no tab icon at all. The
|
|
155
|
-
* title count is not gated on this, so those cases lose nothing.
|
|
155
|
+
* title count is not gated on this, so those cases lose nothing.
|
|
156
|
+
*
|
|
157
|
+
* The links are restored on `pagehide` as well as on teardown, because a
|
|
158
|
+
* browser remembers one icon per URL and renders it for the bookmark, the
|
|
159
|
+
* history row and the new-tab tile. Best-effort there, since the write races
|
|
160
|
+
* the unload. Deliberately not on `freeze`: a frozen background tab is still in
|
|
161
|
+
* the strip rendering its icon, which is the case this exists for. */
|
|
156
162
|
attentionIcons?: boolean;
|
|
157
163
|
}
|
|
158
164
|
|
|
@@ -601,6 +607,39 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
601
607
|
}
|
|
602
608
|
document.addEventListener("visibilitychange", onPageVisible);
|
|
603
609
|
|
|
610
|
+
// onPageGone hands the page's own icon back before the page GOES AWAY,
|
|
611
|
+
// which is a different question from the page being hidden and needs a
|
|
612
|
+
// different event. A browser remembers ONE icon per URL and renders it for
|
|
613
|
+
// the bookmark, the history row and the new-tab tile, so a tab closed while
|
|
614
|
+
// a cue was lit leaves a status variant standing in for the app until the
|
|
615
|
+
// page is next loaded.
|
|
616
|
+
//
|
|
617
|
+
// `pagehide`, and deliberately NOT `freeze`. freeze fires for a background
|
|
618
|
+
// tab the browser is conserving resources on (Android after five minutes in
|
|
619
|
+
// the background; desktop for a collapsed tab group and, since Chrome 133, a
|
|
620
|
+
// CPU-heavy tab under Energy Saver), and that tab is STILL in the strip
|
|
621
|
+
// rendering its icon — restoring there would blank the cue in exactly the
|
|
622
|
+
// case the cue exists for. pagehide fires on unload, tab close and bfcache
|
|
623
|
+
// entry, where no strip entry is left to render.
|
|
624
|
+
//
|
|
625
|
+
// Through apply() rather than the icon sink directly, because the sinks are
|
|
626
|
+
// change-gated on the last applied value: writing behind that memo would
|
|
627
|
+
// leave it believing the variant is still up, and a page restored from the
|
|
628
|
+
// bfcache would then never repaint it. Which is what onPageBack is for — a
|
|
629
|
+
// bfcache restore re-runs the fold, so the cue returns for a page that did
|
|
630
|
+
// not actually go away. Best-effort on a real unload, where the icon write
|
|
631
|
+
// races the teardown, and a no-op when nothing was lit.
|
|
632
|
+
function onPageGone(): void {
|
|
633
|
+
attention.apply(NO_ATTENTION);
|
|
634
|
+
}
|
|
635
|
+
function onPageBack(event: PageTransitionEvent): void {
|
|
636
|
+
if (event.persisted) {
|
|
637
|
+
paintAttention();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
window.addEventListener("pagehide", onPageGone);
|
|
641
|
+
window.addEventListener("pageshow", onPageBack);
|
|
642
|
+
|
|
604
643
|
// paintAttention re-derives the whole attention state from the tab list and
|
|
605
644
|
// this viewer's acknowledgements, and hands it to the surfaces.
|
|
606
645
|
//
|
|
@@ -4115,6 +4154,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
4115
4154
|
offMenuKey();
|
|
4116
4155
|
document.removeEventListener("click", onDocClickMenu);
|
|
4117
4156
|
document.removeEventListener("visibilitychange", onPageVisible);
|
|
4157
|
+
window.removeEventListener("pagehide", onPageGone);
|
|
4158
|
+
window.removeEventListener("pageshow", onPageBack);
|
|
4118
4159
|
document.removeEventListener("contextmenu", onDocContextMenu);
|
|
4119
4160
|
document.removeEventListener("pointerup", onDocTapDismiss, true);
|
|
4120
4161
|
document.removeEventListener("dragover", onDocTabDrop);
|