@cplieger/web-terminal-ui 3.1.0 → 3.3.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 +18 -11
- package/css/05-tabs.css +11 -9
- package/package.json +2 -2
- package/src/features/tabs.ts +103 -43
- package/src/kernel/kernel.ts +11 -0
- package/src/presets.ts +31 -24
package/README.md
CHANGED
|
@@ -85,26 +85,33 @@ render/connection/scroll modules are single-instance per page (tabs multiplex
|
|
|
85
85
|
sessions over the one kernel). `scaffold/index.html` is a complete reference page
|
|
86
86
|
to copy and adapt.
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
Four presets are provided (import from `@cplieger/web-terminal-ui/presets`); each
|
|
89
89
|
is a plain feature-array factory, so you can spread and edit it, or hand-pick
|
|
90
90
|
individual features instead:
|
|
91
91
|
|
|
92
92
|
- `presetSingle()` — single-pane desktop UI (context menu, clipboard,
|
|
93
93
|
scroll-to-bottom, predictive echo, connection banner).
|
|
94
94
|
- `presetTouch()` — `presetSingle()` plus the mobile key toolbar.
|
|
95
|
-
- `presetTabbed()` — the
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
- `presetTabbed()` — the generic tabbed UI: `presetTouch()` plus tabs and
|
|
96
|
+
animations. Tabs are label-only (no activity dots); each tab's title follows
|
|
97
|
+
the process window title (OSC 0/2) when the program sets one and keeps it
|
|
98
|
+
updated, otherwise the last command submitted. Requires a server that speaks
|
|
99
|
+
the session API (`/api/sessions` and `/ws?session=`), such as
|
|
100
|
+
`web-terminal-server`.
|
|
101
|
+
- `presetAgentTabbed()` — `presetTabbed()` plus the activity monitor, so each
|
|
102
|
+
tab carries a live status dot (idle / working / done / needs-input) from the
|
|
103
|
+
server's status SSE. Same title behavior as `presetTabbed`. For an agent shell
|
|
104
|
+
such as `vibecli`.
|
|
99
105
|
|
|
100
106
|
### Options
|
|
101
107
|
|
|
102
|
-
| Option | Default | Purpose
|
|
103
|
-
| ----------- | -------------------------- |
|
|
104
|
-
| `features` | _(none — bare kernel)_ | The feature list. Omitted or empty builds only the terminal (no chrome). Use a preset from `./presets` or a hand-picked array.
|
|
105
|
-
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to.
|
|
106
|
-
| `fontReady` | `'14px "MonaspiceNe NFM"'` | CSS font shorthand awaited before the first resize, so the server is sized against the real web font's cell metrics rather than a fallback.
|
|
107
|
-
| `loading` | _(none)_ | A pre-JS loading overlay element (kept in your served HTML so it paints before this module loads); it is faded out and removed once the first frame renders.
|
|
108
|
+
| Option | Default | Purpose |
|
|
109
|
+
| ----------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
110
|
+
| `features` | _(none — bare kernel)_ | The feature list. Omitted or empty builds only the terminal (no chrome). Use a preset from `./presets` or a hand-picked array. |
|
|
111
|
+
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
|
|
112
|
+
| `fontReady` | `'14px "MonaspiceNe NFM"'` | CSS font shorthand awaited before the first resize, so the server is sized against the real web font's cell metrics rather than a fallback. |
|
|
113
|
+
| `loading` | _(none)_ | A pre-JS loading overlay element (kept in your served HTML so it paints before this module loads); it is faded out and removed once the first frame renders. |
|
|
114
|
+
| `theme` | _(none)_ | Theme overrides (CSS custom properties on the terminal root): `--accent`, `--tab-hover-bg`, `--tab-active-bg`, `--tab-active-fg`. The library ships neutral defaults. |
|
|
108
115
|
|
|
109
116
|
`createTerminal()` returns a handle: `focus()` re-focuses the terminal input
|
|
110
117
|
(and opens the soft keyboard on touch), and `destroy()` tears every feature down
|
package/css/05-tabs.css
CHANGED
|
@@ -231,12 +231,13 @@
|
|
|
231
231
|
per section 12 / WCAG 1.4.1. The one .wt-status-dot vocabulary is shared by
|
|
232
232
|
the desktop strip, the mobile switcher, and the overview sheet rows.
|
|
233
233
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
234
|
+
Revealed PER TAB: the dot is hidden until its own session reports activity
|
|
235
|
+
(OSC 9;4 progress, or a classified OSC 9 notification), at which point the
|
|
236
|
+
tabs feature adds .wt-reports to that dot (paintStatusDot). A program that
|
|
237
|
+
emits no OSC 9 signal (a plain bash/sh) has no activity concept, so its tabs
|
|
238
|
+
stay clean, label-only; a progress-reporting agent (kiro-cli, Claude Code)
|
|
239
|
+
lights up. The element stays in the DOM either way (JS sets data-status
|
|
240
|
+
regardless), so revealing it is a pure CSS switch. */
|
|
240
241
|
.wt-status-dot {
|
|
241
242
|
display: none;
|
|
242
243
|
width: 9px;
|
|
@@ -251,9 +252,10 @@
|
|
|
251
252
|
border: 1.5px solid color-mix(in oklch, var(--text) 45%, transparent);
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
/*
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
/* Reveal a dot only once its session has reported activity (the tabs feature
|
|
256
|
+
adds .wt-reports per dot; see paintStatusDot). Without it the dot — and the
|
|
257
|
+
whole activity vocabulary for that tab — stays invisible. */
|
|
258
|
+
.wt-status-dot.wt-reports {
|
|
257
259
|
display: block;
|
|
258
260
|
}
|
|
259
261
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
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",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"verify": "bash scripts/verify.sh"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@cplieger/web-terminal-engine": ">=2.
|
|
28
|
+
"@cplieger/web-terminal-engine": ">=2.2.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@eslint/js": "10.0.1",
|
package/src/features/tabs.ts
CHANGED
|
@@ -54,6 +54,10 @@ interface SessionInfo {
|
|
|
54
54
|
title: string;
|
|
55
55
|
createdAt: string;
|
|
56
56
|
status: string;
|
|
57
|
+
/** Sticky: true once the session has emitted a genuine activity signal
|
|
58
|
+
* (OSC 9;4 progress, or a classified OSC 9 notification). Gates whether the
|
|
59
|
+
* per-tab activity dot is shown; see paintStatusDot / baseLabel. */
|
|
60
|
+
reportsActivity?: boolean;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
interface Tab {
|
|
@@ -72,10 +76,18 @@ interface Tab {
|
|
|
72
76
|
aria: TabHandle;
|
|
73
77
|
scrollTop: number;
|
|
74
78
|
following: boolean;
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
+
/** Sticky: true once this session emitted a genuine activity signal (OSC 9;4).
|
|
80
|
+
* Its activity dot is shown only while true; a session that never reports
|
|
81
|
+
* activity (a plain shell) keeps a clean, dot-less tab. Fed from the server's
|
|
82
|
+
* reportsActivity via applyStatus. */
|
|
83
|
+
reports: boolean;
|
|
84
|
+
/** A fallback title derived from the LAST non-empty line the user submitted
|
|
85
|
+
* into this tab (updated on every Enter). Used only when the process sets no
|
|
86
|
+
* window title of its own: the OSC 0/2 title (`title` above) takes precedence
|
|
87
|
+
* when present and keeps updating, and this is what the tab reads as when it
|
|
88
|
+
* does not — a plain shell with no PROMPT_COMMAND title, or kiro-cli with its
|
|
89
|
+
* cwd-only title disabled. Undefined until the first non-empty submission; see
|
|
90
|
+
* baseLabel. */
|
|
79
91
|
derived?: string;
|
|
80
92
|
}
|
|
81
93
|
|
|
@@ -291,14 +303,14 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
291
303
|
});
|
|
292
304
|
swReserve.observe(swBar);
|
|
293
305
|
|
|
294
|
-
// Activity dots are
|
|
295
|
-
// (
|
|
296
|
-
//
|
|
297
|
-
// .wt-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
306
|
+
// Activity dots are revealed PER TAB, not chrome-wide: each dot stays
|
|
307
|
+
// hidden (CSS: .wt-status-dot { display: none }) until its session reports
|
|
308
|
+
// activity (OSC 9;4 progress or a classified OSC 9 notification), at which
|
|
309
|
+
// point paintStatusDot adds .wt-reports to reveal it (see applyStatus /
|
|
310
|
+
// syncMobile / updateRow). A program that emits no OSC 9 signal (a plain
|
|
311
|
+
// bash/sh) keeps clean, label-only tabs; an agent's tabs light up. The
|
|
312
|
+
// monitor (below) is the live source; without it the poll fallback feeds
|
|
313
|
+
// the same reportsActivity flag.
|
|
302
314
|
|
|
303
315
|
// Catching-up cue: a switched-into tab's cached screen is stale until its
|
|
304
316
|
// resume delta lands, so it must not read as live (sections 12/13). Shown
|
|
@@ -354,19 +366,21 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
354
366
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
355
367
|
|
|
356
368
|
// baseLabel is a tab's label before de-duplication. Preference order: the
|
|
357
|
-
// title
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
369
|
+
// process window title (OSC 0/2, e.g. a shell's PROMPT_COMMAND title),
|
|
370
|
+
// which takes precedence whenever the program sets one and keeps updating
|
|
371
|
+
// as it changes (the status SSE re-pushes it); then a fallback derived from
|
|
372
|
+
// the last line the user submitted, for a program that sets no title (a
|
|
373
|
+
// bare shell, or kiro-cli with its cwd-only title disabled); then a plain
|
|
374
|
+
// "New tab". fallback=true marks that last case so relabelAll leaves
|
|
361
375
|
// untitled tabs as "New tab" with no numeric suffix.
|
|
362
376
|
function baseLabel(tab: Tab): { text: string; fallback: boolean } {
|
|
363
|
-
const real = tab.
|
|
377
|
+
const real = tab.title.trim() || (tab.derived?.trim() ?? "");
|
|
364
378
|
return real ? { text: real, fallback: false } : { text: "New tab", fallback: true };
|
|
365
379
|
}
|
|
366
380
|
|
|
367
381
|
// relabelAll recomputes every tab's display label with de-duplication:
|
|
368
|
-
// when two tabs resolve to the same base label (e.g. two
|
|
369
|
-
//
|
|
382
|
+
// when two tabs resolve to the same base label (e.g. two shells with the
|
|
383
|
+
// same window title, or two tabs whose last submitted line was identical),
|
|
370
384
|
// the second and later get a " (k)" suffix in creation order, so the strip
|
|
371
385
|
// never shows two identical labels.
|
|
372
386
|
function relabelAll(): void {
|
|
@@ -396,16 +410,17 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
396
410
|
}
|
|
397
411
|
}
|
|
398
412
|
|
|
399
|
-
// ---
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
// submission. Best-effort: an odd
|
|
408
|
-
//
|
|
413
|
+
// --- Fallback title derivation from the user's submitted lines ---
|
|
414
|
+
// Used only when the process sets no OSC window title of its own (baseLabel
|
|
415
|
+
// prefers tab.title). A shell without a PROMPT_COMMAND title, or kiro-cli
|
|
416
|
+
// with its cwd-only title disabled, sets none — so the useful fallback is
|
|
417
|
+
// the last line the user submitted. The kernel routes input to the active
|
|
418
|
+
// session, so an input observer's bytes belong to activeId. A tiny line
|
|
419
|
+
// editor tracks the current input line (handling backspace and skipping
|
|
420
|
+
// the escape sequences arrow keys and bracketed paste emit) and updates the
|
|
421
|
+
// tab's derived title on every non-empty submission. Best-effort: an odd
|
|
422
|
+
// editing sequence just yields no derived title (the tab then reads "New
|
|
423
|
+
// tab" until the next submission).
|
|
409
424
|
const MAX_DERIVED = 60;
|
|
410
425
|
let lineBytes: number[] = [];
|
|
411
426
|
let escState = 0; // 0 normal, 1 saw ESC, 2 in CSI, 3 in SS3 (one more byte)
|
|
@@ -415,8 +430,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
415
430
|
}
|
|
416
431
|
function deriveTitleFromInput(bytes: Uint8Array): void {
|
|
417
432
|
const t = tabList.find((x) => x.id === activeId);
|
|
418
|
-
if (!t
|
|
419
|
-
return; // no active tab
|
|
433
|
+
if (!t) {
|
|
434
|
+
return; // no active tab
|
|
420
435
|
}
|
|
421
436
|
for (const b of bytes) {
|
|
422
437
|
if (escState === 1) {
|
|
@@ -438,10 +453,12 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
438
453
|
} else if (b === 0x0d || b === 0x0a) {
|
|
439
454
|
const line = new TextDecoder().decode(new Uint8Array(lineBytes)).trim();
|
|
440
455
|
lineBytes = [];
|
|
441
|
-
|
|
456
|
+
// Every non-empty submitted line updates the fallback title (the
|
|
457
|
+
// last one wins); a leading "/" counts, since it is a valid shell
|
|
458
|
+
// command path.
|
|
459
|
+
if (line) {
|
|
442
460
|
t.derived = line.slice(0, MAX_DERIVED);
|
|
443
461
|
syncChrome();
|
|
444
|
-
return;
|
|
445
462
|
}
|
|
446
463
|
} else if (b === 0x7f || b === 0x08) {
|
|
447
464
|
// Backspace: drop one codepoint (pop UTF-8 continuation bytes + lead).
|
|
@@ -482,7 +499,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
482
499
|
const idx = tabList.findIndex((t) => t.id === activeId);
|
|
483
500
|
const active = idx >= 0 ? tabList[idx] : undefined;
|
|
484
501
|
swLabel.textContent = active ? active.display : "";
|
|
485
|
-
swDot
|
|
502
|
+
paintStatusDot(swDot, active?.dot.dataset["status"] ?? "idle", active?.reports ?? false);
|
|
486
503
|
const bgInput = tabList.some(
|
|
487
504
|
(t) => t.id !== activeId && t.dot.dataset["status"] === "input",
|
|
488
505
|
);
|
|
@@ -512,7 +529,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
512
529
|
}
|
|
513
530
|
// updateRow refreshes a reused row's live bits (status dot + label).
|
|
514
531
|
function updateRow(row: HTMLElement, t: Tab): void {
|
|
515
|
-
|
|
532
|
+
paintStatusDot(
|
|
533
|
+
pick(row, ".wt-switcher-row-dot"),
|
|
534
|
+
t.dot.dataset["status"] ?? "idle",
|
|
535
|
+
t.reports,
|
|
536
|
+
);
|
|
516
537
|
pick(row, ".wt-switcher-row-label").textContent = t.display;
|
|
517
538
|
}
|
|
518
539
|
// animateRowIn / animateRowOut give a listed tab an enter / leave motion:
|
|
@@ -813,7 +834,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
813
834
|
if (!label || !dot || !close) {
|
|
814
835
|
throw new Error("web-terminal-ui: tab chrome missing parts");
|
|
815
836
|
}
|
|
816
|
-
dot.
|
|
837
|
+
paintStatusDot(dot, info.status, info.reportsActivity ?? false);
|
|
817
838
|
const aria = tablist.registerTab(el);
|
|
818
839
|
// Insert before the "+" button so it stays the last item in the strip.
|
|
819
840
|
bar.insertBefore(el, newBtn);
|
|
@@ -839,6 +860,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
839
860
|
aria,
|
|
840
861
|
scrollTop: 0,
|
|
841
862
|
following: true,
|
|
863
|
+
reports: info.reportsActivity ?? false,
|
|
842
864
|
};
|
|
843
865
|
// Set an initial label immediately (relabelAll refines it with de-dup
|
|
844
866
|
// once the tab is in tabList and syncChrome runs).
|
|
@@ -1019,6 +1041,20 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1019
1041
|
clearCatchup();
|
|
1020
1042
|
});
|
|
1021
1043
|
|
|
1044
|
+
// Live window-title updates for the active session: the engine sends a
|
|
1045
|
+
// TITLE frame on the live socket whenever the process changes its OSC 0/2
|
|
1046
|
+
// title, which the kernel republishes as wire:title. baseLabel prefers the
|
|
1047
|
+
// OSC title, so applying it here updates the active tab's label at once
|
|
1048
|
+
// rather than waiting for the next status-SSE/poll sweep (background tabs,
|
|
1049
|
+
// which have no live socket, still refresh their title from that sweep).
|
|
1050
|
+
ctx.on("wire:title", ({ session, title }) => {
|
|
1051
|
+
const t = tabList.find((x) => x.id === session);
|
|
1052
|
+
if (t && title !== t.title) {
|
|
1053
|
+
t.title = title;
|
|
1054
|
+
syncChrome();
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1022
1058
|
// switchRelative moves delta tabs from the active one (swipe left = next).
|
|
1023
1059
|
// The direction feeds the slide animation (next slides in from the right,
|
|
1024
1060
|
// prev from the left).
|
|
@@ -1334,14 +1370,21 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1334
1370
|
ctx.toast("Swipe to switch terminals");
|
|
1335
1371
|
}
|
|
1336
1372
|
|
|
1337
|
-
// applyStatus updates one tab's dot + title from a status
|
|
1338
|
-
// the SSE monitor and the polling fallback).
|
|
1339
|
-
|
|
1373
|
+
// applyStatus updates one tab's dot (status + reveal) + title from a status
|
|
1374
|
+
// record (shared by the SSE monitor and the polling fallback). reports is
|
|
1375
|
+
// the server's sticky reportsActivity flag; it gates the dot's visibility.
|
|
1376
|
+
function applyStatus(
|
|
1377
|
+
id: string,
|
|
1378
|
+
status: string,
|
|
1379
|
+
title: string | undefined,
|
|
1380
|
+
reports: boolean,
|
|
1381
|
+
): void {
|
|
1340
1382
|
const t = tabList.find((tab) => tab.id === id);
|
|
1341
1383
|
if (!t) {
|
|
1342
1384
|
return;
|
|
1343
1385
|
}
|
|
1344
|
-
t.
|
|
1386
|
+
t.reports = reports;
|
|
1387
|
+
paintStatusDot(t.dot, status, reports);
|
|
1345
1388
|
// Record the raw server title; the displayed label (fallback + de-dup)
|
|
1346
1389
|
// is recomputed by relabelAll via syncChrome, which the callers run
|
|
1347
1390
|
// right after applyStatus.
|
|
@@ -1363,8 +1406,14 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1363
1406
|
return;
|
|
1364
1407
|
}
|
|
1365
1408
|
// Adopt a session created in another browser so all clients converge.
|
|
1366
|
-
adoptSession({
|
|
1367
|
-
|
|
1409
|
+
adoptSession({
|
|
1410
|
+
id: s.id,
|
|
1411
|
+
title: s.title,
|
|
1412
|
+
createdAt: s.createdAt,
|
|
1413
|
+
status: s.status,
|
|
1414
|
+
reportsActivity: s.reportsActivity ?? false,
|
|
1415
|
+
});
|
|
1416
|
+
applyStatus(s.id, s.status, s.title, s.reportsActivity ?? false);
|
|
1368
1417
|
syncChrome();
|
|
1369
1418
|
});
|
|
1370
1419
|
} else {
|
|
@@ -1379,7 +1428,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1379
1428
|
const seen = new Set(list.map((s) => s.id));
|
|
1380
1429
|
for (const info of list) {
|
|
1381
1430
|
adoptSession(info); // add sessions created elsewhere (no local tab)
|
|
1382
|
-
applyStatus(info.id, info.status, info.title);
|
|
1431
|
+
applyStatus(info.id, info.status, info.title, info.reportsActivity ?? false);
|
|
1383
1432
|
}
|
|
1384
1433
|
// A tab the server no longer lists was reaped/closed elsewhere: drop it
|
|
1385
1434
|
// locally (no DELETE — it is already gone).
|
|
@@ -1928,6 +1977,17 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1928
1977
|
};
|
|
1929
1978
|
}
|
|
1930
1979
|
|
|
1980
|
+
// paintStatusDot applies a status dot's two orthogonal bits: data-status drives
|
|
1981
|
+
// its appearance (idle / working / done / input / exited via CSS), and the
|
|
1982
|
+
// .wt-reports class controls its visibility — the dot is hidden by default and
|
|
1983
|
+
// shown only once the session has reported activity (OSC 9;4 progress or a
|
|
1984
|
+
// classified OSC 9 notification), so a plain shell's tabs stay clean and
|
|
1985
|
+
// label-only while an agent's light up.
|
|
1986
|
+
function paintStatusDot(el: HTMLElement, status: string, reports: boolean): void {
|
|
1987
|
+
el.dataset["status"] = status || "idle";
|
|
1988
|
+
el.classList.toggle("wt-reports", reports);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1931
1991
|
// pick returns a required descendant element or throws (static chrome only).
|
|
1932
1992
|
function pick(root: ParentNode, selector: string): HTMLElement {
|
|
1933
1993
|
const el = root.querySelector<HTMLElement>(selector);
|
package/src/kernel/kernel.ts
CHANGED
|
@@ -156,6 +156,17 @@ export function createTerminal(
|
|
|
156
156
|
if (!connection.sendBinary(out)) {
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
|
+
// Classic-terminal behavior (GNOME/xterm): user input re-engages follow and
|
|
160
|
+
// snaps to the bottom, so typing while scrolled up jumps to the input line.
|
|
161
|
+
// Instant (not the jump-button's smooth scroll), so it isn't janky per
|
|
162
|
+
// keystroke. This is the ONLY thing that scrolls a held view down — program
|
|
163
|
+
// output never does (the scroll controller's follow/hold job is unchanged),
|
|
164
|
+
// so scrolling up to read while output streams is preserved. A no-op on the
|
|
165
|
+
// alt screen (no scrollback) and when already following. NOTE: every caller
|
|
166
|
+
// of sendBytes today is genuine user input (typed keys, paste, mobile
|
|
167
|
+
// toolbar); if mouse tracking is ever wired through ctx.send, its motion
|
|
168
|
+
// bytes must NOT come through here or the view would snap on every move.
|
|
169
|
+
scroll.scrollToBottom();
|
|
159
170
|
for (const obs of inputObservers) {
|
|
160
171
|
obs(out);
|
|
161
172
|
}
|
package/src/presets.ts
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
// lists (convenient, full UI); a consumer wanting a minimal footprint imports
|
|
5
5
|
// individual features instead.
|
|
6
6
|
//
|
|
7
|
-
// The tabbed presets
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// The tabbed presets (presetTabbed / presetAgentTabbed) are now identical: both
|
|
8
|
+
// include the activity monitor, because the per-tab activity dot reveals itself
|
|
9
|
+
// only when a session reports activity (OSC 9;4 progress), so a plain shell
|
|
10
|
+
// simply never shows one. The generic-vs-agent distinction is now purely
|
|
11
|
+
// server-side — an agent server sets a status classifier that maps OSC 9
|
|
12
|
+
// notifications to done/needs-input. presetAgentTabbed is kept as an alias for
|
|
13
|
+
// its consumers (vibecli).
|
|
12
14
|
|
|
13
15
|
import type { TerminalFeature } from "./kernel/types.js";
|
|
14
16
|
import { clipboard } from "./features/clipboard.js";
|
|
@@ -19,7 +21,6 @@ import { connectionBanner } from "./features/connection-banner.js";
|
|
|
19
21
|
import { mobileToolbar } from "./features/mobile-toolbar.js";
|
|
20
22
|
import { tabs } from "./features/tabs.js";
|
|
21
23
|
import { activityMonitor } from "./features/activity-monitor.js";
|
|
22
|
-
import type { ActivityMonitorApi } from "./features/activity-monitor.js";
|
|
23
24
|
import { animations } from "./features/animations.js";
|
|
24
25
|
|
|
25
26
|
/** Single-pane desktop UI: context menu, clipboard, scroll-to-bottom, predictive
|
|
@@ -42,35 +43,41 @@ export function presetTouch(): TerminalFeature<unknown>[] {
|
|
|
42
43
|
return [...presetSingle(), mobileToolbar()];
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
// buildTabbed composes the tabbed UI
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// to
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
46
|
+
// buildTabbed composes the tabbed UI: the single-pane touch features, an
|
|
47
|
+
// externally-driven mobile toolbar (its grid is opened from a keyboard button in
|
|
48
|
+
// the tab bar, not its own toggle), the activity monitor (the status-SSE data
|
|
49
|
+
// source), tabs wired to both, and animations. The activity dot reveals itself
|
|
50
|
+
// per tab only when a session reports activity (OSC 9;4), so the monitor is
|
|
51
|
+
// always included — a plain shell just never reveals a dot. The toolbar and
|
|
52
|
+
// monitor are ordered before tabs because tabs reads their APIs via ctx.use.
|
|
53
|
+
function buildTabbed(): TerminalFeature<unknown>[] {
|
|
52
54
|
const kb = mobileToolbar({ externalToggle: true });
|
|
55
|
+
const monitor = activityMonitor();
|
|
53
56
|
return [
|
|
54
57
|
...presetSingle(),
|
|
55
58
|
kb,
|
|
56
|
-
|
|
57
|
-
tabs({ keyboardToggle: kb,
|
|
59
|
+
monitor,
|
|
60
|
+
tabs({ keyboardToggle: kb, activityMonitor: monitor }),
|
|
58
61
|
animations(),
|
|
59
62
|
];
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
/**
|
|
63
|
-
* and animations.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
65
|
+
/** Tabbed UI: the touch features, tabs, the mobile keyboard bar, the activity
|
|
66
|
+
* monitor, and animations. Requires a server that speaks the session API
|
|
67
|
+
* (`/api/sessions`, `/ws?session=`, and the status SSE `/api/sessions/events`),
|
|
68
|
+
* such as `web-terminal-server` or `vibecli`. Each tab's title follows the
|
|
69
|
+
* process OSC 0/2 window title when the program sets one, else the last
|
|
70
|
+
* submitted line; each tab's activity dot stays hidden until its session
|
|
71
|
+
* reports activity via OSC 9;4 progress (kiro-cli, Claude Code, …), so a plain
|
|
72
|
+
* bash/sh keeps clean, label-only tabs. */
|
|
66
73
|
export function presetTabbed(): TerminalFeature<unknown>[] {
|
|
67
74
|
return buildTabbed();
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
/**
|
|
71
|
-
*
|
|
72
|
-
* server
|
|
73
|
-
*
|
|
77
|
+
/** Alias of presetTabbed, kept for the agent-shell consumer (vibecli). The UI is
|
|
78
|
+
* identical — the generic-vs-agent distinction is now purely server-side: an
|
|
79
|
+
* agent server sets a status classifier (mapping OSC 9 notifications to
|
|
80
|
+
* done/needs-input), and each tab's dot reveals itself from OSC 9;4 either way. */
|
|
74
81
|
export function presetAgentTabbed(): TerminalFeature<unknown>[] {
|
|
75
|
-
return buildTabbed(
|
|
82
|
+
return buildTabbed();
|
|
76
83
|
}
|