@cplieger/web-terminal-engine 6.4.0-dev.6 → 6.4.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/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "6.4.0-dev.6",
3
+ "version": "6.4.0-dev.9",
4
4
  "license": "MPL-2.0",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "6.4.0-dev.6",
3
+ "version": "6.4.0-dev.9",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "typecheck": "tsc -p tsconfig.json",
@@ -22,8 +22,8 @@
22
22
  "@stryker-mutator/vitest-runner": "10.0.0",
23
23
  "@types/pngjs": "6.0.5",
24
24
  "@typescript/native": "npm:typescript@7.0.2",
25
- "@vitest/browser-playwright": "5.0.4",
26
- "@vitest/coverage-v8": "5.0.4",
25
+ "@vitest/browser-playwright": "5.0.5",
26
+ "@vitest/coverage-v8": "5.0.5",
27
27
  "esbuild": "0.28.2",
28
28
  "eslint": "10.12.0",
29
29
  "fast-check": "4.10.2",
@@ -33,7 +33,7 @@
33
33
  "prettier": "3.9.10",
34
34
  "typescript": "npm:@typescript/typescript6@6.0.2",
35
35
  "typescript-eslint": "8.71.2",
36
- "vitest": "5.0.4"
36
+ "vitest": "5.0.5"
37
37
  },
