@cplieger/web-terminal-ui 8.6.0-dev.8 → 8.6.0-dev.9

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
@@ -14,7 +14,7 @@ web-terminal-ui is built for a shell or a coding agent served by web-terminal-en
14
14
 
15
15
  - One `createTerminal()` call builds the UI inside an element you provide. Four presets cover a single, a touch and two tabbed layouts.
16
16
  - On a touchscreen it adds a key bar with Tab, Esc, arrows, Enter and a sticky Ctrl, a long-press paste menu and iOS soft-keyboard handling.
17
- - Tabs show activity dots from the program's `OSC 9;4` progress reports and raise browser notifications. Two tabs can sit side by side. A full-page terminal gives each tab and each split its own page address, and names the browser tab after the active tab, followed by the page's own title: `fix build · Web Terminal`.
17
+ - Tabs show activity dots from the program's `OSC 7501` status, `OSC 9;4` progress and classified `OSC 9` notifications, and raise browser notifications. Two tabs can sit side by side. A full-page terminal gives each tab and each split its own page address, and names the browser tab after the active tab, followed by the page's own title: `fix build · Web Terminal`.
18
18
  - Native text selection, IME input and dictation work, and predictive echo hides latency.
19
19
 
20
20
  Consider [xterm.js](https://github.com/xtermjs/xterm.js) if you want a terminal component for any PTY backend, such as node-pty. It has an optional GPU-accelerated renderer and a set of addons, and VS Code uses it. To build a UI of your own on web-terminal-engine, depend on the engine alone.
package/css/00-tokens.css CHANGED
@@ -80,11 +80,12 @@
80
80
  --status-working: #52a9fe; /* agent working (waves): the same blue as the default --accent */
81
81
  --status-done: #22c55e; /* turn finished: solid green */
82
82
  --status-input: #fb923c; /* action required: solid orange + ring */
83
- /* OSC 9;4 progress states 4 (warning) and 2 (error). Both render as the SAME
84
- object as working -- identical glow + travelling wave, one shared rule and
85
- one shared pair of keyframes (30-tabs.css) -- differing ONLY in hue, because
86
- the escape code models one progress indicator with several states rather
87
- than three unrelated dots. Neither wears a ring: the frozen hard ring is
83
+ /* The warning and failed statuses (STATUS_WARNING and STATUS_FAILED in
84
+ features/tabs/model.ts name their sources). Both render as the SAME object
85
+ as working -- identical glow + travelling wave, one shared rule and one
86
+ shared pair of keyframes (30-tabs.css) -- differing ONLY in hue, because
87
+ OSC 9;4 models one progress indicator with several states rather than three
88
+ unrelated dots. Neither wears a ring: the frozen hard ring is
88
89
  --status-input's exclusive meaning ("blocked, waiting on you"), and neither
89
90
  warning nor error asks the user to act.
90
91
  LIGHTNESS SEPARATION IS LOAD-BEARING, NOT DECORATIVE. Three states that
@@ -100,8 +101,8 @@
100
101
  indistinguishable to a colour-blind user, whatever their hues. warning is
101
102
  YELLOW, deliberately not amber -- amber would collide with --status-input's
102
103
  orange (L 75.8%), which sits between working and warning. */
103
- --status-warning: #facc15; /* OSC 9;4 state 4: yellow disc, working's wave */
104
- --status-failed: #dc2626; /* OSC 9;4 state 2: red disc, working's wave; crashed reuses it solid and static */
104
+ --status-warning: #facc15; /* warning: yellow disc, working's wave */
105
+ --status-failed: #dc2626; /* a reported error: red disc, working's wave; crashed reuses it solid and static */
105
106
  --radius: 0.75rem;
106
107
  --radius-inner: 0.5rem;
107
108
  /* Small square icon buttons (e.g. the 24px tab-close x): at that size the
package/css/30-tabs.css CHANGED
@@ -107,8 +107,8 @@
107
107
  width: 300px;
108
108
  flex: 0 1 auto;
109
109
  min-width: 100px;
110
- /* Positioning context for the chip's own overlay: the OSC 9;4 determinate
111
- progress bar sits on its bottom edge (see .wt-progress-bar). Nothing else
110
+ /* Positioning context for the chip's own overlay: the determinate progress
111
+ bar sits on its bottom edge (see .wt-progress-bar). Nothing else
112
112
  inside a chip is absolutely positioned, and the strip's two root-level
113
113
  overlays (.wt-tab-ghost, .wt-tab-menu) are children of .wt-root, not of a
114
114
  chip, so this cannot capture them. */
@@ -660,13 +660,13 @@
660
660
  }
