@cplieger/web-terminal-engine 5.1.0 → 5.2.0
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 +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/connection.ts +17 -44
- package/src/modes.ts +0 -1
- package/src/status-stream.ts +15 -0
- package/src/types.ts +6 -4
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
|
|
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
package/package.json
CHANGED
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
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
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;
|
|
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
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
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 (
|
|
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
|
|
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
|
-
|
|
756
|
-
|
|
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/status-stream.ts
CHANGED
|
@@ -74,6 +74,21 @@ export interface SessionInfo {
|
|
|
74
74
|
* only when this is set; a program that emits no OSC 9 signal (a plain shell)
|
|
75
75
|
* keeps its tab dot hidden. */
|
|
76
76
|
readonly reportsActivity?: boolean;
|
|
77
|
+
/** the session's SECONDARY activity: a host-reported background activity that
|
|
78
|
+
* OUTLIVES the turn, carried beside `status` and never merged into it. The
|
|
79
|
+
* closed set is `"working"` (a background task is running), `"waiting"`
|
|
80
|
+
* (stopped and resumable, nobody is being asked) and `"input"` (a background
|
|
81
|
+
* task is blocked on the user); absent or an empty string means no secondary
|
|
82
|
+
* activity, and a consumer must tolerate an UNKNOWN value from a newer server
|
|
83
|
+
* by rendering nothing for it.
|
|
84
|
+
*
|
|
85
|
+
* Typed `string`, deliberately NOT a union like `status`: that union's own doc
|
|
86
|
+
* already admits it must tolerate values outside itself, which forces a cast at
|
|
87
|
+
* every forward-compatible site. Absent from a server that reports none. */
|
|
88
|
+
readonly activity?: string;
|
|
89
|
+
/** how many sources produced `activity`: >= 1 whenever `activity` is non-empty,
|
|
90
|
+
* 0 when it is empty or the host does not count them. */
|
|
91
|
+
readonly activityCount?: number;
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
/** SessionStatus is one session's current status as carried on the status
|
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.
|
|
126
|
-
*
|
|
127
|
-
* focus bytes
|
|
128
|
-
*
|
|
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
|
/**
|