@cplieger/web-terminal-ui 7.3.2 → 8.0.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.
Files changed (48) hide show
  1. package/README.md +114 -18
  2. package/css/30-tabs.css +21 -0
  3. package/css/31-switcher.css +15 -0
  4. package/css/32-split.css +176 -0
  5. package/css/MANIFEST +1 -0
  6. package/css/MANIFEST.tabbed +1 -0
  7. package/package.json +13 -12
  8. package/src/composition.ts +165 -237
  9. package/src/features/activity-monitor.ts +20 -55
  10. package/src/features/animations.ts +12 -30
  11. package/src/features/clipboard.ts +36 -64
  12. package/src/features/connection-banner.ts +10 -19
  13. package/src/features/context-menu.ts +70 -145
  14. package/src/features/dom.ts +44 -54
  15. package/src/features/menu-position.ts +24 -28
  16. package/src/features/mobile-toolbar.ts +32 -62
  17. package/src/features/predictive-echo.ts +15 -49
  18. package/src/features/scroll-to-bottom.ts +12 -24
  19. package/src/features/tabs/index.ts +1398 -1806
  20. package/src/features/tabs/ink-centre.ts +45 -54
  21. package/src/features/tabs/model.ts +301 -383
  22. package/src/features/tabs/strip.ts +45 -97
  23. package/src/features/tabs/switcher.ts +70 -48
  24. package/src/index.ts +13 -13
  25. package/src/kernel/a11y.ts +106 -27
  26. package/src/kernel/attention.ts +157 -0
  27. package/src/kernel/conn-state.ts +28 -22
  28. package/src/kernel/fatal.ts +76 -0
  29. package/src/kernel/feature-host.ts +136 -0
  30. package/src/kernel/gesture.ts +9 -13
  31. package/src/kernel/kernel.ts +57 -2139
  32. package/src/kernel/layout-policy.ts +44 -0
  33. package/src/kernel/loading-status.ts +30 -88
  34. package/src/kernel/navigation.ts +11 -17
  35. package/src/kernel/notify.ts +158 -0
  36. package/src/kernel/pane.ts +1415 -0
  37. package/src/kernel/realm.ts +11 -0
  38. package/src/kernel/regions.ts +8 -11
  39. package/src/kernel/scrollback.ts +88 -168
  40. package/src/kernel/selection.ts +25 -0
  41. package/src/kernel/shell.ts +1434 -0
  42. package/src/kernel/split-handle.ts +280 -0
  43. package/src/kernel/status-share.ts +100 -0
  44. package/src/kernel/types.ts +457 -441
  45. package/src/predict.ts +144 -170
  46. package/src/viewport.ts +124 -197
  47. package/src/features/tabs/attention.ts +0 -250
  48. package/src/features/tabs/notify.ts +0 -265
package/src/predict.ts CHANGED
@@ -1,181 +1,155 @@
1
- // Predictive local echo (INPUT-01) — Mosh-style mini-VT that advances
2
- // a "predicted cursor" optimistically as the user types, before the
3
- // server's next screen frame confirms or contradicts the prediction.
4
- //
5
- // On the wire we still send everything through the connection module;
6
- // this is purely a local-render optimisation. The visible effect is
7
- // that the cursor moves immediately on each keystroke even on a 200 ms
8
- // link, so typing feels responsive instead of laggy.
9
- //
10
- // Scope (deliberately conservative — wrong predictions are worse than
11
- // missing predictions, since they look like the terminal is broken):
12
- // - printable ASCII / UTF-8 codepoints: advance one cell per
13
- // codepoint (approximate for fullwidth / combining marks; snaps
14
- // to truth on next server frame).
15
- // - Backspace (0x08) and DEL (0x7f): cursor back one cell.
16
- // - CR (0x0d): col = 0.
17
- // - LF (0x0a): row += 1 (clamped to height-1).
18
- // - Anything else (ESC sequences, TAB, other C0): suspend prediction
19
- // until the next server frame. Suspending hides the predicted
20
- // cursor so the user sees only the server's truth.
21
- //
22
- // Each server screen frame resets the predicted cursor to the server's
23
- // reported position and re-arms prediction. So a wrong prediction is
24
- // at most one flushInterval (50 ms) old before correction.
1
+ // Predictive local echo: a Mosh-style mini-VT that advances a predicted cursor
2
+ // as the user types, before the server's next screen frame confirms it. Purely a
3
+ // local-render optimisation, deliberately conservative because a wrong
4
+ // prediction looks like a broken terminal: printable codepoints advance one
5
+ // cell, BS/DEL back one, CR to col 0, LF down one row; anything else (ESC, TAB,
6
+ // other C0) suspends prediction until the next server frame, which resets the
7
+ // predicted cursor to the server's position and re-arms it.
25
8
 
