@cplieger/web-terminal-ui 7.0.3 → 7.0.5
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/css/30-tabs.css +0 -21
- package/package.json +9 -7
- package/src/composition.ts +69 -1
- package/src/features/context-menu.ts +10 -0
- package/src/features/tabs/attention.ts +19 -4
- package/src/features/tabs/index.ts +4 -5
- package/src/features/tabs/ink-centre.ts +3 -3
- package/src/features/tabs/notify.ts +1 -1
- package/src/features/tabs/switch-anim.ts +1 -1
- package/src/kernel/kernel.ts +14 -5
- package/src/kernel/navigation.ts +19 -0
- package/src/kernel/style-contract.ts +1 -1
- package/src/presets/tabbed.ts +14 -18
package/css/30-tabs.css
CHANGED
|
@@ -355,27 +355,6 @@
|
|
|
355
355
|
color: var(--text);
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
/* On an inactive tab the close appears only on hover, kept in the layout via
|
|
359
|
-
visibility so the tab width never jumps; on the active tab it is always shown. */
|
|
360
|
-
:where(.wt-root) .wt-tab:not(.wt-tab-active) .wt-tab-close {
|
|
361
|
-
visibility: hidden;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
:where(.wt-root) .wt-tab:not(.wt-tab-active):hover .wt-tab-close {
|
|
365
|
-
visibility: visible;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/* Coarse pointer (an iPad using the strip): no hover fires, so an inactive tab's close
|
|
369
|
-
would be unreachable without first activating the tab. Reveal every tab's close there,
|
|
370
|
-
matching the always-visible mobile switcher closes. No narrow gate needed: on the
|
|
371
|
-
narrow-coarse phone the whole strip is display:none (31-switcher.css), so this rule
|
|
372
|
-
is moot there. */
|
|
373
|
-
@media (pointer: coarse) {
|
|
374
|
-
:where(.wt-root) .wt-tab:not(.wt-tab-active) .wt-tab-close {
|
|
375
|
-
visibility: visible;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
358
|
/* The add-tab (+) button: a fixed bar item OUTSIDE the tab scroller (it never
|
|
380
359
|
scrolls away with an overflowing list; the shrink-wrapped scroller keeps it
|
|
381
360
|
visually trailing the last tab while the tabs fit — see .wt-tab-scroll). Its
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -50,19 +50,21 @@
|
|
|
50
50
|
"@stryker-mutator/core": "10.0.0",
|
|
51
51
|
"@stryker-mutator/typescript-checker": "10.0.0",
|
|
52
52
|
"@stryker-mutator/vitest-runner": "10.0.0",
|
|
53
|
-
"@types/node": "26.
|
|
53
|
+
"@types/node": "26.4.1",
|
|
54
54
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
55
|
+
"@vitest/browser": "4.1.11",
|
|
56
|
+
"@vitest/browser-playwright": "4.1.11",
|
|
55
57
|
"@vitest/coverage-v8": "4.1.11",
|
|
56
|
-
"eslint": "10.
|
|
58
|
+
"eslint": "10.9.1",
|
|
57
59
|
"fast-check": "4.9.0",
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
60
|
+
"html-validate": "11.12.0",
|
|
61
|
+
"knip": "6.34.0",
|
|
62
|
+
"playwright": "1.62.1",
|
|
61
63
|
"prettier": "3.9.6",
|
|
62
64
|
"stylelint": "17.14.1",
|
|
63
65
|
"stylelint-config-standard": "40.0.0",
|
|
64
66
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
65
|
-
"typescript-eslint": "8.
|
|
67
|
+
"typescript-eslint": "8.69.0",
|
|
66
68
|
"vitest": "4.1.11"
|
|
67
69
|
},
|
|
68
70
|
"publishConfig": {
|
package/src/composition.ts
CHANGED
|
@@ -39,6 +39,31 @@ let sendingComposition = false;
|
|
|
39
39
|
let compositionStart = 0;
|
|
40
40
|
let compositionSuffix = "";
|
|
41
41
|
|
|
42
|
+
// A soft keyboard may announce its composition only at COMMIT time: the
|
|
43
|
+
// per-character `input` events arrive with no composition in progress, the
|
|
44
|
+
// kernel sends each keystroke as typed, and then the keyboard fires
|
|
45
|
+
// compositionstart + compositionend back-to-back on the committing key while
|
|
46
|
+
// writing the WHOLE accumulated word into the textarea. The deferred read below
|
|
47
|
+
// then sends that word a second time — reported on Android Chrome with
|
|
48
|
+
// SwiftKey, where typing "hello" and pressing space produced "hellohello ".
|
|
49
|
+
//
|
|
50
|
+
// The terminal has already been given the literal keystrokes and cannot be
|
|
51
|
+
// asked to retract them, so a commit that only repeats them is a no-op.
|
|
52
|
+
// `noteDirectSend` records what the kernel sent with no composition running;
|
|
53
|
+
// the finaliser drops a commit that matches it.
|
|
54
|
+
//
|
|
55
|
+
// Unreachable on a keyboard that announces its composition BEFORE the
|
|
56
|
+
// keystrokes (every desktop IME, iOS, Android's own keyboard): there the
|
|
57
|
+
// kernel's `input` listener returns early while composing, so nothing is
|
|
58
|
+
// recorded and there is nothing for a commit to match.
|
|
59
|
+
const ECHO_TAIL_MAX = 64;
|
|
60
|
+
const ECHO_WINDOW_MS = 1500;
|
|
61
|
+
const ECHO_MAX_COMPOSE_MS = 60;
|
|
62
|
+
|
|
63
|
+
let directTail = "";
|
|
64
|
+
let directTailAt = 0;
|
|
65
|
+
let compositionStartedAt: number | null = null;
|
|
66
|
+
|
|
42
67
|
export function init(opts: {
|
|
43
68
|
textarea: HTMLTextAreaElement;
|
|
44
69
|
compositionView: HTMLElement;
|
|
@@ -67,6 +92,42 @@ export function isComposing(): boolean {
|
|
|
67
92
|
return composing || sendingComposition;
|
|
68
93
|
}
|
|
69
94
|
|
|
95
|
+
/** Record text the kernel put on the wire with no composition running, so a
|
|
96
|
+
* keyboard that announces its composition only at commit time cannot have the
|
|
97
|
+
* same keystrokes delivered twice (see the ECHO_* note above). Typed text
|
|
98
|
+
* only — a paste is never re-committed by a keyboard. */
|
|
99
|
+
export function noteDirectSend(text: string): void {
|
|
100
|
+
if (text === "") {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
if (now - directTailAt > ECHO_WINDOW_MS) {
|
|
105
|
+
directTail = "";
|
|
106
|
+
}
|
|
107
|
+
directTail = (directTail + text).slice(-ECHO_TAIL_MAX);
|
|
108
|
+
directTailAt = now;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Whether this commit merely repeats keystrokes already delivered literally. */
|
|
112
|
+
function repeatsDirectSends(composed: string, startedAt: number | null, endedAt: number): boolean {
|
|
113
|
+
if (directTail === "") {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
// A composition the user typed INTO spans human time; a commit-time
|
|
117
|
+
// announcement starts and ends inside one keystroke.
|
|
118
|
+
if (startedAt !== null && endedAt - startedAt > ECHO_MAX_COMPOSE_MS) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
// Only the burst still being typed can be a re-commit of itself.
|
|
122
|
+
if (endedAt - directTailAt > ECHO_WINDOW_MS) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
// The committing key (space, tab) is delivered as its own `input` event, so
|
|
126
|
+
// what has to match is the WORD the keyboard is re-committing.
|
|
127
|
+
const word = composed.replace(/[ \t]+$/u, "");
|
|
128
|
+
return word !== "" && directTail.endsWith(word);
|
|
129
|
+
}
|
|
130
|
+
|
|
70
131
|
/** Abort any in-flight composition without sending, and clear the textarea.
|
|
71
132
|
* The kernel calls this on a tab switch (detach): committing half-composed IME
|
|
72
133
|
* text to either the outgoing or the incoming session is wrong, so we discard
|
|
@@ -77,6 +138,8 @@ export function isComposing(): boolean {
|
|
|
77
138
|
export function cancelComposition(): void {
|
|
78
139
|
composing = false;
|
|
79
140
|
sendingComposition = false;
|
|
141
|
+
compositionStartedAt = null;
|
|
142
|
+
directTail = "";
|
|
80
143
|
compositionView.textContent = "";
|
|
81
144
|
compositionView.classList.remove("active");
|
|
82
145
|
resetToPlaceholder(textarea);
|
|
@@ -104,6 +167,7 @@ export function teardown(): void {
|
|
|
104
167
|
|
|
105
168
|
function onStart(): void {
|
|
106
169
|
composing = true;
|
|
170
|
+
compositionStartedAt = Date.now();
|
|
107
171
|
const start = textarea.selectionStart;
|
|
108
172
|
const end = textarea.selectionEnd;
|
|
109
173
|
compositionStart = Math.min(start, end);
|
|
@@ -131,6 +195,9 @@ function onEnd(): void {
|
|
|
131
195
|
sendingComposition = true;
|
|
132
196
|
const startSnapshot = compositionStart;
|
|
133
197
|
const suffixSnapshot = compositionSuffix;
|
|
198
|
+
const startedAt = compositionStartedAt;
|
|
199
|
+
const endedAt = Date.now();
|
|
200
|
+
compositionStartedAt = null;
|
|
134
201
|
setTimeout(() => {
|
|
135
202
|
if (!sendingComposition) {
|
|
136
203
|
return;
|
|
@@ -144,9 +211,10 @@ function onEnd(): void {
|
|
|
144
211
|
const composed = value
|
|
145
212
|
.substring(startSnapshot, Math.max(startSnapshot, valueEnd))
|
|
146
213
|
.replace(/\u00A0/g, " ");
|
|
147
|
-
if (composed.length > 0) {
|
|
214
|
+
if (composed.length > 0 && !repeatsDirectSends(composed, startedAt, endedAt)) {
|
|
148
215
|
send(composed);
|
|
149
216
|
}
|
|
217
|
+
directTail = "";
|
|
150
218
|
resetToPlaceholder(textarea);
|
|
151
219
|
}, 0);
|
|
152
220
|
}
|
|
@@ -75,6 +75,16 @@ export interface ContextMenuOptions {
|
|
|
75
75
|
* opens, so a wrong answer here costs at most a duplicated menu on an unusual
|
|
76
76
|
* device, never a broken selection and never a lost paste. */
|
|
77
77
|
function isAppleTouchDevice(): boolean {
|
|
78
|
+
// Dead in every runtime that reaches this, and left in place deliberately.
|
|
79
|
+
// Measured on this repo's Node (v24.18.0, the version ts-ci pins): `typeof
|
|
80
|
+
// navigator` is "object", with userAgent "Node.js/24", platform
|
|
81
|
+
// "Linux x86_64" and maxTouchPoints undefined — so the fall-through already
|
|
82
|
+
// answers false there, by the same route a browser on a non-Apple device
|
|
83
|
+
// takes. A Web Worker has a WorkerNavigator, so it does not reach this
|
|
84
|
+
// either. What the guard still buys is totality: without it the bare
|
|
85
|
+
// `navigator` reads below would throw a ReferenceError in a runtime that has
|
|
86
|
+
// no navigator at all. It answers the same `false` the fall-through would, so
|
|
87
|
+
// nothing depends on reaching it, and no test can reach it without stubbing.
|
|
78
88
|
if (typeof navigator === "undefined") {
|
|
79
89
|
return false;
|
|
80
90
|
}
|
|
@@ -19,10 +19,25 @@
|
|
|
19
19
|
// the switcher's aggregate dot about what wants the user; the fold and the raise
|
|
20
20
|
// share isUnseenCue (model.ts).
|
|
21
21
|
//
|
|
22
|
-
// DOM-free
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
22
|
+
// DOM-free by construction, like notify.ts: every capability arrives through
|
|
23
|
+
// AttentionEnv, so the fold, the idempotence and the degradation paths are driven
|
|
24
|
+
// in the tests by plain recorder functions, and browserAttentionEnv() below is the
|
|
25
|
+
// only function here that reads a global (globalThis.navigator, document).
|
|
26
|
+
//
|
|
27
|
+
// That last clause is a reading of the source, and nothing enforces it — on
|
|
28
|
+
// purpose, not by omission. An ambient-absence test would assert the RUNTIME
|
|
29
|
+
// rather than this module: the only importer is tabs/index.ts, the node project
|
|
30
|
+
// holds one file and it reads stylesheets off disk, so nothing here loads this
|
|
31
|
+
// module outside a browser and no test can observe an absent `document` without
|
|
32
|
+
// passing for a third wrong reason (.kiro/steering/testing-ts.md,
|
|
33
|
+
// "Capability-absence is NOT a project selection"). The half that does ship IS
|
|
34
|
+
// enforced, and at the site: attention.test.ts shadows the whole `navigator`
|
|
35
|
+
// object and drives the real browserAttentionEnv through it. The mechanism that
|
|
36
|
+
// could enforce the load-time half is an AST scan of this file's top-level
|
|
37
|
+
// statements — vibekit carries one for its ported copy, where a node-project test
|
|
38
|
+
// statically imports the module and a module-scope global read really does break
|
|
39
|
+
// something. Copy it here the day anything needs this module to load without a
|
|
40
|
+
// document.
|
|
26
41
|
|
|
27
42
|
import { type CueStatus, cueIconName, isUnseenCue, worseCue } from "./model.js";
|
|
28
43
|
|
|
@@ -1007,8 +1007,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1007
1007
|
// sync, so a user browsing a scrolled strip is never yanked back by an
|
|
1008
1008
|
// unrelated repaint), the newly active chip is brought into view in the
|
|
1009
1009
|
// overflowed scroller; inline: "nearest" is a no-op when it is already
|
|
1010
|
-
// visible. The typeof guard covers
|
|
1011
|
-
// scrollIntoView.
|
|
1010
|
+
// visible. The typeof guard covers an environment without scrollIntoView.
|
|
1012
1011
|
let lastRevealedActive = "";
|
|
1013
1012
|
function paintActive(): void {
|
|
1014
1013
|
for (const t of tabList) {
|
|
@@ -2247,7 +2246,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2247
2246
|
}
|
|
2248
2247
|
}
|
|
2249
2248
|
if (invert.size === 0) {
|
|
2250
|
-
return; // nothing moved on screen (
|
|
2249
|
+
return; // nothing moved on screen (every box measured zero)
|
|
2251
2250
|
}
|
|
2252
2251
|
applyShift(invert, "none");
|
|
2253
2252
|
// The read forces the reflow that COMMITS the from-state. Without it the
|
|
@@ -2537,8 +2536,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2537
2536
|
// Reordering re-appends every chip, and reparenting the focused field blurs
|
|
2538
2537
|
// it — so an open edit would be committed as a side effect of moving some
|
|
2539
2538
|
// OTHER tab, via a blur the user never performed. Resolve it here instead,
|
|
2540
|
-
// so the outcome is chosen
|
|
2541
|
-
//
|
|
2539
|
+
// so the outcome is chosen rather than emergent: a blur that arrives as a
|
|
2540
|
+
// side effect of reparenting is not a decision anyone made.
|
|
2542
2541
|
resolveEdit("blur", false);
|
|
2543
2542
|
const from = tabList.findIndex((t) => t.id === id);
|
|
2544
2543
|
const to = from + delta;
|
|
@@ -249,9 +249,9 @@ export function centreChipLabels(
|
|
|
249
249
|
};
|
|
250
250
|
|
|
251
251
|
remeasure(false);
|
|
252
|
-
// The DOM lib types document.fonts as always present;
|
|
253
|
-
// FontFaceSet at all,
|
|
254
|
-
//
|
|
252
|
+
// The DOM lib types document.fonts as always present; an engine without the CSS
|
|
253
|
+
// Font Loading API ships no FontFaceSet at all, so the honest type is the
|
|
254
|
+
// optional one.
|
|
255
255
|
const fonts = document.fonts as FontFaceSet | undefined;
|
|
256
256
|
// `ready` settles the initial load (including a failed one); `loadingdone`
|
|
257
257
|
// covers a face the host page adds afterwards, which `ready` never reports.
|
|
@@ -48,7 +48,7 @@ export type NotificationCtorLike = new (
|
|
|
48
48
|
* not recognise must degrade rather than be asserted. */
|
|
49
49
|
export interface NotifierEnv {
|
|
50
50
|
/** The Notification constructor, or undefined where the API does not exist
|
|
51
|
-
* (iOS Safari outside an installed PWA, a non-secure context
|
|
51
|
+
* (iOS Safari outside an installed PWA, a non-secure context). */
|
|
52
52
|
ctor: NotificationCtorLike | undefined;
|
|
53
53
|
/** Current permission: "default" | "granted" | "denied" (or anything else). */
|
|
54
54
|
permission: () => string;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// on the 360 ms fallback timer instead of the animation's own end, and every test
|
|
11
11
|
// still passes because the tests dispatch the JS constant.
|
|
12
12
|
//
|
|
13
|
-
// Living here rather than inside the feature is what lets `css-contract.test.ts`
|
|
13
|
+
// Living here rather than inside the feature is what lets `css-contract.node.test.ts`
|
|
14
14
|
// import it and assert the CSS agrees. Same reason `input-placeholder.ts` and
|
|
15
15
|
// `kernel/gesture.ts` exist: a constant two places must keep in lockstep gets its
|
|
16
16
|
// own module. NOT part of the package's `exports` map, so it is internal and can
|
package/src/kernel/kernel.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { createConnState } from "./conn-state.js";
|
|
|
40
40
|
import { createScrollbackKeeper } from "./scrollback.js";
|
|
41
41
|
import { STARTUP_FAILURE_COPY } from "./startup-copy.js";
|
|
42
42
|
import { attachLoadingStatus, DEFAULT_LOADING_MESSAGES } from "./loading-status.js";
|
|
43
|
+
import { reloadPage } from "./navigation.js";
|
|
43
44
|
import type {
|
|
44
45
|
CreateTerminalOptions,
|
|
45
46
|
FeatureInstance,
|
|
@@ -184,7 +185,7 @@ function renderFatalStartupInto(
|
|
|
184
185
|
reloadButton.type = "button";
|
|
185
186
|
reloadButton.textContent = STARTUP_FAILURE_COPY.reloadLabel;
|
|
186
187
|
reloadButton.addEventListener("click", () => {
|
|
187
|
-
|
|
188
|
+
reloadPage();
|
|
188
189
|
});
|
|
189
190
|
card.append(title, messageEl, reloadButton);
|
|
190
191
|
surface.appendChild(card);
|
|
@@ -389,6 +390,14 @@ function buildTerminal(
|
|
|
389
390
|
function sendText(text: string): void {
|
|
390
391
|
sendBytes(encoder.encode(text));
|
|
391
392
|
}
|
|
393
|
+
/** A keystroke the user typed with no composition running. Recorded so a
|
|
394
|
+
* keyboard that announces its composition only at commit time cannot have
|
|
395
|
+
* the same characters delivered twice (composition.ts, the ECHO_* note). */
|
|
396
|
+
function sendTyped(text: string): void {
|
|
397
|
+
const out = normalizeTypedText(text);
|
|
398
|
+
sendText(out);
|
|
399
|
+
composition.noteDirectSend(out);
|
|
400
|
+
}
|
|
392
401
|
function paste(text: string): void {
|
|
393
402
|
// The one bracketed-paste + newline-normalize funnel; every feature and the
|
|
394
403
|
// kernel's own paste paths route through here (paste-jacking defense).
|
|
@@ -762,14 +771,14 @@ function buildTerminal(
|
|
|
762
771
|
paste(ev.data);
|
|
763
772
|
} else {
|
|
764
773
|
// Normalize iOS's NBSP-for-space quirk, then send through the funnel.
|
|
765
|
-
|
|
774
|
+
sendTyped(ev.data);
|
|
766
775
|
}
|
|
767
776
|
} else {
|
|
768
777
|
const v = input.value;
|
|
769
778
|
if (v.length > INPUT_PLACEHOLDER.length && v.startsWith(INPUT_PLACEHOLDER)) {
|
|
770
|
-
|
|
779
|
+
sendTyped(v.slice(INPUT_PLACEHOLDER.length));
|
|
771
780
|
} else if (v !== INPUT_PLACEHOLDER && v.length > 0) {
|
|
772
|
-
|
|
781
|
+
sendTyped(v);
|
|
773
782
|
}
|
|
774
783
|
}
|
|
775
784
|
resetToPlaceholder(input);
|
|
@@ -1170,7 +1179,7 @@ function buildTerminal(
|
|
|
1170
1179
|
// an uncanceled keydown's insertion target after a listener moved focus
|
|
1171
1180
|
// mid-dispatch. Chromium does; no specification requires it.
|
|
1172
1181
|
ev.preventDefault();
|
|
1173
|
-
|
|
1182
|
+
sendTyped(char);
|
|
1174
1183
|
return;
|
|
1175
1184
|
}
|
|
1176
1185
|
applyMappedKey(ev, result);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// navigation.ts — the library's one page-navigation call, behind its own module.
|
|
2
|
+
//
|
|
3
|
+
// `window.location.reload` cannot be substituted in a real browser: `reload` is
|
|
4
|
+
// an own, sealed property of the location instance (`configurable: false`,
|
|
5
|
+
// `writable: false`), `Location.prototype` carries no `reload` to spy on, and
|
|
6
|
+
// `window`, `globalThis.window` and `globalThis.location` are all
|
|
7
|
+
// non-configurable. So a test cannot redefine it, assign over it, or stub the
|
|
8
|
+
// global, and letting the real call run navigates the test runner's own frame
|
|
9
|
+
// and takes the whole run down with it.
|
|
10
|
+
//
|
|
11
|
+
// A separate module is the seam that works: a caller's import is a live binding
|
|
12
|
+
// the test framework can replace, which an intra-module call is not. Keeping the
|
|
13
|
+
// call here also means there is exactly one place in the library that navigates,
|
|
14
|
+
// so the next one has to be added deliberately.
|
|
15
|
+
|
|
16
|
+
/** Reload the current page. The only navigation this library performs. */
|
|
17
|
+
export function reloadPage(): void {
|
|
18
|
+
window.location.reload();
|
|
19
|
+
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// These lists are HAND-WRITTEN on purpose. They are the public contract — a
|
|
26
26
|
// decision about what this library promises to support — not a fact derived
|
|
27
27
|
// from the stylesheets. What IS derived from the stylesheets is the
|
|
28
|
-
// verification that the promise holds: `src/css-contract.test.ts` generates the
|
|
28
|
+
// verification that the promise holds: `src/css-contract.node.test.ts` generates the
|
|
29
29
|
// declared/referenced token inventory from the CSS itself and fails if any
|
|
30
30
|
// token named here is not both DECLARED and READ by a rule. A token that is
|
|
31
31
|
// declared but never read is the silent-no-op case above, reached from the
|
package/src/presets/tabbed.ts
CHANGED
|
@@ -10,6 +10,18 @@ import { activityMonitor } from "../features/activity-monitor.js";
|
|
|
10
10
|
import { animations } from "../features/animations.js";
|
|
11
11
|
import { presetSingle } from "./single.js";
|
|
12
12
|
|
|
13
|
+
/** Options shared by the two tabbed presets, passed through to the tabs feature.
|
|
14
|
+
* A preset that takes arguments is called as `features: () =>
|
|
15
|
+
* presetTabbed({...})`, which keeps the call inside createTerminal's failure
|
|
16
|
+
* boundary (see CreateTerminalOptions.features). */
|
|
17
|
+
export interface TabbedPresetOptions {
|
|
18
|
+
/** Swap the page's icon links to a status variant while a background session
|
|
19
|
+
* wants the user. Off by default, and enabling it is a promise that the
|
|
20
|
+
* variant assets are served — see TabsOptions.attentionIcons for the naming
|
|
21
|
+
* contract and .kiro/scripts/gen-attention-icons.py, which writes them. */
|
|
22
|
+
attentionIcons?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
/** buildTabbed composes the tabbed UI: the single-pane touch features, an
|
|
14
26
|
* externally-driven mobile toolbar (its grid is opened from a keyboard button
|
|
15
27
|
* in the tab bar, not its own toggle), the activity monitor (the status-SSE
|
|
@@ -27,18 +39,6 @@ import { presetSingle } from "./single.js";
|
|
|
27
39
|
* foreground-process/cwd inference — and both presets render what it reports. A
|
|
28
40
|
* browser that re-derived any of that could only disagree with the server and
|
|
29
41
|
* with every other client attached to the same session. */
|
|
30
|
-
/** Options shared by the two tabbed presets, passed through to the tabs feature.
|
|
31
|
-
* A preset that takes arguments is called as `features: () =>
|
|
32
|
-
* presetTabbed({...})`, which keeps the call inside createTerminal's failure
|
|
33
|
-
* boundary (see CreateTerminalOptions.features). */
|
|
34
|
-
export interface TabbedPresetOptions {
|
|
35
|
-
/** Swap the page's icon links to a status variant while a background session
|
|
36
|
-
* wants the user. Off by default, and enabling it is a promise that the
|
|
37
|
-
* variant assets are served — see TabsOptions.attentionIcons for the naming
|
|
38
|
-
* contract and .kiro/scripts/gen-attention-icons.py, which writes them. */
|
|
39
|
-
attentionIcons?: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
42
|
export function buildTabbed(
|
|
43
43
|
agentShell: boolean,
|
|
44
44
|
opts: TabbedPresetOptions = {},
|
|
@@ -59,12 +59,8 @@ export function buildTabbed(
|
|
|
59
59
|
];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/** Tabbed UI:
|
|
63
|
-
*
|
|
64
|
-
* title when the program sets one, else the last submitted line; each tab's
|
|
65
|
-
* activity dot stays hidden until its session reports activity via OSC 9;4
|
|
66
|
-
* progress (kiro-cli, Claude Code, …), so a plain bash/sh keeps clean,
|
|
67
|
-
* label-only tabs. */
|
|
62
|
+
/** Tabbed UI: buildTabbed with agent activity presumed off (see buildTabbed for
|
|
63
|
+
* title and activity-dot sourcing). */
|
|
68
64
|
export function presetTabbed(opts: TabbedPresetOptions = {}): TerminalFeature<unknown>[] {
|
|
69
65
|
return buildTabbed(false, opts);
|
|
70
66
|
}
|