661
661
 
662
662
  /* ===== Activity dots: one .wt-status-dot vocabulary for the strip, the
663
- switcher and the overview rows. Eight states told apart without colour
664
- (WCAG 1.4.1, by shape and motion): three animated progress hues (working,
665
- warning, failed) spread across lightness, a ringed disc (input), two static
666
- discs (done, crashed) and two hollow ones (idle, exited). A dot shows once
667
- its session reports OSC 9;4 progress or a classified OSC 9 notification
668
- (.wt-reports, set by paintStatusDot), or from creation under tabs'
669
- presumeReports option; data-status is set either way. */
663
+ switcher and the overview rows. Eight built-in states told apart without
664
+ colour (WCAG 1.4.1, by shape and motion): three animated progress hues
665
+ (working, warning, failed) spread across lightness, a ringed disc (input), two
666
+ static discs (done, crashed) and two hollow ones (idle, exited); any other
667
+ status keeps the default hollow dot below. A dot shows once its session
668
+ reports activity (.wt-reports, set by paintStatusDot) or from creation under
669
+ tabs' presumeReports option; data-status is set either way. */
670
670
  :where(.wt-root) .wt-status-dot {
671
671
  display: none;
672
672
  width: 9px;
@@ -688,12 +688,10 @@
688
688
  animation: none;
689
689
  }
690
690
 
