@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.
- package/README.md +114 -18
- package/css/30-tabs.css +21 -0
- package/css/31-switcher.css +15 -0
- package/css/32-split.css +176 -0
- package/css/MANIFEST +1 -0
- package/css/MANIFEST.tabbed +1 -0
- package/package.json +13 -12
- package/src/composition.ts +165 -237
- package/src/features/activity-monitor.ts +20 -55
- package/src/features/animations.ts +12 -30
- package/src/features/clipboard.ts +36 -64
- package/src/features/connection-banner.ts +10 -19
- package/src/features/context-menu.ts +70 -145
- package/src/features/dom.ts +44 -54
- package/src/features/menu-position.ts +24 -28
- package/src/features/mobile-toolbar.ts +32 -62
- package/src/features/predictive-echo.ts +15 -49
- package/src/features/scroll-to-bottom.ts +12 -24
- package/src/features/tabs/index.ts +1398 -1806
- package/src/features/tabs/ink-centre.ts +45 -54
- package/src/features/tabs/model.ts +301 -383
- package/src/features/tabs/strip.ts +45 -97
- package/src/features/tabs/switcher.ts +70 -48
- package/src/index.ts +13 -13
- package/src/kernel/a11y.ts +106 -27
- package/src/kernel/attention.ts +157 -0
- package/src/kernel/conn-state.ts +28 -22
- package/src/kernel/fatal.ts +76 -0
- package/src/kernel/feature-host.ts +136 -0
- package/src/kernel/gesture.ts +9 -13
- package/src/kernel/kernel.ts +57 -2139
- package/src/kernel/layout-policy.ts +44 -0
- package/src/kernel/loading-status.ts +30 -88
- package/src/kernel/navigation.ts +11 -17
- package/src/kernel/notify.ts +158 -0
- package/src/kernel/pane.ts +1415 -0
- package/src/kernel/realm.ts +11 -0
- package/src/kernel/regions.ts +8 -11
- package/src/kernel/scrollback.ts +88 -168
- package/src/kernel/selection.ts +25 -0
- package/src/kernel/shell.ts +1434 -0
- package/src/kernel/split-handle.ts +280 -0
- package/src/kernel/status-share.ts +100 -0
- package/src/kernel/types.ts +457 -441
- package/src/predict.ts +144 -170
- package/src/viewport.ts +124 -197
- package/src/features/tabs/attention.ts +0 -250
- package/src/features/tabs/notify.ts +0 -265
package/src/predict.ts
CHANGED
|
@@ -1,181 +1,155 @@
|
|
|
1
|
-
// Predictive local echo
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
}
|