@cplieger/web-terminal-ui 6.0.2 → 6.1.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 CHANGED
@@ -174,6 +174,7 @@ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
174
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
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
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. |
177
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. |
178
179
 
179
180
  `createTerminal()` returns a handle: `focus()` re-focuses the terminal input
@@ -181,8 +182,9 @@ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
181
182
  session through the kernel's sanitizing input funnel (the supported host path
182
183
  for a "type this command" affordance); `reset()` drops the local scrollback and
183
184
  screen without injecting keystrokes (send a redraw keystroke yourself if you
184
- want one, for example Ctrl+L); and `destroy()` tears every feature down and
185
- releases the kernel.
185
+ want one, for example Ctrl+L); `reattach()` attaches again to whatever the server
186
+ serves now, for a session that has ended (see "When a session ends"); and
187
+ `destroy()` tears every feature down and releases the kernel.
186
188
 
187
189
  ### Persisting scrollback across a discard
188
190
 
@@ -387,6 +389,49 @@ the package root and at `@cplieger/web-terminal-ui/startup-copy`, which imports
387
389
  nothing and touches no DOM so a build script can read it) and substitute the
388
390
  strings into your HTML at build time, rather than restating them by hand.
389
391
 
392
+ ### When a session ends
393
+
394
+ A session whose process exits is over: the engine closes with its definitive
395
+ process-exited code, the banner reads "Session ended", and no reconnect is
396
+ attempted. That refusal is deliberate. On a server that hands each session its own
397
+ endpoint, reconnecting could only collect the same close again, which is an
398
+ endless "Reconnecting…" flap over a screen that will never change.
399
+
400
+ Some hosts are the other shape. If your endpoint hands out a NEW session on the
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
+ what would produce a working terminal, and you are the only party who knows that.
404
+ So the library gives you the fact and the move, and keeps the policy out of it:
405
+
406
+ ```ts
407
+ const term = createTerminal("#terminal", {
408
+ features: presetTouch,
409
+ wsPath: "/api/shell/ws",
410
+ onSessionEnded: () => {
411
+ // Your endpoint's own restart call goes here if it needs one, then:
412
+ term.reattach();
413
+ },
414
+ });
415
+ ```
416
+
417
+ `reattach()` drops the local scrollback and screen, moves the connection state
418
+ off `ended`, and reconnects. The order is the reason it belongs here: the local
419
+ buffer holds the dead session's content and a line index the replacement has
420
+ never reached, so a bare reconnect would resume against the wrong index space, and
421
+ a bare reset would leave "Session ended" standing over a screen it just blanked.
422
+
423
+ It reconnects and nothing more. It starts no process and calls no API of yours, so
424
+ if your server needs to be told to make a new session, tell it first and reattach
425
+ after. Do not call it on a live session: that is a needless full replay, and it
426
+ drops history the server may have evicted since.
427
+
428
+ **Bound your own retries.** A shell that dies as fast as it is spawned will end
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 — the banner is
431
+ the honest outcome when a session cannot stay up. The library will not do this for
432
+ you, because how many attempts is worth making, and whether any are, is a fact
433
+ about your server rather than about this terminal.
434
+
390
435
  ## What ships
391
436
 
392
437
  | Path | Purpose |
package/css/30-tabs.css CHANGED
@@ -690,15 +690,29 @@
690
690
  in css/31-switcher.css. On the narrow-coarse phone the strip is hidden and
691
691
  the mobile switcher's inset is applied through viewport.ts
692
692
  (--wt-reserve-bottom) instead. Two rules because the class half cannot mix
693
- into a media condition; they set the same declaration. */
693
+ into a media condition; they set the same declaration.
694
+
695
+ The clearance is the strip's height PLUS --safe-bottom, because that is where
696
+ the strip is: .wt-tab-bar sits at `bottom: var(--safe-bottom)`, so the band it
697
+ occupies runs from --safe-bottom to --safe-bottom + --wt-tabbar-h. Clearing
698
+ the height alone left the surface reaching exactly env(safe-area-inset-bottom)
699
+ past the strip's top edge — 0 on a desktop, 20px in iPad Safari (viewport-fit
700
+ is cover, so the insets are live), where the terminal grew one extra row that
701
+ rendered behind the strip. --wt-tabbar-h is the bar's OWN height and must stay
702
+ that: the wide-touchscreen key grid (31-switcher.css) already adds the inset to
703
+ it itself, and folding the inset into the variable would double-count there.
704
+ The mobile switcher reaches the same total from the other direction, measuring
705
+ `innerHeight - rect.top` so the inset rides along inside --wt-reserve-bottom.
706
+ --kb-inset is deliberately absent: viewport.ts owns the keyboard case by
707
+ writing an inline bottom on the surface, which outranks this rule entirely. */
694
708
  @media (pointer: fine) {
695
709
  :where(.wt-root) .term.wt-with-tabbar {
696
- bottom: var(--wt-tabbar-h, 44px);
710
+ bottom: calc(var(--wt-tabbar-h, 44px) + var(--safe-bottom, 0px));
697
711
  }
698
712
  }
699
713
 
700
714
  :where(.wt-root:not(.wt-narrow)) .term.wt-with-tabbar {
701
- bottom: var(--wt-tabbar-h, 44px);
715
+ bottom: calc(var(--wt-tabbar-h, 44px) + var(--safe-bottom, 0px));
702
716
  }
703
717
 