691
- /* ===== Determinate progress bar (OSC 9;4 percentage) =====
692
- The second display mode the specs name for a progress report (iTerm2 calls it
693
- a progress bar; ConEmu also puts the value on the taskbar, which is the
694
- document-title prefix's job). One 2px line on the chip's bottom edge, shared
695
- by all three chip sites (desktop strip chip, mobile active row, expanded list
696
- row) because it ships in the one chipContent builder.
691
+ /* ===== Determinate progress bar (the session's percentage: OSC 7501 progress, else OSC 9;4) =====
692
+ One 2px line on the chip's bottom edge, shared by all three chip sites
693
+ (desktop strip chip, mobile active row, expanded list row) because it ships
694
+ in the one chipContent builder.
697
695
 
698
696
  The ACCENT colour, not a per-state tint: the bar answers "how far", the dot
699
697
  answers "in what state", and tinting the bar per state would say the same
@@ -746,8 +744,8 @@
746
744
  the element), which read as a blink and coupled the layers — an element's
747
745
  opacity always multiplies onto its pseudos, so true independence requires
748
746
  animating disc COLORS, not element opacity. Server-side this is driven by the
749
- OSC 9;4 progress signal, so it tracks agent activity, not the user typing at
750
- the prompt.
747
+ program's own status reports, so it tracks agent activity, not the user
748
+ typing at the prompt.
751
749
 
752
750
  WORKING, WARNING AND FAILED SHARE THIS RULE, AND BOTH KEYFRAMES, VERBATIM.
753
751
  OSC 9;4 models ONE progress indicator with several states (1/3 active,
@@ -360,7 +360,7 @@
360
360
  process crashed or reported an error. Those four only (CueStatus) — the rule
361
361
  the user set is "only states that want the user, like a notification", so the
362
362
  ongoing progress states (working / warning) never raise it. `failed` DOES,
363
- because OSC 9;4 state 2 is a result the program parked rather than a phase in
363
+ because a reported error is a result the program parked rather than a phase in
364
364
  progress; the override below is what keeps it looking like a notification here.
365
365
  Reuses the exact .wt-status-dot rules (30-tabs.css, --status-* tokens) so it
366
366
  matches the per-tab dots. Hidden until the tabs feature sets data-status;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "8.6.0-dev.8",
3
+ "version": "8.6.0-dev.9",
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",
@@ -330,14 +330,13 @@ class SettleCreateFailed extends Error {
330
330
  }
331
331
 
332
332
  /** Read the layout, then list, then (when nothing live is listed) create one
333
- * session, starting the turn again after any 503, so a
334
- * create refused mid-restore never adds a tab beside the restored ones. A
335
- * non-503 layout read proves the host was not restoring when it was served, so
336
- * a list issued after it sees the whole restored set. Past
337
- * `CREATE_RETRY_MAX_TOTAL_MS` a refused layout read or list returns the last
338
- * good list, or rethrows the 503; a create that fails for good, a spent 503
339
- * included, throws `SettleCreateFailed` carrying the list, an abort its
340
- * reason. */
333
+ * session, starting the turn again after any 503, so a create refused
334
+ * mid-restore never adds a tab beside the restored ones. A non-503 layout read
335
+ * proves the host was not restoring when it was served, so a list issued after
336
+ * it sees the whole restored set. Past `CREATE_RETRY_MAX_TOTAL_MS` a refused
337
+ * layout read or list returns the last good list, or rethrows the 503; a create
338
+ * that fails for good, a spent 503 included, throws `SettleCreateFailed`
339
+ * carrying the list, an abort its reason. */
341
340
  async function settleSessionSet(
342
341
  api: SessionAPIClient,
343
342
  ctx: TerminalContext,
@@ -1068,9 +1067,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
1068
1067
  const prefersReduce = (): boolean =>
1069
1068
  win.matchMedia("(prefers-reduced-motion: reduce)").matches;
1070
1069
 
1071
- // Recomputes every display label with de-duplication. The percentage prefix
1072
- // is applied at render time and stored nowhere, so de-duplication and the
1073
- // rename field see real names and clearing it needs no cleanup.
1070
+ // Recomputes every display label with de-duplication.
1074
1071
  function relabelAll(): void {
1075
1072
  // The "(k)" suffix is numbered by the session's AGE, never by its slot:
1076
1073
  // numbered by encounter order, the labels stayed put while the sessions
@@ -1140,11 +1137,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
1140
1137
  return foldedCueStatus(statusOf(t), t.activity);
1141
1138
  }
1142
1139
 
1143
- /** shownProgress is a tab's percentage as it may currently be DISPLAYED:
1144
- * the last value the server reported, dropped unless the current status is
1145
- * one the progress channel owns. Those are the only two ways a percentage
1146
- * stops showing — the program's own OSC 9;4;0 (an explicit -1) and a status
1147
- * from another channel; see renderedProgress for why there is no third. */
1140
+ /** shownProgress is a tab's percentage as it may currently be DISPLAYED;
1141
+ * renderedProgress owns when that is. */
1148
1142
  function shownProgress(t: Tab): number {
1149
1143
  return renderedProgress(statusOf(t), t.progress);
1150
1144
  }
@@ -3344,8 +3338,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
3344
3338
  t.seenSeq = seen;
3345
3339
  closeReadSurfaces(t);
3346
3340
  }
3347
- // A program that speaks OSC 9 may also post a notification: arm the
3348
- // permission request for the next user gesture.
3341
+ // A program that reports activity may also post an OSC 9 notification:
3342
+ // arm the permission request for the next user gesture.
3349
3343
  if (reports) {
3350
3344
  ctx.shell.notifications.arm();
3351
3345
  }
