@cplieger/web-terminal-ui 3.4.3 → 3.4.4
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 +2 -2
- package/css/00-tokens.css +1 -1
- package/css/01-reset.css +6 -0
- package/css/05-tabs.css +17 -24
- package/css/06-mobile.css +1 -1
- package/package.json +13 -12
- package/scaffold/index.html +6 -4
- package/src/features/tabs.ts +65 -26
- package/src/presets.ts +2 -2
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ individual features instead:
|
|
|
102
102
|
- `presetAgentTabbed()` — the same feature set as `presetTabbed()` (activity
|
|
103
103
|
monitor included), but with `preferInputTitle`: each tab's label follows the
|
|
104
104
|
latest submitted line (persisted server-side and recovered on reload) and the
|
|
105
|
-
program's OSC 0/2 title is ignored — for an agent shell such as `
|
|
105
|
+
program's OSC 0/2 title is ignored — for an agent shell such as `web-terminal-kiro`
|
|
106
106
|
whose program emits a non-empty but useless OSC title. Its status dots
|
|
107
107
|
(idle / working / done / needs-input) come from the same activity monitor,
|
|
108
108
|
driven by the server's status SSE and its OSC-9 classifier.
|
|
@@ -143,7 +143,7 @@ The web-terminal family:
|
|
|
143
143
|
Consumers that ship this UI:
|
|
144
144
|
|
|
145
145
|
- [`vibekit`](https://github.com/cplieger/vibekit)
|
|
146
|
-
- [`
|
|
146
|
+
- [`web-terminal-kiro`](https://github.com/cplieger/web-terminal-kiro)
|
|
147
147
|
|
|
148
148
|
## Disclaimer
|
|
149
149
|
|
package/css/00-tokens.css
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/* ===== Themeable tokens (the consumer-configurable "template") =====
|
|
11
11
|
These default to the neutral built-in look; a consumer recolors the UI by
|
|
12
12
|
overriding them via createTerminal's `theme` option (which sets them on the
|
|
13
|
-
terminal root) or its own CSS.
|
|
13
|
+
terminal root) or its own CSS. web-terminal-kiro sets a purple palette. Everything
|
|
14
14
|
above is the fixed design palette; everything here is the public theme API. */
|
|
15
15
|
--accent: oklch(72% 0.15 250deg); /* light blue; themed per consumer */
|
|
16
16
|
--tab-bg: oklch(30% 0 0deg / 90%); /* inactive-tab chip (translucent) */
|
package/css/01-reset.css
CHANGED
|
@@ -14,6 +14,12 @@ body {
|
|
|
14
14
|
background: var(--bg);
|
|
15
15
|
color-scheme: dark;
|
|
16
16
|
-webkit-font-smoothing: antialiased;
|
|
17
|
+
/* Disable the browser's landscape text autosizing ("text inflation"): without
|
|
18
|
+
this, rotating a phone to landscape zooms the text. iOS Safari implements
|
|
19
|
+
ONLY the -webkit- prefixed form (the unprefixed property is ignored there),
|
|
20
|
+
so both are required — unprefixed alone left iPhones inflating on rotate. */
|
|
21
|
+
/* stylelint-disable-next-line property-no-vendor-prefix -- iOS Safari only implements the prefixed form */
|
|
22
|
+
-webkit-text-size-adjust: 100%;
|
|
17
23
|
text-size-adjust: 100%;
|
|
18
24
|
overscroll-behavior: none;
|
|
19
25
|
/* Disable pinch-to-zoom and double-tap-to-zoom. The viewport meta tag
|
package/css/05-tabs.css
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
|
|
73
73
|
/* The active tab reads clearly above the inactive chips: a stronger fill plus an
|
|
74
74
|
accent-coloured edge (inactive chips carry a faint white --border; the active
|
|
75
|
-
one gets the accent equivalent). Themeable via --accent (
|
|
75
|
+
one gets the accent equivalent). Themeable via --accent (web-terminal-kiro -> purple). */
|
|
76
76
|
.wt-tab-active {
|
|
77
77
|
background: var(--tab-active-bg);
|
|
78
78
|
border-color: var(--tab-active-border);
|
|
@@ -195,24 +195,21 @@
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/* Desktop-strip keyboard button (tabs feature): opens the on-screen key grid on
|
|
198
|
-
a WIDE touchscreen (an iPad using the multi-tab strip
|
|
199
|
-
mobile switcher to carry one. Built + wired by
|
|
200
|
-
switcher's keyboard button (see makeKbButton
|
|
201
|
-
.wt-btn primitive for its 44px box + centered
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
198
|
+
a WIDE touchscreen (an iPad using the multi-tab strip, or a phone rotated to
|
|
199
|
+
landscape), where there is no mobile switcher to carry one. Built + wired by
|
|
200
|
+
the SAME factory as the mobile switcher's keyboard button (see makeKbButton
|
|
201
|
+
in tabs.ts). Uses the shared .wt-btn primitive for its 44px box + centered
|
|
202
|
+
icon, borderless like the switcher's — the old fixed-overlay styling gave it
|
|
203
|
+
a border and pinned it right of the "+", which read as a different control
|
|
204
|
+
the moment a phone rotated to landscape. Now an ordinary flex item inside the
|
|
205
|
+
strip, before the "+" (kb-then-new, mirroring the mobile switcher's order).
|
|
206
|
+
Hidden on a fine-pointer desktop (a real keyboard is present) and until a
|
|
207
|
+
keyboardToggle feature is wired ([hidden]); shown only on a coarse pointer at
|
|
208
|
+
strip widths (the media block below, kept in lockstep with SWITCHER_MEDIA in
|
|
209
|
+
src/features/tabs.ts). */
|
|
208
210
|
.wt-tab-kb {
|
|
209
211
|
display: none;
|
|
210
|
-
|
|
211
|
-
right: var(--sp-2);
|
|
212
|
-
bottom: calc(var(--safe-bottom) + var(--sp-2));
|
|
213
|
-
z-index: 16;
|
|
214
|
-
background: var(--surface);
|
|
215
|
-
border: 1px solid var(--border);
|
|
212
|
+
flex: none;
|
|
216
213
|
}
|
|
217
214
|
|
|
218
215
|
/* Grid open: filled so the toggled state reads at a glance (mirrors the
|
|
@@ -229,17 +226,13 @@
|
|
|
229
226
|
color: var(--bg);
|
|
230
227
|
}
|
|
231
228
|
|
|
232
|
-
/* Show
|
|
233
|
-
|
|
234
|
-
|
|
229
|
+
/* Show only on a WIDE touchscreen (where the desktop strip is used AND a
|
|
230
|
+
keyboardToggle is wired, hence :not([hidden])). In-flow now, so the strip
|
|
231
|
+
needs no reserved right padding for it. */
|
|
235
232
|
@media (pointer: coarse) and (width >= 601px) {
|
|
236
233
|
.wt-tab-kb:not([hidden]) {
|
|
237
234
|
display: inline-flex;
|
|
238
235
|
}
|
|
239
|
-
|
|
240
|
-
.wt-tab-bar {
|
|
241
|
-
padding-right: calc(var(--touch-target) + 2 * var(--sp-2));
|
|
242
|
-
}
|
|
243
236
|
}
|
|
244
237
|
|
|
245
238
|
/* The tab being dragged (reorder) dims while it moves. */
|
package/css/06-mobile.css
CHANGED
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
/* Right padding reserves room for the close (x) overlaid inside the row. */
|
|
112
112
|
padding: 0 var(--touch-target) 0 var(--sp-2);
|
|
113
113
|
/* The bar row always represents the active tab, so it wears the active-tab
|
|
114
|
-
fill/foreground the desktop strip uses (themeable; purple in
|
|
114
|
+
fill/foreground the desktop strip uses (themeable; purple in web-terminal-kiro). */
|
|
115
115
|
background: var(--tab-active-bg);
|
|
116
116
|
color: var(--tab-active-fg);
|
|
117
117
|
border: 1px solid var(--tab-active-border);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.4",
|
|
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": "GPL-3.0-or-later",
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
],
|
|
14
14
|
"types": "./src/index.ts",
|
|
15
15
|
"scripts": {
|
|
16
|
-
"typecheck": "
|
|
17
|
-
"typecheck:tests": "
|
|
16
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
17
|
+
"typecheck:tests": "tsc -p tsconfig.test.json",
|
|
18
18
|
"test": "vitest --run",
|
|
19
19
|
"test:watch": "vitest",
|
|
20
20
|
"test:coverage": "vitest --run --coverage",
|
|
@@ -29,19 +29,20 @@
|
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@eslint/js": "10.0.1",
|
|
32
|
-
"@types/node": "25.9.
|
|
33
|
-
"@
|
|
34
|
-
"
|
|
35
|
-
"
|
|
32
|
+
"@types/node": "25.9.5",
|
|
33
|
+
"@typescript/native": "npm:typescript@7.0.2",
|
|
34
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
35
|
+
"eslint": "10.7.0",
|
|
36
|
+
"fast-check": "4.9.0",
|
|
36
37
|
"happy-dom": "20.10.6",
|
|
37
38
|
"html-validate": "11.5.5",
|
|
38
|
-
"knip": "6.
|
|
39
|
-
"prettier": "3.9.
|
|
39
|
+
"knip": "6.26.0",
|
|
40
|
+
"prettier": "3.9.5",
|
|
40
41
|
"stylelint": "17.14.0",
|
|
41
42
|
"stylelint-config-standard": "40.0.0",
|
|
42
|
-
"typescript": "6.0.
|
|
43
|
-
"typescript-eslint": "8.
|
|
44
|
-
"vitest": "4.1.
|
|
43
|
+
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
44
|
+
"typescript-eslint": "8.63.0",
|
|
45
|
+
"vitest": "4.1.10"
|
|
45
46
|
},
|
|
46
47
|
"publishConfig": {
|
|
47
48
|
"access": "public"
|
package/scaffold/index.html
CHANGED
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
<link rel="stylesheet" href="/style.css">
|
|
10
10
|
<!-- [html-validate-disable-next no-style-tag -- critical CSS for instant loading indicator] -->
|
|
11
11
|
<style>
|
|
12
|
-
#loading{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
|
|
12
|
+
#loading{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000;color:var(--accent,#6ea8ff);transition:opacity .3s ease}
|
|
13
13
|
#loading.fade{opacity:0}
|
|
14
|
-
#loading .
|
|
15
|
-
|
|
14
|
+
#loading .bar{position:relative;inline-size:min(220px,62vw);block-size:5px;overflow:hidden;border-radius:999px;background:color-mix(in oklch,currentColor 16%,transparent)}
|
|
15
|
+
#loading .bar::before{content:"";position:absolute;inset-block:0;inline-size:42%;border-radius:inherit;background:linear-gradient(90deg,transparent,currentColor 28%,color-mix(in oklch,currentColor,#fff 45%) 50%,currentColor 72%,transparent);filter:drop-shadow(0 0 4px color-mix(in oklch,currentColor 55%,transparent));animation:wt-loading-slide 1.5s cubic-bezier(.65,0,.35,1) infinite}
|
|
16
|
+
@keyframes wt-loading-slide{0%{transform:translateX(-110%)}100%{transform:translateX(250%)}}
|
|
17
|
+
@media (prefers-reduced-motion:reduce){#loading .bar::before{animation-duration:3s}}
|
|
16
18
|
</style>
|
|
17
19
|
</head>
|
|
18
20
|
<body>
|
|
@@ -20,7 +22,7 @@
|
|
|
20
22
|
element. The loading overlay is a sibling (not a child of the root) so it is
|
|
21
23
|
never cleared; it is faded out once the first frame renders. -->
|
|
22
24
|
<div id="terminal"></div>
|
|
23
|
-
<div id="loading"><div class="
|
|
25
|
+
<div id="loading" role="status" aria-label="Loading"><div class="bar" aria-hidden="true"></div></div>
|
|
24
26
|
<!-- Deployment-specific: the importmap resolves the bare package specifiers to
|
|
25
27
|
the compiled JS your build emits. Adjust the paths to your build layout. -->
|
|
26
28
|
<script type="importmap">
|
package/src/features/tabs.ts
CHANGED
|
@@ -146,7 +146,7 @@ export interface TabsOptions {
|
|
|
146
146
|
/** Prefer the input-derived title over the process OSC 0/2 title. Default
|
|
147
147
|
* false (OSC-first: a program that sets its own window title wins, else the
|
|
148
148
|
* last submitted line). Set true for an agent shell whose program emits a
|
|
149
|
-
* non-empty but useless OSC title (kiro-cli under
|
|
149
|
+
* non-empty but useless OSC title (kiro-cli under web-terminal-kiro): the label then
|
|
150
150
|
* follows the latest submitted line (live) or the persisted client title (on
|
|
151
151
|
* reload), and the unreliable OSC title is ignored entirely. presetAgentTabbed
|
|
152
152
|
* enables this. */
|
|
@@ -359,7 +359,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
359
359
|
}
|
|
360
360
|
// Persist the input-derived tab title server-side so it survives a page reload
|
|
361
361
|
// and shows on other devices. The engine stores it as the session's fallback
|
|
362
|
-
// title and uses it only when the program emits no OSC title (the
|
|
362
|
+
// title and uses it only when the program emits no OSC title (the web-terminal-kiro case:
|
|
363
363
|
// kiro-cli emits no usable OSC title, so the latest user message is the label).
|
|
364
364
|
// Best-effort and fire-and-forget: a failure never disrupts the terminal — the
|
|
365
365
|
// locally-derived title still displays, it just is not persisted this time.
|
|
@@ -458,20 +458,19 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
458
458
|
bar.className = "wt-tab-bar";
|
|
459
459
|
bar.setAttribute("role", "tablist");
|
|
460
460
|
slot.appendChild(bar);
|
|
461
|
-
//
|
|
462
|
-
// the
|
|
463
|
-
//
|
|
464
|
-
//
|
|
461
|
+
// Strip-end controls, both flex items INSIDE the strip so they flow after
|
|
462
|
+
// the tabs (appending them to the slot flowed them out of the strip):
|
|
463
|
+
// the keyboard button first, then the "+" — mirroring the mobile
|
|
464
|
+
// switcher's kb-before-new order, so rotating a phone to landscape (which
|
|
465
|
+
// swaps in this strip) keeps the keyboard button LEFT of the "+" rather
|
|
466
|
+
// than jumping to its right. Built + wired by the same shared factories
|
|
467
|
+
// as the mobile switcher's; the kb button is CSS-gated to a wide
|
|
468
|
+
// touchscreen and un-hidden below only when a keyboardToggle is wired.
|
|
469
|
+
// addTabChrome inserts each tab before deskKb, keeping [tabs… kb +].
|
|
470
|
+
const deskKb = makeKbButton("wt-tab-kb wt-btn");
|
|
471
|
+
bar.appendChild(deskKb);
|
|
465
472
|
const newBtn = makeNewButton("wt-tab-new");
|
|
466
473
|
bar.appendChild(newBtn);
|
|
467
|
-
// Desktop-strip keyboard button, built + wired by the SAME factory as the
|
|
468
|
-
// mobile switcher's. A SIBLING of the strip (appended to the region slot,
|
|
469
|
-
// not into .wt-tab-bar) so it never affects the strip's child order; CSS
|
|
470
|
-
// shows it only on a wide touchscreen (where the strip, not the switcher,
|
|
471
|
-
// is used) and hides it on a fine-pointer desktop. Un-hidden below only
|
|
472
|
-
// when a keyboardToggle feature is wired.
|
|
473
|
-
const deskKb = makeKbButton("wt-tab-kb wt-btn");
|
|
474
|
-
slot.appendChild(deskKb);
|
|
475
474
|
|
|
476
475
|
// Pull the terminal surface up off the fixed BOTTOM strip on desktop so
|
|
477
476
|
// the bar does not overlap the last rows. The surface is position:fixed
|
|
@@ -522,8 +521,12 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
522
521
|
swBar.append(swKb, swSwitch, swNew);
|
|
523
522
|
// Latest-wins notification state for the switch button's dot: overwritten
|
|
524
523
|
// by each qualifying background-tab event (applyStatus) and cleared when
|
|
525
|
-
// the list opens (expandSwitcher)
|
|
524
|
+
// the list opens (expandSwitcher) or the raising tab is visited or closed
|
|
525
|
+
// (acknowledgeSwitchNotify). "" = no pending cue; the dot is hidden.
|
|
526
526
|
let switchNotify: "" | "input" | "done" = "";
|
|
527
|
+
// The session that raised the pending cue, so arriving on that tab (a
|
|
528
|
+
// swipe or any switch) acknowledges it without opening the list.
|
|
529
|
+
let switchNotifyId: string | null = null;
|
|
527
530
|
function paintSwitchDot(): void {
|
|
528
531
|
// Reuse the per-tab status-dot colours (single source, css/05-tabs.css
|
|
529
532
|
// .wt-status-dot[data-status="input"|"done"]) instead of re-declaring
|
|
@@ -535,6 +538,21 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
535
538
|
swSwitchDot.dataset["status"] = switchNotify;
|
|
536
539
|
}
|
|
537
540
|
}
|
|
541
|
+
function clearSwitchNotify(): void {
|
|
542
|
+
switchNotify = "";
|
|
543
|
+
switchNotifyId = null;
|
|
544
|
+
paintSwitchDot();
|
|
545
|
+
}
|
|
546
|
+
// acknowledgeSwitchNotify clears the pending cue when its subject session
|
|
547
|
+
// is reached (switchTo — including a swipe arriving on it) or ceases to
|
|
548
|
+
// exist (a close/reap): the notification is resolved or moot then, and
|
|
549
|
+
// only opening the list used to clear it (the reported "swiping to the
|
|
550
|
+
// concerned tab leaves the dot lit").
|
|
551
|
+
function acknowledgeSwitchNotify(id: string): void {
|
|
552
|
+
if (switchNotifyId === id) {
|
|
553
|
+
clearSwitchNotify();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
538
556
|
ctx.region("bottom-switcher", "switcher").appendChild(switcher);
|
|
539
557
|
// The keyboard buttons open the key grid; show them only when a toolbar is
|
|
540
558
|
// wired to drive. Read the toolbar's API lazily at tap time (ctx.use), so
|
|
@@ -1223,8 +1241,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1223
1241
|
}
|
|
1224
1242
|
// Opening the list acknowledges any pending background-tab notification:
|
|
1225
1243
|
// the user is now looking at the tabs, so clear the switch button's dot.
|
|
1226
|
-
|
|
1227
|
-
paintSwitchDot();
|
|
1244
|
+
clearSwitchNotify();
|
|
1228
1245
|
renderSwitcherList();
|
|
1229
1246
|
setExpandedState(true);
|
|
1230
1247
|
ctx.announce("Terminal list expanded");
|
|
@@ -1257,8 +1274,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1257
1274
|
}
|
|
1258
1275
|
paintStatusDot(dot, info.status, info.reportsActivity ?? false);
|
|
1259
1276
|
const aria = tablist.registerTab(el);
|
|
1260
|
-
// Insert before the
|
|
1261
|
-
|
|
1277
|
+
// Insert before the strip-end controls (keyboard button, then "+") so
|
|
1278
|
+
// they stay the last items in the strip.
|
|
1279
|
+
bar.insertBefore(el, deskKb);
|
|
1262
1280
|
// Runtime-added tabs animate in; initial tabs do not (see `started`).
|
|
1263
1281
|
// The timer (not animationend) also clears the class on the hidden mobile
|
|
1264
1282
|
// strip, where the animation never fires.
|
|
@@ -1390,6 +1408,10 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1390
1408
|
// rebuild viewport-first, so the last-known screen paints with no
|
|
1391
1409
|
// round-trip. Then let the kernel reconnect the WS to it (resume delta).
|
|
1392
1410
|
activeId = next.id;
|
|
1411
|
+
// Arriving on the tab that raised the switch-button cue resolves it
|
|
1412
|
+
// (a swipe through the tabs must dismiss the dot, not only opening the
|
|
1413
|
+
// list).
|
|
1414
|
+
acknowledgeSwitchNotify(next.id);
|
|
1393
1415
|
try {
|
|
1394
1416
|
localStorage.setItem(ACTIVE_TAB_KEY, next.id);
|
|
1395
1417
|
} catch {
|
|
@@ -1522,9 +1544,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1522
1544
|
|
|
1523
1545
|
// dragTargetBefore returns the first tab whose horizontal midpoint is past
|
|
1524
1546
|
// x (the element the dragged tab should sit before), or null to drop at the
|
|
1525
|
-
// end (before the
|
|
1526
|
-
// strip's DOM order after a drag, so position indicators, the
|
|
1527
|
-
// close-to-the-right/left all follow the visible order.
|
|
1547
|
+
// end (before the strip-end controls). syncOrderFromDom rebuilds tabList to
|
|
1548
|
+
// match the strip's DOM order after a drag, so position indicators, the
|
|
1549
|
+
// switcher, and close-to-the-right/left all follow the visible order.
|
|
1528
1550
|
function dragTargetBefore(x: number): HTMLElement | null {
|
|
1529
1551
|
for (const el of bar.querySelectorAll<HTMLElement>(".wt-tab:not(.wt-tab-dragging)")) {
|
|
1530
1552
|
const rect = el.getBoundingClientRect();
|
|
@@ -1647,6 +1669,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1647
1669
|
// Tombstone this id briefly so a stale status snapshot/poll that predates
|
|
1648
1670
|
// the server reaping it cannot re-adopt the just-closed tab.
|
|
1649
1671
|
tombstone(id);
|
|
1672
|
+
// A pending switch-button cue whose subject just closed is moot: clear
|
|
1673
|
+
// it rather than leaving a dot no tab visit can ever resolve.
|
|
1674
|
+
acknowledgeSwitchNotify(id);
|
|
1650
1675
|
tab.aria.remove();
|
|
1651
1676
|
// Remove immediately (no exit animation): a lingering element made the
|
|
1652
1677
|
// "+" teleport after a delay, and made a last-tab replacement appear in
|
|
@@ -1711,6 +1736,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1711
1736
|
// server reaping these sessions cannot re-adopt (flash back) a just-
|
|
1712
1737
|
// closed tab -- mirrors the single-close path in dropTab (h-f2).
|
|
1713
1738
|
tombstone(t.id);
|
|
1739
|
+
acknowledgeSwitchNotify(t.id); // a cue for a closed tab is moot
|
|
1714
1740
|
t.aria.remove();
|
|
1715
1741
|
t.el.remove();
|
|
1716
1742
|
ctx.dropSession(t.id);
|
|
@@ -1880,6 +1906,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1880
1906
|
// dedicated button.
|
|
1881
1907
|
if (id !== activeId && (status === "input" || status === "done")) {
|
|
1882
1908
|
switchNotify = status;
|
|
1909
|
+
switchNotifyId = id;
|
|
1883
1910
|
paintSwitchDot();
|
|
1884
1911
|
}
|
|
1885
1912
|
// Record the raw server title; the displayed label (fallback + de-dup)
|
|
@@ -1914,15 +1941,20 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1914
1941
|
return;
|
|
1915
1942
|
}
|
|
1916
1943
|
// Adopt a session created in another browser so all clients converge.
|
|
1944
|
+
// Carry the persisted clientTitle through: the SSE snapshot usually
|
|
1945
|
+
// beats the initial GET /api/sessions (whose adoptSession then dedups
|
|
1946
|
+
// and never re-applies its fields), so dropping it here left every
|
|
1947
|
+
// SSE-adopted tab without its persisted title — in preferInputTitle
|
|
1948
|
+
// mode the label then fell back to "New tab" on reload.
|
|
1917
1949
|
adoptSession({
|
|
1918
1950
|
id: s.id,
|
|
1919
1951
|
title: s.title,
|
|
1920
|
-
clientTitle:
|
|
1952
|
+
clientTitle: s.clientTitle,
|
|
1921
1953
|
createdAt: s.createdAt,
|
|
1922
1954
|
status: s.status,
|
|
1923
1955
|
reportsActivity: s.reportsActivity ?? false,
|
|
1924
1956
|
});
|
|
1925
|
-
applyStatus(s.id, s.status, s.title,
|
|
1957
|
+
applyStatus(s.id, s.status, s.title, s.clientTitle, s.reportsActivity ?? false);
|
|
1926
1958
|
ensureActive();
|
|
1927
1959
|
syncChrome();
|
|
1928
1960
|
});
|
|
@@ -1994,7 +2026,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1994
2026
|
if (e.dataTransfer) {
|
|
1995
2027
|
e.dataTransfer.dropEffect = "move";
|
|
1996
2028
|
}
|
|
1997
|
-
bar.insertBefore(draggingEl, dragTargetBefore(e.clientX) ??
|
|
2029
|
+
bar.insertBefore(draggingEl, dragTargetBefore(e.clientX) ?? deskKb);
|
|
1998
2030
|
});
|
|
1999
2031
|
// Active-row close (x): closes the current tab (mirrors a listed row's x).
|
|
2000
2032
|
// stopPropagation so it is not read as a tap/swipe on the row surface.
|
|
@@ -2054,9 +2086,16 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2054
2086
|
return;
|
|
2055
2087
|
}
|
|
2056
2088
|
const target = e.target as HTMLElement | null;
|
|
2089
|
+
// The tab strip counts as chrome like the switcher: without it, the
|
|
2090
|
+
// desktop-strip keyboard button's own pointerup landed here, closed the
|
|
2091
|
+
// grid, and the button's click then re-opened it — so tapping the button
|
|
2092
|
+
// to CLOSE the grid never worked (a wide-touchscreen / landscape-phone
|
|
2093
|
+
// bug; the switcher's kb button was already exempt via .wt-switcher).
|
|
2057
2094
|
const inChrome =
|
|
2058
2095
|
target !== null &&
|
|
2059
|
-
(target.closest(".wt-switcher") !== null ||
|
|
2096
|
+
(target.closest(".wt-switcher") !== null ||
|
|
2097
|
+
target.closest(".wt-tab-bar") !== null ||
|
|
2098
|
+
target.closest(".key-toolbar") !== null);
|
|
2060
2099
|
if (inChrome) {
|
|
2061
2100
|
return;
|
|
2062
2101
|
}
|
package/src/presets.ts
CHANGED
|
@@ -66,7 +66,7 @@ function buildTabbed(preferInputTitle: boolean): TerminalFeature<unknown>[] {
|
|
|
66
66
|
/** Tabbed UI: the touch features, tabs, the mobile keyboard bar, the activity
|
|
67
67
|
* monitor, and animations. Requires a server that speaks the session API
|
|
68
68
|
* (`/api/sessions`, `/ws?session=`, and the status SSE `/api/sessions/events`),
|
|
69
|
-
* such as `web-terminal-server` or `
|
|
69
|
+
* such as `web-terminal-server` or `web-terminal-kiro`. Each tab's title follows the
|
|
70
70
|
* process OSC 0/2 window title when the program sets one, else the last
|
|
71
71
|
* submitted line; each tab's activity dot stays hidden until its session
|
|
72
72
|
* reports activity via OSC 9;4 progress (kiro-cli, Claude Code, …), so a plain
|
|
@@ -75,7 +75,7 @@ export function presetTabbed(): TerminalFeature<unknown>[] {
|
|
|
75
75
|
return buildTabbed(false);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/** Tabbed UI for an agent shell (
|
|
78
|
+
/** Tabbed UI for an agent shell (web-terminal-kiro). Same features as presetTabbed, but
|
|
79
79
|
* with `preferInputTitle`: the agent's program (kiro-cli) emits a non-empty but
|
|
80
80
|
* useless OSC 0/2 title, so each tab's label follows the latest submitted line
|
|
81
81
|
* (persisted server-side and recovered on reload) and the OSC title is ignored.
|