704
718
  /* ===== Activity dots (activityMonitor via tabs) =====
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "6.0.2",
3
+ "version": "6.1.1",
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",
@@ -689,7 +689,7 @@ function buildTerminal(
689
689
  },
690
690
  onProcessExit() {
691
691
  // The engine's definitive 4001 close: the session's process has exited
692
- // and the engine will not reconnect it. Two jobs here. markReady()
692
+ // and the engine will not reconnect it. Three jobs here. markReady()
693
693
  // guarantees the page is usable even when the exit lands before any
694
694
  // screen frame (attaching to an already-dead session on a server that
695
695
  // races the replay) — without it the loading overlay would sit on top of
@@ -700,6 +700,18 @@ function buildTerminal(
700
700
  discardUnverifiedRestore({ session: connection.currentSessionId() });
701
701
  markReady();
702
702
  connState.ended();
703
+ // The host's turn, and LAST: everything above is unconditional, so a
704
+ // handler that throws cannot leave the page without its overlay lowered
705
+ // or its banner shown. The host is told because the kernel's own answer
706
+ // stops here — only the host knows whether its endpoint yields a new
707
+ // session on the next connect, which is what reattach() is for.
708
+ if (opts.onSessionEnded !== undefined) {
709
+ try {
710
+ opts.onSessionEnded();
711
+ } catch (err) {
712
+ console.error("web-terminal-ui: onSessionEnded handler failed", err);
713
+ }
714
+ }
703
715
  },
704
716
  onWireIncompatible() {
705
717
  // Compatibility refusal is terminal for this page instance. Lower the
@@ -1763,6 +1775,22 @@ function buildTerminal(
1763
1775
  render.resetScrollback();
1764
1776
  render.resetScreen();
1765
1777
  },
1778
+ reattach() {
1779
+ if (destroyed) {
1780
+ return;
1781
+ }
1782
+ // The old session's content goes first: it belongs to a process that is
1783
+ // gone, and holding it would also put a `haveThrough` on the resume that
1784
+ // claims lines above anything the replacement has committed.
1785
+ render.resetScrollback();
1786
+ render.resetScreen();
1787
+ // Off `ended`, or the banner would keep saying "Session ended" over a
1788
+ // screen this call just blanked. `reconnecting` carries a grace delay, so a
1789
+ // connect that lands promptly shows no banner at all and a slow one reads
1790
+ // as what it is.
1791
+ connState.reconnecting();
1792
+ connection.reconnectNow();
1793
+ },
1766
1794
  destroy() {
1767
1795
  if (rootReleased) {
1768
1796
  return;
@@ -645,6 +645,22 @@ export interface CreateTerminalOptions {
645
645
  * surface, so a reporting failure cannot leave the page blank. */
646
646
  // eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- an observing handler returns nothing; only a literal `true` claims the recovery surface, so forcing `return undefined` on every observer is worse than the union
647
647
  onFatalError?: (failure: TerminalStartupFailure) => boolean | void;
648
+ /** Called when the active session's process has ended and nothing is retrying:
649
+ * the engine's definitive process-exited close, the same fact the connection
650
+ * banner renders as "Session ended".
651
+ *
652
+ * Wired by a host that can do something about it, which means a host that
653
+ * knows its endpoint yields a new session on the next connect (see
654
+ * `TerminalHandle.reattach`). Everything the kernel does about the end — lower
655
+ * the loading overlay, surface the state, discard an unconfirmed restore —
656
+ * happens first and happens regardless, so a host that wires nothing loses
657
+ * nothing, and a callback that throws cannot take the banner down with it (the
658
+ * throw is logged and swallowed).
659
+ *
660
+ * Observation only: the recovery POLICY stays with the host, deliberately.
661
+ * Whether to reattach at all, how many times and how fast depends on what sits
662
+ * behind the endpoint, which this library does not know. */
663
+ onSessionEnded?: () => void;
648
664
  /** Theme overrides: CSS custom properties set on the terminal root so a
649
665
  * consumer recolors the UI (accent, tab hover/active, the activity-dot
650
666
  * palette) without shipping CSS. Keys must be CSS custom-property names
@@ -679,6 +695,28 @@ export interface TerminalHandle {
679
695
  * no keystroke — a host that wants a freshly drawn prompt sends one itself
680
696
  * (e.g. Ctrl+L via send()). No-op after destroy(). */
681
697
  reset(): void;
698
+ /** Attach again to whatever the server serves now, discarding this terminal's
699
+ * view of the session that is gone.
700
+ *
701
+ * For the one state the kernel cannot leave on its own: `ended`. The engine
702
+ * refuses to reconnect a definitively closed session, and it is right to on a
703
+ * per-session server, where reconnecting could only collect the same close
704
+ * again. A host whose endpoint hands out a NEW session on the next connect —
705
+ * one shared PTY that the server replaces once it is spent — is the only party
706
+ * that knows a reconnect is worth making, so it is the party that asks.
707
+ *
708
+ * Three steps, in this order, and none of them is a host's to perform: drop
709
+ * the local scrollback and screen (the old session's content, and a stale
710
+ * absolute-index base whose `haveThrough` would otherwise claim lines the new
711
+ * session has never reached), move the connection state off `ended` so the
712
+ * banner stops contradicting a blank screen, then reconnect.
713
+ *
714
+ * Reconnecting is ALL it does: it starts no process and asks the server for
715
+ * nothing. Whatever makes a new session exist is the host's own call to its
716
+ * own API, made before this one. Do not call it on a live session — that is a
717
+ * needless full replay, and it drops history the server may have evicted since.
718
+ * No-op after destroy(). */
719
+ reattach(): void;
682
720
  /** Tear down every feature in reverse order, dispose all subscriptions, and
683
721
  * release the kernel's DOM and engine wiring. */
684
722
  destroy(): void;