@@ -19,7 +19,9 @@ export type StatusRecord = SessionInfo & {
19
19
  };
20
20
 
21
21
  // The status vocabulary is module-private so a status's MEANING has one home:
22
- // every consumer asks a predicate below instead of comparing strings.
22
+ // every consumer asks a predicate below instead of comparing strings. The
23
+ // protocol states named below are the built-in sources; a classified OSC 9
24
+ // notification may set any status.
23
25
  //
24
26
  // An exited session is viewable history that can never produce output again, so
25
27
  // session selection prefers live sessions everywhere.
@@ -30,11 +32,14 @@ const STATUS_EXITED = "exited";
30
32
  * restart never paints as a failure. Terminal, like "exited". */
31
33
  const STATUS_CRASHED = "crashed";
32
34
 
33
- /** OSC 9;4 progress state 2, the error state (iTerm2 semantics). A STATE that
34
- * persists until the program reports another, or the process dies. */
35
+ /** An error the program reported: OSC 7501 state `error`, or OSC 9;4 progress
36
+ * state 2 (iTerm2 semantics). A STATE that persists until the program reports
37
+ * another, or the process dies. */
35
38
  const STATUS_FAILED = "failed";
36
39
 
37
- /** OSC 9;4 progress state 4, the warning state (iTerm2 semantics). */
40
+ /** OSC 9;4 progress state 4, the warning state (iTerm2 semantics). OSC 9;4
41
+ * cannot set it once the session has sent an OSC 7501 report, until a
42
+ * terminal reset (RIS): OSC 7501 outranks OSC 9;4 until a RIS forgets it. */
38
43
  const STATUS_WARNING = "warning";
39
44
 