38
38
  "overrides": {
39
39
  "qs": "^6.15.2"
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ export { wsURL } from "./wsurl.js";
40
40
  export { WS_PATH, SESSIONS_PATH, SESSION_EVENTS_PATH } from "./routes.js";
41
41
  export { connectStatusStream } from "./status-stream.js";
42
42
  export type {
43
+ KnownStatus,
43
44
  SessionInfo,
44
45
  SessionStatus,
45
46
  StatusStream,
package/src/keyboard.ts CHANGED
@@ -339,10 +339,10 @@ function anyModifier(ev: KeyboardEvent): boolean {
339
339
  }
340
340
 
341
341
  /**
342
- * The kitty "unicode-key-code": the UNSHIFTED (lower-case / base-layout)
343
- * codepoint of the key. Letters and digits are read from ev.code (the physical
344
- * key), so a composed Alt/Option character on macOS still maps to its base key;
345
- * other single-character keys fall back to the case-folded ev.key.
342
+ * The kitty "unicode-key-code", read as the UNSHIFTED US-layout codepoint of
343
+ * the physical key: letters, digits and KITTY_CODE_BASE's punctuation come from
344
+ * ev.code, so a composed Option character or a shifted symbol maps to its base
345
+ * key; any other single-character key falls back to the case-folded ev.key.
346
346
  */
347
347
  function kittyKeyCodepoint(ev: KeyboardEvent): number {
348
348
  const code = ev.code;
@@ -431,6 +431,12 @@ function encodeKittyDisambiguate(ev: KeyboardEvent): KeyboardResult {
431
431
  if (ev.metaKey && !ev.ctrlKey && !ev.altKey) {
432
432
  return { kind: "ignore" };
433
433
  }
434
+ // A desktop AltGr character arrives with ctrlKey and altKey clear, so it stays
435
+ // text: on Windows, Chromium and Firefox swap Ctrl+Alt for AltGraph whenever
436
+ // the chord types a character (https://chromestatus.com/feature/5059438092222464,
437
+ // https://searchfox.org/mozilla-central/source/widget/windows/KeyboardLayout.cpp).
438
+ // AltGraph must not exempt a key here: Firefox sets it on every macOS Option
439
+ // chord (widget/cocoa/nsCocoaUtils.mm), and Ctrl+Option+a is 97;7u.
434
440
  if (ev.ctrlKey || ev.altKey || ev.metaKey) {
435
441
  const cp = kittyKeyCodepoint(ev);
436
442
  if (cp > 0) {
@@ -480,15 +486,6 @@ export function mapKeyboardEvent(ev: KeyboardEvent, modes: KeyboardModes): Keybo
480
486
  }
481
487
  }
482
488
 
483
- // Browser-reserved meta combos (Cmd+R, Cmd+T, Cmd+Q, Cmd+W on Mac;
484
- // Ctrl+R, Ctrl+T, Ctrl+W with no Shift on others). Let the browser
485
- // handle them. Heuristic: only take Ctrl+letter when Shift is NOT
486
- // held AND the letter is one of the ones xterm consumes (a-z),
487
- // skipping the few that browsers commonly reserve.
488
- // (Ctrl+R, Ctrl+T and Ctrl+W still go to the PTY: readline binds them to
489
- // reverse search, transpose and word rubout, and full-screen programs bind
490
- // them too. On macOS, Cmd reaches the browser's own combos.)
491
-
492
489
  // Shift+PageUp / Shift+PageDown — local scrollback navigation
493
490
  // (matches xterm.js KeyboardResultType.PAGE_UP/PAGE_DOWN).
494
491
  if (ev.key === "PageUp" && ev.shiftKey && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {
@@ -590,7 +587,9 @@ export function mapKeyboardEvent(ev: KeyboardEvent, modes: KeyboardModes): Keybo
590
587
  return { kind: "ignore" }; // let `input` event handle plain space
591
588
  }
592
589
 
593
- // Single printable character with modifiers.
590
+ // Single printable character with modifiers. Neither branch takes Meta, so
591
+ // the browser keeps Cmd+R/T/W/C/V; Ctrl+R/T/W go to the PTY, where readline
592
+ // binds them.
594
593
  if (ev.key.length === 1) {
595
594
  const ch = ev.key;
596
595
 
@@ -7,6 +7,11 @@
7
7
 
8
8
  import { nextBackoffDelay } from "./reconnect.js";
9
9
 
10
+ /** KnownStatus is a status the server derives itself, one per Go terminal.Status*
11
+ * constant. */
12
+ export type KnownStatus =
13
+ "working" | "idle" | "input" | "done" | "exited" | "crashed" | "failed" | "warning";
14
+
10
15
  /** SessionInfo is one session's wire shape: the JSON object the session REST
11
16
  * API (GET/POST /api/sessions) returns per session, kept in lockstep with the Go
12
17
  * terminal.SessionInfo by hand.
@@ -16,21 +21,14 @@ import { nextBackoffDelay } from "./reconnect.js";
16
21
  * a CLIENT-DERIVED automatic title a client asked the server to remember. */
17
22
  export interface SessionInfo {
18
23
  readonly id: string;
19
- /** the session's current status. "working"/"idle"/"exited"/"crashed" are
20
- * derived by the server; "input"/"done" are latched from a classified OSC 9
21
- * notification; "failed" is OSC 9;4 progress state 2 and "warning" is state 4
22
- * (iTerm2 semantics), both of which PERSIST until the program changes the
23
- * progress state. "exited" and "crashed" split the session's end: "exited" is
24
- * an ordinary one (status 0, or any exit the server itself caused — a closed
25
- * session, the idle reaper, a server shutdown), "crashed" is a non-zero exit
26
- * status or a terminating signal the program was not asked for, so a routine
27
- * restart never renders as a failure. Neither is a progress state: they
28
- * outrank everything, and nothing clears them. A consumer must tolerate an
29
- * UNKNOWN status string from a newer server: the stream is parsed, not
30
- * validated, and an unrecognised value is forwarded rather than dropped
31
- * (forward compatibility across the wire floor). */
32
- readonly status:
33
- "working" | "idle" | "input" | "done" | "exited" | "crashed" | "failed" | "warning";
24
+ /** the session's current status. The server derives a KnownStatus from the
25
+ * program's OSC 7501 report, or from OSC 9;4 progress while it has sent none
26
+ * since a hard reset (RIS); "exited" and "crashed" mark the process's end
27
+ * and nothing clears them. A classified OSC 9 notification latches whatever
28
+ * string the server's classifier returns, and a newer server may add values,
29
+ * so a consumer must handle any other string: the stream forwards it
30
+ * unchanged. The engine's docs/sessions.md gives every mapping. */
31
+ readonly status: KnownStatus | (string & {});
34
32
  readonly title: string;
35
33
  /** the session's short public name, safe to put in a URL: 1 to 64 characters
36
34
  * of `[A-Za-z0-9_-]`, unique among live sessions, and NOT a capability
@@ -72,11 +70,11 @@ export interface SessionInfo {
72
70
  * view: a list built from that hybrid still names the live set exactly, so the
73
71
  * server accepts it and it becomes the arrangement every device sees. */
74
72
  readonly order?: number;
75
- /** true once the session has emitted a genuine activity signal — OSC 9;4
76
- * progress or a classified OSC 9 notification.
77
- * Sticky for the session's life. Consumers reveal the per-tab activity dot
78
- * only when this is set; a program that emits no OSC 9 signal (a plain shell)
79
- * keeps its tab dot hidden. */
73
+ /** true once the session has emitted a genuine activity signal — an OSC 7501
74
+ * report, OSC 9;4 progress or a classified OSC 9 notification.
75
+ * Sticky for the session's life, a hard reset (RIS) included. Consumers
76
+ * reveal the per-tab activity dot only when this is set; a program that
77
+ * emits none of the three (a plain shell) keeps its tab dot hidden. */
80
78
  readonly reportsActivity?: boolean;
81
79
  /** the session's SECONDARY activity: a host-reported background activity that
82
80
  * OUTLIVES the turn, carried beside `status` and never merged into it. The
@@ -84,18 +82,19 @@ export interface SessionInfo {
84
82
  * (stopped and resumable, nobody is being asked) and `"input"` (a background
85
83
  * task is blocked on the user); absent or an empty string means no secondary
86
84
  * activity, and a consumer must tolerate an UNKNOWN value from a newer server
87
- * by rendering nothing for it.
88
- *
89
- * Typed `string`, deliberately NOT a union like `status`: that union's own doc
90
- * already admits it must tolerate values outside itself, which forces a cast at
91
- * every forward-compatible site. Absent from a server that reports none. */
85
+ * by rendering nothing for it. Absent from a server that reports none. */
92
86
  readonly activity?: string;
93
87
  /** how many sources produced `activity`: >= 1 whenever `activity` is non-empty,
94
88
  * 0 when it is empty or the host does not count them. */
95
89
  readonly activityCount?: number;
96
- /** numbers the occurrence of `status`: it rises by one each time the status
97
- * changes or a fresh notification re-latches it, and never falls for one id.
98
- * Absent (or 0) means nothing numbered yet, including from an older server. */
90
+ /** numbers the occurrence of the session's status. A sweep raises it by one
91
+ * when it emits a changed status, a status a classified notification latched
92
+ * since the last sweep, or an input, done or failed status after an OSC 7501
93
+ * report since then created a record, changed one's state or kind, or
94
+ * cleared one, and so changed which record the status came from, or its
95
+ * state or kind, at that report. A report keeping a record's state and kind
96
+ * raises it only by changing the status. It never falls for one id; 0,
97
+ * absent on the wire, means nothing numbered yet. */
99
98
  readonly statusSeq?: number;
100
99
  /** the highest `statusSeq` any reader acknowledged through
101
100
  * `PUT /api/sessions/{id}/seen`. Never falls for one id and never exceeds
@@ -110,9 +109,10 @@ export interface SessionInfo {
110
109
  export interface SessionStatus extends SessionInfo {
111
110
  /** true when the session is gone (closed or reaped); the consumer drops it. */
112
111
  readonly removed?: boolean;
113
- /** the OSC 9;4 percentage: -1 when absent or unknown (no sequence seen, or a
114
- * state that carries none — clear/indeterminate), else 0-100. Paired with
115
- * `status` to render a determinate bar. */
112
+ /** the session's percentage: the OSC 7501 report's `progress` when it
113
+ * carries one, else the OSC 9;4 percentage. -1 when absent or unknown (no
114
+ * sequence seen, or a state that carries none — clear/indeterminate), else
115
+ * 0-100. Paired with `status` to render a determinate bar. */
116
116
  readonly progressValue?: number;
117
117
  /** the OSC 9 notification message, delivered to EVERY subscriber whether or
118
118
  * not the server has a status classifier installed. A notification is an