@cplieger/web-terminal-engine 5.1.0 → 5.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
@@ -93,7 +93,7 @@ document.addEventListener("keydown", (ev) => {
93
93
  - **`modes`** — DEC private mode state (synced from server's `ModesMessage`). `setModes`, `isBracketedPaste`, `isApplicationCursor`, `getMouseMode`, `isMouseSGR`, `isFocusReporting`, `isApplicationKeypad`, `isReverseVideo`.
94
94
  - **`decodeWireBinary(buf)`**: Top-level decoder for the binary WebSocket frames. Returns a `ServerMessage` or `null` for invalid/truncated frames.
95
95
  - **Wire compatibility metadata**: `WIRE_PROTOCOL_VERSION`, `MIN_SUPPORTED_SERVER_WIRE_VERSION`, `WIRE_INCOMPATIBLE_CLOSE_CODE`, and `WIRE_COMPATIBILITY` publish this client release's directional contract. The same values ship as a language-neutral JSON artifact for non-TypeScript consumers — see [Wire compatibility manifest](#wire-compatibility-manifest).
96
- - **`connection`**: Client → server WebSocket lifecycle: owns the socket, exponential-backoff reconnect, and the resume/inputAck reliability layer (outbox + server-restart detection). `init(callbacks)`, `connect`, `sendBinary(bytes)`, `sendEphemeral(text)`, `setClientFocus(focused)`, `sendResize`, `reconnectNow`. `sendEphemeral` carries BEST-EFFORT input (mouse reports): it sends only when a socket is live right now, returns whether it did, and never touches the outbox or the byte counters, so a report is forgotten rather than replayed against a screen the resume has since repainted. Against a server that declares neither capability it falls back to `sendBinary`, because an unrecognized control is dropped post-latch and total silence is a worse failure than a stale report — on that path the report rides the reliable outbox and can be retransmitted after a reconnect. `setClientFocus` reports the terminal widget's focus: it sends the `focus` control when the server declared it derives the DEC 1004 answer (from attachment state, every attached client's report, and its own `terminal.WithKeepUnfocused`), and otherwise writes `CSI I` / `CSI O` itself while the application has 1004 enabled — including on the enable edge, so an application that turns focus reporting on while the tab is already focused is told so immediately. The callbacks expose `onMessage(ServerMessage)`, `onOpen`/`onClose`/`onConnecting`/`onOutboxFull`/`onServerRestart`, `onWireVersionMismatch`, and the definitive `onWireIncompatible`; a `computeSize()` provider; and an optional `wsPath` (defaults to `"/ws"`). An explicit below-floor server revision or close code 4002 stops automatic reconnects until `disconnect()` or a page reload. Version-silent and future-revision servers remain tolerated. The module decodes frames internally and applies `modes.setModes`, so a consumer only needs to dispatch screen/scroll to `render`. Prefer this over wiring `WebSocket` + `decodeWireBinary` by hand unless you need full control.
96
+ - **`connection`**: Client → server WebSocket lifecycle: owns the socket, exponential-backoff reconnect, and the resume/inputAck reliability layer (outbox + server-restart detection). `init(callbacks)`, `connect`, `sendBinary(bytes)`, `sendEphemeral(text)`, `setClientFocus(focused)`, `sendResize`, `reconnectNow`. `sendEphemeral` carries BEST-EFFORT input (mouse reports): it sends only when a socket is live right now, returns whether it did, and never touches the outbox or the byte counters, so a report is forgotten rather than replayed against a screen the resume has since repainted. Against a server that declares neither capability it falls back to `sendBinary`, because an unrecognized control is dropped post-latch and total silence is a worse failure than a stale report — on that path the report rides the reliable outbox and can be retransmitted after a reconnect. `setClientFocus` reports the terminal widget's focus: it sends the `focus` control when the server declared it derives the DEC 1004 answer (from attachment state, every attached client's report, and its own `terminal.WithKeepUnfocused`), and otherwise drops the report. The client writes no focus bytes in either case, because one client cannot answer for a session two devices may be attached to and a client-written `CSI I` would override a keep-unfocused policy it cannot see; the next resumeAck re-asserts the current value once the capabilities are known. The callbacks expose `onMessage(ServerMessage)`, `onOpen`/`onClose`/`onConnecting`/`onOutboxFull`/`onServerRestart`, `onWireVersionMismatch`, and the definitive `onWireIncompatible`; a `computeSize()` provider; and an optional `wsPath` (defaults to `"/ws"`). An explicit below-floor server revision or close code 4002 stops automatic reconnects until `disconnect()` or a page reload. Version-silent and future-revision servers remain tolerated. The module decodes frames internally and applies `modes.setModes`, so a consumer only needs to dispatch screen/scroll to `render`. Prefer this over wiring `WebSocket` + `decodeWireBinary` by hand unless you need full control.
97
97
  - **`controlFrame(msg)` / `wsURL(proto, host, path?)`** — Low-level helpers for the client → server protocol (0x00-prefixed JSON control frames, WebSocket URL building). Used internally by `connection`; exported for advanced consumers.
98
98
 
99
99
  Wire types (`WireRun`, `ScreenMessage`, `ScrollMessage`, `ModesMessage`, `TitleMessage`, `ResumeAckMessage`, `ServerMessage`, `ControlMessage`) are re-exported from the package root and match the Go server's wire format byte-for-byte.
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "5.1.0",
3
+ "version": "5.1.1",
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": "5.1.0",
3
+ "version": "5.1.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "typecheck": "tsc -p tsconfig.json",
package/src/connection.ts CHANGED
@@ -334,14 +334,6 @@ let clientFocused = false;
334
334
  */
335
335
  let focusReported: boolean | null = null;
336
336
 
337
- /**
338
- * The 1004 state last seen in a modes frame on this socket, for the fallback
339
- * path's enable-edge detection. Per socket: a fresh socket is re-announced its
340
- * modes, and the edge has to be detected against nothing rather than against the
341
- * previous socket's last value.
342
- */
343
- let lastFocusReportingSeen = false;
344
-
345
337
  /** WebSocket.OPEN. Spelled out because a test's fake constructor has no statics. */
346
338
  const WS_OPEN = 1;
347
339
 
@@ -362,17 +354,16 @@ function clearHistoryTimers(): void {
362
354
  /**
363
355
  * Reset everything a new socket must not inherit: the fetch state (capability,
364
356
  * single-flight, pacing and the adaptive budget), the declared server
365
- * capabilities, and the focus reporting latches. All of it goes together,
366
- * atomically. Splitting them is how a client ends up paging against a server
367
- * that never declared it, bursting into a depleted server bucket, or holding a
368
- * focus report back from the new socket as a repeat of what the old one was told.
357
+ * capabilities, and the focus latch. All of it goes together, atomically.
358
+ * Splitting them is how a client ends up paging against a server that never
359
+ * declared it, bursting into a depleted server bucket, or holding a focus report
360
+ * back from the new socket as a repeat of what the old one was told.
369
361
  */
370
362
  function resetForNewSocket(): void {
371
363
  clearHistoryTimers();
372
364
  history = newHistoryState();
373
365
  caps = { ephemeralInput: false, serverFocus: false };
374
366
  focusReported = null;
375
- lastFocusReportingSeen = false;
376
367
  }
377
368
 
378
369
  /**
@@ -727,35 +718,29 @@ export function sendEphemeral(text: string): boolean {
727
718
  * while no socket is live is dropped rather than queued, and a reconnect
728
719
  * re-asserts the current one. Where the server declared `serverFocus` this sends
729
720
  * the `focus` control and the server derives the DEC 1004 answer for the whole
730
- * session; otherwise the client writes the legacy CSI I / CSI O itself, and only
731
- * while the application has 1004 enabled.
721
+ * session; where it did not, the report is dropped.
732
722
  */
733
723
  export function setClientFocus(focused: boolean): void {
734
724
  clientFocused = focused;
735
725
  if (connState.status !== "connected") {
736
- // A focus report is never QUEUED. The fallback path below writes through the
737
- // RELIABLE outbox, so a report made while the socket is down would be
738
- // retransmitted on reconnect and re-assert a focus the widget may since have
739
- // lost, which is the replay this whole derivation exists to remove. Nothing
740
- // is lost by dropping it: both the resumeAck and the 1004 enable edge
741
- // re-report the current value on the new socket.
726
+ // Focus is STATE, so a report with no socket to carry it is DROPPED rather
727
+ // than held: the widget's value may change again before the next socket
728
+ // exists, and a held report would assert a focus it may since have lost.
729
+ // Nothing is lost by dropping it — the resumeAck re-reports the current
730
+ // value on the new socket.
742
731
  return;
743
732
  }
744
- if (focused === focusReported) {
733
+ if (!caps.serverFocus || !connState.upgraded) {
734
+ // A server that does not derive the answer gets NO report, because the client
735
+ // never writes the DEC 1004 bytes itself. Nothing is lost by dropping it: the
736
+ // resumeAck re-reports the current value once the capabilities are known.
745
737
  return;
746
738
  }
747
- focusReported = focused;
748
- if (caps.serverFocus && connState.upgraded) {
749
- sendControl({ type: "focus", focused });
750
- return;
751
- }
752
- if (!modes.isFocusReporting()) {
739
+ if (focused === focusReported) {
753
740
  return;
754
741
  }
755
- // The legacy path, and it rides the RELIABLE outbox: a focus report can be
756
- // retransmitted after a blip, re-asserting a state that may no longer be true.
757
- // That is one of the things the server-owned path fixes.
758
- sendBinary(ephemeralEncoder.encode(focused ? "\x1b[I" : "\x1b[O"));
742
+ focusReported = focused;
743
+ sendControl({ type: "focus", focused });
759
744
  }
760
745
 
761
746
  function sendControl(msg: ControlMessage): void {
@@ -1766,18 +1751,6 @@ export function connect(): void {
1766
1751
  };
1767
1752
  st.modes = snap;
1768
1753
  modes.applySnapshot(snap);
1769
- if (!caps.serverFocus) {
1770
- // The 1004 ENABLE edge on the fallback path. An application that enables
1771
- // focus reporting while the tab is already focused otherwise gets nothing
1772
- // until the user clicks away; xterm.js's _reportFocus reports the current
1773
- // state at that instant, and this client did not.
1774
- const rising = msg.focusReporting && !lastFocusReportingSeen;
1775
- lastFocusReportingSeen = msg.focusReporting;
1776
- if (rising) {
1777
- focusReported = null;
1778
- setClientFocus(clientFocused);
1779
- }
1780
- }
1781
1754
  if (typeof msg.inputAck === "number") {
1782
1755
  applyAck(st, msg.inputAck);
1783
1756
  }
package/src/modes.ts CHANGED
@@ -11,7 +11,6 @@
11
11
  // - paste handling: wraps in \e[200~..\e[201~ only when
12
12
  // bracketedPaste is on; otherwise sends raw text.
13
13
  // - mouse.ts: encodes mouse events only when mouseMode > 0.
14
- // - focus reporting: sends ESC[I / ESC[O when focusReporting is on.
15
14
  //
16
15
  // Defaults: bracketedPaste starts true because most modern shells
17
16
  // (bash 4.4+, zsh, fish) enable it immediately on startup via
package/src/types.ts CHANGED
@@ -122,10 +122,12 @@ export interface ResumeAckMessage {
122
122
  /**
123
123
  * The server DECLARES that it owns DEC 1004 focus for this session: it derives
124
124
  * the answer from attachment state, every attached client's reported focus and
125
- * its own keep-unfocused declaration, and honors the `focus` control. Report
126
- * the terminal widget's focus with `connection.setClientFocus` and write no
127
- * focus bytes. Absent (or false) means the server does not, and the client
128
- * writes CSI I / CSI O itself while the application has 1004 enabled.
125
+ * its own keep-unfocused declaration, and honors the `focus` control. Absent
126
+ * (or false) means the server does not, and the report is DROPPED — the client
127
+ * writes no focus bytes in either case, because one client cannot answer for a
128
+ * session two devices may be attached to and a client-written CSI I would
129
+ * override a keep-unfocused policy it cannot see. Report the terminal widget's
130
+ * focus with `connection.setClientFocus` regardless of this bit.
129
131
  */
130
132
  serverFocus?: boolean;
131
133
  /**