40
45
  /** Whether the session's process is GONE, whichever way it went: reloading onto
@@ -51,8 +56,9 @@ export function statusRevealsDot(status: string): boolean {
51
56
  }
52
57
 
53
58
  /** The human wording for a status, for BOTH the dot's tooltip and the tab's
54
- * accessible name, so they cannot disagree. An unknown status (a newer server)
55
- * falls back to the raw value rather than being hidden. */
59
+ * accessible name, so they cannot disagree. A status outside the built-in
60
+ * vocabulary (a host classifier's, or a newer server's) falls back to the raw
61
+ * value rather than being hidden. */
56
62
  export function statusPhrase(status: string): string {
57
63
  switch (status) {
58
64
  case "working":
@@ -94,22 +100,16 @@ export function normalizeProgress(value: unknown): number {
94
100
  return Math.min(100, Math.round(value));
95
101
  }
96
102
 
97
- /** Whether the OSC 9;4 progress channel is speaking through this status; only
98
- * these may show a bar. The server keeps sending the last percentage while the
99
- * state persists, and `input`, `done`, `idle`, `exited` and `crashed` come from
100
- * other channels, so a percentage under them is a claim about the wrong one. */
101
- export function statusOwnsProgress(status: string): boolean {
102
- return status === "working" || status === STATUS_FAILED || status === STATUS_WARNING;
103
- }
104
-
105
- /** The percentage a status may SHOW. Two things clear it: the program's own
106
- * OSC 9;4;0 (arriving as PROGRESS_ABSENT) and a status the channel does not own
107
- * (a state-4 percentage persists under a done latch, which state 4 does not
108
- * clear, and would otherwise paint a bar beside a done dot). Deliberately
109
- * no third clear: 100% is a STATE that persists, and a timeout would assert a
110
- * change the program never reported. */
103
+ /** The percentage a status may SHOW. Under any live status it is the server's
104
+ * value, because the status and the percentage can come from different
105
+ * channels: OSC 7501 or a notification can set the status while OSC 9;4
106
+ * supplies the percentage. Two things clear it: the server's PROGRESS_ABSENT
107
+ * (the program cleared or never reported a percentage, e.g. OSC 9;4;0) and an
108
+ * ended process, whose percentage nothing will ever clear. No timeout clears
109
+ * it: 100% is a STATE that persists, and a timeout would assert a change the
110
+ * program never reported. */
111
111
  export function renderedProgress(status: string, progress: number): number {
112
- return statusOwnsProgress(status) ? progress : PROGRESS_ABSENT;
112
+ return isEndedStatus(status) ? PROGRESS_ABSENT : progress;
113
113
  }
114
114
 
115
115
  // The secondary-activity vocabulary: a host-reported background activity that
@@ -221,8 +221,8 @@ export function cueIconName(status: CueStatus): "input" | "done" | "alert" {
221
221
 
222
222
  /** The cue statuses meaning the turn is OVER WITHOUT INCIDENT, the only ones a
223
223
  * live background task may blank. `done` is the whole set: every other cue must
224
- * reach the viewer AT ONCE, and `failed` here is OSC 9;4 state 2 rather than a
225
- * turn verdict. */
224
+ * reach the viewer AT ONCE, and `failed` is an error the program reported,
225
+ * not a clean end of the turn. */
226
226
  const SETTLED_CUES: ReadonlySet<string> = new Set<CueStatus>(["done"]);
227
227
 
228
228
  /** The status the CUE surfaces see: the session's own, unless a settled turn's
@@ -380,10 +380,9 @@ export interface Tab {
380
380
  /** The chip's determinate progress bar (the 2px line on its bottom edge).
381
381
  * Present on every chip, hidden until a percentage exists. */
382
382
  progressEl: HTMLElement;
383
- /** The last OSC 9;4 percentage the server reported for this session, or
383
+ /** The last percentage the server reported for this session, or
384
384
  * PROGRESS_ABSENT. Held raw: whether it is currently SHOWN is a render-time
385
- * question (renderedProgress), and the prefix it produces is never stored on
386
- * any title field. */
385
+ * question (renderedProgress). */
387
386
  progress: number;
388
387
  /** The chip's secondary activity mark (the rounded-square ring beside the
389
388
  * status dot). Present on every chip, and costing no layout until a state
@@ -403,9 +402,10 @@ export interface Tab {
403
402
  * offset away, and it stopped meaning the same line once the session produced
404
403
  * output while backgrounded. In memory only. */
405
404
  view: ViewMemory | null;
406
- /** Sticky: true once this session emitted a genuine activity signal (OSC 9;4).
407
- * Its activity dot is shown only while true; a session that never reports
408
- * activity (a plain shell) keeps a clean, dot-less tab. Fed from the server's
405
+ /** Sticky: true once this session emitted a genuine activity signal (OSC 7501
406
+ * status, OSC 9;4 progress or a classified OSC 9 notification). Its activity
407
+ * dot is shown only while true; a session that never reports activity (a
408
+ * plain shell) keeps a clean, dot-less tab. Fed from the server's
409
409
  * reportsActivity via applyStatus. */
410
410
  reports: boolean;
411
411
  /** The user's pinned name (the server's `pinnedTitle` wire field). Outranks
@@ -235,7 +235,7 @@ export function paintStatusDot(el: HTMLElement, status: string, reports: boolean
235
235
  * a tooltip worded by activityPhrase. No state REMOVES the attribute, which is
236
236
  * what collapses the mark's footprint to zero. The reveal is the mark's OWN
237
237
  * state, not the status dot's reportsActivity gate: a background run is a fact
238
- * independent of whether the program ever spoke OSC 9. */
238
+ * independent of whether the program ever reported status. */
239
239
  export function paintActivityMark(el: HTMLElement, state: string, count: number): void {
240
240
  const phrase = activityPhrase(state, count);
241
241
  if (phrase === "") {
@@ -10,14 +10,12 @@ import { type TabbedPresetOptions, buildTabbed } from "./tabbed.js";
10
10
  /** Tabbed UI for an agent shell. Same features as presetTabbed, tuned for
11
11
  * sessions that ARE agents:
12
12
  * - `presumeReports`: every session's program is an agent that WILL report
13
- * OSC 9;4 progress, so the idle activity dot shows from tab creation
14
- * instead of popping in seconds later when the agent has booted far enough
15
- * to first report; the server's sticky reportsActivity flag then merely
16
- * confirms. (presetTabbed keeps the evidence-driven reveal instead: a
17
- * plain shell never grows a meaningless dot.)
18
- * One agent-vs-generic distinction lives SERVER-side rather than here: the
19
- * status classifier that maps OSC 9 notifications to done/needs-input. Tab
20
- * names are server-side too but are not agent-specific — see buildTabbed. */
13
+ * activity (OSC 7501 status, OSC 9;4 progress or a classified OSC 9
14
+ * notification), so the idle activity dot
15
+ * shows from tab creation instead of popping in when the agent first
16
+ * reports; the server's sticky reportsActivity flag then merely confirms.
17
+ * (presetTabbed keeps the evidence-driven reveal: a plain shell never grows
18
+ * a meaningless dot.) */
21
19
  export function presetAgentTabbed(opts: TabbedPresetOptions = {}): TerminalFeature<unknown>[] {
22
20
  return buildTabbed(true, opts);
23
21
  }
@@ -29,10 +29,11 @@ export interface TabbedPresetOptions {
29
29
  * toolbar opened from the tab bar's keyboard button, the activity monitor (the
30
30
  * status SSE), tabs wired to both, and animations. The monitor is always
31
31
  * included: a tab's dot shows only once its session reports activity (OSC
32
- * 9;4), or from creation under presumeReports. The toolbar and monitor precede
33
- * tabs, which reads their APIs via ctx.use. Titles are the engine's: a browser
34
- * that re-derived one would disagree with the server and every other client.
35
- * Shared by presetTabbed and presetAgentTabbed (agent-tabbed.ts). */
32
+ * 7501 status, OSC 9;4 progress or a classified OSC 9 notification), or from
33
+ * creation under presumeReports.
34
+ * The toolbar and monitor precede tabs, which reads their APIs via ctx.use.
35
+ * Titles are the engine's: a browser that re-derived one would disagree with
36
+ * the server and every other client. Shared with presetAgentTabbed. */
36
37
  export function buildTabbed(
37
38
  agentShell: boolean,
38
39
  opts: TabbedPresetOptions = {},
package/src/presets.ts CHANGED
@@ -11,13 +11,14 @@
11
11
  * The tabbed presets (presetTabbed / presetAgentTabbed) share the same feature
12
12
  * set (both include the activity monitor, which drives the per-tab activity
13
13
  * dot). They differ in one flag, `presumeReports`: presetAgentTabbed presumes
14
- * every session's program is an agent that WILL report OSC 9;4 progress, so the
15
- * idle dot shows from tab creation instead of appearing seconds later on the
16
- * first report, while presetTabbed keeps the evidence-driven reveal so a plain
17
- * shell never grows a meaningless dot. Session NAMES are not a preset concern:
18
- * the engine resolves them, including a title its host pushes.
19
- * The generic-vs-agent STATUS distinction stays server-side — an agent server
20
- * sets a classifier mapping OSC 9 notifications to done/needs-input.
14
+ * every session's program is an agent that WILL report activity (OSC 7501
15
+ * status, OSC 9;4 progress or a classified OSC 9 notification), so the idle dot
16
+ * shows from tab creation instead of appearing seconds later on the first
17
+ * report, while presetTabbed keeps the evidence-driven reveal so a plain shell
18
+ * never grows a meaningless dot.
19
+ * Session NAMES are not a preset concern: the engine resolves them, including a
20
+ * title its host pushes. Which status a session holds is the server's: the UI
21
+ * renders the status it is sent.
21
22
  *
22
23
  * @module
23
24
  */