26
- let predRow = 0;
27
- let predCol = 0;
28
- let predActive = false;
29
- let predPendingWrap = false; // matches server VT pendingWrap semantics
30
- let predFrozen = false; // true when server cursor is hidden (selection prompts)
31
- let cols = 80;
32
- let rows = 30;
33
-
34
- let onChange: (() => void) | null = null;
35
-
36
- /** Subscribe to predicted-cursor changes (the renderer registers
37
- * this so it can move its predicted-cursor overlay). */
38
- export function subscribe(cb: () => void): void {
39
- onChange = cb;
9
+ /** One pane's predicted cursor. */
10
+ export interface Predictor {
11
+ /** Subscribe to predicted-cursor changes; one subscriber (the overlay). */
12
+ subscribe(cb: () => void): void;
13
+ /** The screen size the wrap arithmetic uses; the predicted position is
14
+ * clamped into it. */
15
+ setDimensions(cols: number, rows: number): void;
16
+ /** Drop every prediction and hide the overlay (a server restart, a switch). */
17
+ reset(): void;
18
+ /** Reset to the server's reported cursor and re-arm, unless the server cursor
19
+ * is hidden (a selection prompt), which freezes prediction. */
20
+ onScreenFrame(serverRow: number, serverCol: number, cursorHidden?: boolean): void;
21
+ /** Apply locally-typed bytes; suspends on the first byte it cannot model. */
22
+ applyInput(bytes: Uint8Array): void;
23
+ /** `active` is false while prediction is suspended. */
24
+ get(): { row: number; col: number; active: boolean };
25
+ /** Clear the subscriber and the state. */
26
+ dispose(): void;
40
27
  }
41
28
 
42
- /** Update the screen dimensions used for wrap calculations. Should be
43
- * called whenever the renderer learns a new size from the server. */
44
- export function setDimensions(c: number, r: number): void {
45
- if (c > 0) {
46
- cols = c;
47
- }
48
- if (r > 0) {
49
- rows = r;
50
- }
51
- const prevCol = predCol;
52
- const prevRow = predRow;
53
- // Clamp predicted position to new bounds so a resize mid-typing
54
- // doesn't leave predCol/predRow past the edge.
55
- if (predCol >= cols) {
56
- predCol = cols - 1;
57
- }
58
- if (predRow >= rows) {
59
- predRow = rows - 1;
60
- }
61
- // Notify subscribers when the clamp moved the predicted cursor, so the
62
- // renderer's overlay doesn't show a stale (possibly off-screen) position
63
- // until the next server frame arrives.
64
- if (predCol !== prevCol || predRow !== prevRow) {
29
+ export function createPredictor(): Predictor {
30
+ let predRow = 0;
31
+ let predCol = 0;
32
+ let predActive = false;
33
+ let predPendingWrap = false;
34
+ let predFrozen = false;
35
+ let cols = 80;
36
+ let rows = 30;
37
+ let onChange: (() => void) | null = null;
38
+
39
+ function reset(): void {
40
+ predRow = 0;
41
+ predCol = 0;
42
+ predActive = false;
43
+ predPendingWrap = false;
44
+ predFrozen = false;
65
45
  onChange?.();
66
46
  }
67
- }
68
-
69
- /** Reset all prediction state. Called on server restart to avoid
70
- * stale cursor overlay from the previous session. */
71
- export function reset(): void {
72
- predRow = 0;
73
- predCol = 0;
74
- predActive = false;
75
- predPendingWrap = false;
76
- predFrozen = false;
77
- onChange?.();
78
- }
79
47
 
80
- /** Reset prediction to the server's reported cursor and re-arm.
81
- * When the server cursor is hidden (e.g. during a selection prompt),
82
- * prediction is frozen — the ghost cursor disappears and typed input
83
- * doesn't move it. */
84
- export function onScreenFrame(serverRow: number, serverCol: number, cursorHidden?: boolean): void {
85
- predRow = serverRow;
86
- predCol = serverCol;
87
- predPendingWrap = false;
88
- predFrozen = cursorHidden ?? false;
89
- predActive = !predFrozen;
90
- onChange?.();
91
- }
92
-
93
- /** Apply locally-typed input bytes to the predicted cursor. Bails out
94
- * (suspending prediction) on the first byte we can't model. Also
95
- * bails when frozen (cursor hidden — user is in a selection prompt). */
96
- export function applyInput(bytes: Uint8Array): void {
97
- if (!predActive || predFrozen) {
98
- return;
99
- }
100
- let i = 0;
101
- while (i < bytes.length) {
102
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- index checked by loop condition
103
- const b = bytes[i]!;
104
- if (b === 0x1b) {
105
- // ESC — start of an escape sequence. Don't try to model it.
106
- predActive = false;
107
- onChange?.();
108
- return;
109
- }
110
- if (b === 0x08 || b === 0x7f) {
111
- // BS / DEL — backspace one cell.
112
- predPendingWrap = false;
113
- if (predCol > 0) {
114
- predCol--;
115
- } else if (predRow > 0) {
116
- // wrap to end of previous row (terminals typically don't,
117
- // but kiro-cli's prompt frequently does on Backspace at col
118
- // 0, so this is closer to expected behavior than staying put)
119
- predRow--;
48
+ return {
49
+ subscribe(cb) {
50
+ onChange = cb;
51
+ },
52
+ setDimensions(c, r) {
53
+ if (c > 0) {
54
+ cols = c;
55
+ }
56
+ if (r > 0) {
57
+ rows = r;
58
+ }
59
+ const prevCol = predCol;
60
+ const prevRow = predRow;
61
+ if (predCol >= cols) {
120
62
  predCol = cols - 1;
121
63
  }
122
- i++;
123
- continue;
124
- }
125
- if (b === 0x0d) {
126
- // CR
127
- predPendingWrap = false;
128
- predCol = 0;
129
- i++;
130
- continue;
131
- }
132
- if (b === 0x0a) {
133
- // LF
64
+ if (predRow >= rows) {
65
+ predRow = rows - 1;
66
+ }
67
+ if (predCol !== prevCol || predRow !== prevRow) {
68
+ onChange?.();
69
+ }
70
+ },
71
+ reset,
72
+ onScreenFrame(serverRow, serverCol, cursorHidden) {
73
+ predRow = serverRow;
74
+ predCol = serverCol;
134
75
  predPendingWrap = false;
135
- predRow = Math.min(predRow + 1, rows - 1);
136
- i++;
137
- continue;
138
- }
139
- if (b < 0x20) {
140
- // Other C0 — probably interpreted by the application; bail.
141
- predActive = false;
76
+ predFrozen = cursorHidden ?? false;
77
+ predActive = !predFrozen;
142
78
  onChange?.();
143
- return;
144
- }
145
- // Printable ASCII or UTF-8 lead byte. Detect the codepoint length
146
- // and advance one cell. Mirror the server's pendingWrap semantics
147
- // so the predicted cursor matches the server VT exactly: when
148
- // pendingWrap is set we wrap to (row+1, 0) BEFORE writing, then
149
- // record pendingWrap if the new col is the last one.
150
- let len = 1;
151
- if (b >= 0xc0 && b < 0xe0) {
152
- len = 2;
153
- } else if (b >= 0xe0 && b < 0xf0) {
154
- len = 3;
155
- } else if (b >= 0xf0) {
156
- len = 4;
157
- }
158
- i += len;
159
- if (predPendingWrap) {
160
- predCol = 0;
161
- predRow = Math.min(predRow + 1, rows - 1);
162
- predPendingWrap = false;
163
- }
164
- if (predCol >= cols - 1) {
165
- // Last column: don't advance, set pendingWrap for the next char.
166
- predCol = cols - 1;
167
- predPendingWrap = true;
168
- } else {
169
- predCol++;
170
- }
171
- }
172
- onChange?.();
173
- }
174
-
175
- /** Current predicted cursor state. `active` is false when prediction
176
- * is suspended (e.g. after an unmodelled byte). When the predicted
177
- * position equals the server's position, the renderer should hide
178
- * the overlay so the user sees only one cursor. */
179
- export function get(): { row: number; col: number; active: boolean } {
180
- return { row: predRow, col: predCol, active: predActive };
79
+ },
80
+ applyInput(bytes) {
81
+ if (!predActive || predFrozen) {
82
+ return;
83
+ }
84
+ let i = 0;
85
+ while (i < bytes.length) {
86
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- index checked by loop condition
87
+ const b = bytes[i]!;
88
+ if (b === 0x1b) {
89
+ predActive = false;
90
+ onChange?.();
91
+ return;
92
+ }
93
+ if (b === 0x08 || b === 0x7f) {
94
+ predPendingWrap = false;
95
+ if (predCol > 0) {
96
+ predCol--;
97
+ } else if (predRow > 0) {
98
+ // kiro-cli's prompt wraps a Backspace at col 0 to the previous row.
99
+ predRow--;
100
+ predCol = cols - 1;
101
+ }
102
+ i++;
103
+ continue;
104
+ }
105
+ if (b === 0x0d) {
106
+ predPendingWrap = false;
107
+ predCol = 0;
108
+ i++;
109
+ continue;
110
+ }
111
+ if (b === 0x0a) {
112
+ predPendingWrap = false;
113
+ predRow = Math.min(predRow + 1, rows - 1);
114
+ i++;
115
+ continue;
116
+ }
117
+ if (b < 0x20) {
118
+ predActive = false;
119
+ onChange?.();
120
+ return;
121
+ }
122
+ // Mirror the server VT's pendingWrap: at the last column stay put and
123
+ // wrap before the NEXT character.
124
+ let len = 1;
125
+ if (b >= 0xc0 && b < 0xe0) {
126
+ len = 2;
127
+ } else if (b >= 0xe0 && b < 0xf0) {
128
+ len = 3;
129
+ } else if (b >= 0xf0) {
130
+ len = 4;
131
+ }
132
+ i += len;
133
+ if (predPendingWrap) {
134
+ predCol = 0;
135
+ predRow = Math.min(predRow + 1, rows - 1);
136
+ predPendingWrap = false;
137
+ }
138
+ if (predCol >= cols - 1) {
139
+ predCol = cols - 1;
140
+ predPendingWrap = true;
141
+ } else {
142
+ predCol++;
143
+ }
144
+ }
145
+ onChange?.();
146
+ },
147
+ get() {
148
+ return { row: predRow, col: predCol, active: predActive };
149
+ },
150
+ dispose() {
151
+ onChange = null;
152
+ reset();
153
+ },
154
+ };
181
155
  }