@camstack/addon-terminal 0.1.12 → 0.1.15
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/dist/addon.js
CHANGED
|
@@ -22,9 +22,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
enumerable: true
|
|
23
23
|
}) : target, mod));
|
|
24
24
|
//#endregion
|
|
25
|
+
let node_child_process = require("node:child_process");
|
|
25
26
|
let node_fs = require("node:fs");
|
|
26
|
-
let node_module = require("node:module");
|
|
27
27
|
let node_path = require("node:path");
|
|
28
|
+
node_path = __toESM(node_path);
|
|
29
|
+
let node_url = require("node:url");
|
|
30
|
+
let node_module = require("node:module");
|
|
28
31
|
let sharp = require("sharp");
|
|
29
32
|
sharp = __toESM(sharp);
|
|
30
33
|
let node_http = require("node:http");
|
|
@@ -18428,24 +18431,28 @@ var snapshotCapability = {
|
|
|
18428
18431
|
*
|
|
18429
18432
|
* `getSnapshotOverview` is cache-only by contract: it answers from whatever
|
|
18430
18433
|
* the wrapper happens to hold and never captures. Under D93 the client
|
|
18431
|
-
* versions its image URL on that answer, and an image REQUEST
|
|
18432
|
-
*
|
|
18433
|
-
*
|
|
18434
|
-
*
|
|
18435
|
-
*
|
|
18436
|
-
*
|
|
18437
|
-
*
|
|
18438
|
-
*
|
|
18434
|
+
* versions its image URL on that answer, and an image REQUEST was the only
|
|
18435
|
+
* demand signal. Both of those are satisfiable by the client's own image
|
|
18436
|
+
* cache — `expo-image` is URL-keyed and never revalidates — so a URL painted
|
|
18437
|
+
* in a previous session comes off disk with no network, no demand, and no
|
|
18438
|
+
* capture. Measured on the live hub: reopening after two minutes idle
|
|
18439
|
+
* painted 15 of 16 tiles at **168 s old** with zero HTTP requests, and the
|
|
18440
|
+
* fleet only recovered because a later poll happened to observe a different
|
|
18441
|
+
* identity.
|
|
18439
18442
|
*
|
|
18440
18443
|
* ## The two properties that fix it
|
|
18441
18444
|
*
|
|
18442
18445
|
* **It is an RPC, so no client cache can answer it.** The demand signal
|
|
18443
|
-
* always reaches the wrapper. This method therefore
|
|
18444
|
-
*
|
|
18445
|
-
*
|
|
18446
|
-
*
|
|
18447
|
-
*
|
|
18448
|
-
*
|
|
18446
|
+
* always reaches the wrapper. This method therefore CAPTURES, where
|
|
18447
|
+
* `getSnapshotOverview` must never (D93) — the distinction is not "one is
|
|
18448
|
+
* newer" but that the overview poll is app-wide (a capturing overview would
|
|
18449
|
+
* dial every camera on the install) while this is called by a rendered
|
|
18450
|
+
* surface naming the tiles it is actually painting, at the width it is
|
|
18451
|
+
* painting them.
|
|
18452
|
+
*
|
|
18453
|
+
* Since 2026-08-11 this is the ONLY thing that refreshes a snapshot: the
|
|
18454
|
+
* server-side keep-warm loop was removed (operator directive — on-demand,
|
|
18455
|
+
* always), so a camera nobody is looking at costs nothing at all.
|
|
18449
18456
|
*
|
|
18450
18457
|
* **It waits, briefly and boundedly, for the capture it triggered.** The
|
|
18451
18458
|
* returned `capturedAt` is the frame the link will serve, not the frame the
|
|
@@ -27414,9 +27421,10 @@ var DeclaredDevices = class {
|
|
|
27414
27421
|
}
|
|
27415
27422
|
const integrationId = spec.integrationId ?? await this.ensureIntegration(spec.integrationName);
|
|
27416
27423
|
const index = await this.readIndex();
|
|
27424
|
+
const live = await this.readLiveByStableId();
|
|
27417
27425
|
const outcomes = [];
|
|
27418
27426
|
for (const declaration of spec.devices) {
|
|
27419
|
-
const outcome = await this.applyDeclaration(declaration, integrationId, index);
|
|
27427
|
+
const outcome = await this.applyDeclaration(declaration, integrationId, index, live);
|
|
27420
27428
|
if (outcome !== null) outcomes.push(outcome);
|
|
27421
27429
|
}
|
|
27422
27430
|
return {
|
|
@@ -27462,6 +27470,26 @@ var DeclaredDevices = class {
|
|
|
27462
27470
|
return new Map(rows.map((row) => [row.stableId, row]));
|
|
27463
27471
|
}
|
|
27464
27472
|
/**
|
|
27473
|
+
* Devices this kernel already has CONSTRUCTED, by stableId.
|
|
27474
|
+
*
|
|
27475
|
+
* Distinct from {@link readIndex}, and the distinction is the bug: the index
|
|
27476
|
+
* is persisted rows, this is live objects. A row without an object must be
|
|
27477
|
+
* adopted; an object must be left exactly as it is.
|
|
27478
|
+
*
|
|
27479
|
+
* Failure is non-fatal and deliberately so — an empty map degrades to the
|
|
27480
|
+
* previous behaviour (attempt the adopt) rather than skipping a device that
|
|
27481
|
+
* genuinely needs bringing up.
|
|
27482
|
+
*/
|
|
27483
|
+
async readLiveByStableId() {
|
|
27484
|
+
try {
|
|
27485
|
+
const devices = await this.ports.devices.getAll();
|
|
27486
|
+
return new Map(devices.map((device) => [device.stableId, device]));
|
|
27487
|
+
} catch (err) {
|
|
27488
|
+
this.ports.logger.warn("could not read live devices — falling back to adopt-by-row", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
27489
|
+
return /* @__PURE__ */ new Map();
|
|
27490
|
+
}
|
|
27491
|
+
}
|
|
27492
|
+
/**
|
|
27465
27493
|
* One declaration: adopt what exists, create what does not.
|
|
27466
27494
|
*
|
|
27467
27495
|
* The create branch is the destructive one — it seeds `initialMeta`, and
|
|
@@ -27470,8 +27498,15 @@ var DeclaredDevices = class {
|
|
|
27470
27498
|
* the declared name over the operator's rename. D49: that branch needs a
|
|
27471
27499
|
* second read to agree.
|
|
27472
27500
|
*/
|
|
27473
|
-
async applyDeclaration(declaration, integrationId, index) {
|
|
27501
|
+
async applyDeclaration(declaration, integrationId, index, live) {
|
|
27474
27502
|
try {
|
|
27503
|
+
const alreadyLive = live.get(declaration.stableId);
|
|
27504
|
+
if (alreadyLive !== void 0) return {
|
|
27505
|
+
stableId: declaration.stableId,
|
|
27506
|
+
deviceId: alreadyLive.id,
|
|
27507
|
+
device: alreadyLive,
|
|
27508
|
+
created: false
|
|
27509
|
+
};
|
|
27475
27510
|
let existing = index.get(declaration.stableId);
|
|
27476
27511
|
if (existing === void 0) {
|
|
27477
27512
|
existing = (await this.readIndex()).get(declaration.stableId);
|
|
@@ -33345,6 +33380,46 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
|
|
|
33345
33380
|
DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
33346
33381
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
33347
33382
|
//#endregion
|
|
33383
|
+
//#region src/curses-shim.ts
|
|
33384
|
+
/**
|
|
33385
|
+
* macOS repair for the managed Python's curses.
|
|
33386
|
+
*
|
|
33387
|
+
* The python-build-standalone distribution the dependency resolver installs on
|
|
33388
|
+
* macOS links ncurses statically with the terminfo database compiled OUT
|
|
33389
|
+
* (`--disable-database`) and an ABI mismatch that misparses every numeric
|
|
33390
|
+
* capability of its five built-in fallback terminals (`colors=-1`,
|
|
33391
|
+
* `cols=524368`). `has_colors()` is therefore False under every TERM value and
|
|
33392
|
+
* `init_pair` is hard-blocked, so glances takes its B&W branch and paints the
|
|
33393
|
+
* whole screen with SGR conceal/reverse/alt-charset — the "text missing,
|
|
33394
|
+
* light bars instead" terminal camera frames. No environment variable can fix
|
|
33395
|
+
* it: the code that would read TERMINFO/TERMINFO_DIRS was never compiled in.
|
|
33396
|
+
*
|
|
33397
|
+
* The repair ships prebuilt `_curses` / `_curses_panel` extension modules
|
|
33398
|
+
* linked against the system `/usr/lib/libncurses.5.4.dylib` (universal2) and
|
|
33399
|
+
* puts their directory on PYTHONPATH at PTY spawn — PYTHONPATH precedes
|
|
33400
|
+
* lib-dynload on sys.path, so the working modules shadow the broken ones with
|
|
33401
|
+
* no change to the managed distribution. The filename is ABI-locked to
|
|
33402
|
+
* cpython-312: a managed-python bump to 3.13 makes the shim silently inert
|
|
33403
|
+
* (python ignores a mismatched ABI tag), which is why the addon probes
|
|
33404
|
+
* `tigetnum("colors")` at startup and logs loudly when colours are missing.
|
|
33405
|
+
*/
|
|
33406
|
+
/** `<addonRoot>/assets/curses-shim/py312`, valid from both src/ and dist/. */
|
|
33407
|
+
function cursesShimDir() {
|
|
33408
|
+
return node_path.default.join(node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)), "..", "assets", "curses-shim", "py312");
|
|
33409
|
+
}
|
|
33410
|
+
function cursesShimDirExists(shimDir) {
|
|
33411
|
+
return (0, node_fs.existsSync)(shimDir);
|
|
33412
|
+
}
|
|
33413
|
+
/**
|
|
33414
|
+
* The env to merge into the glances profile, or undefined when the shim does
|
|
33415
|
+
* not apply (non-macOS, or the assets were not shipped).
|
|
33416
|
+
*/
|
|
33417
|
+
function resolveGlancesCursesShimEnv(options) {
|
|
33418
|
+
if (options.platform !== "darwin" || !options.shimDirExists) return void 0;
|
|
33419
|
+
const existing = options.existingPythonPath?.trim();
|
|
33420
|
+
return { PYTHONPATH: existing ? `${options.shimDir}:${existing}` : options.shimDir };
|
|
33421
|
+
}
|
|
33422
|
+
//#endregion
|
|
33348
33423
|
//#region src/pty.ts
|
|
33349
33424
|
/**
|
|
33350
33425
|
* Minimal pty abstraction. The manager depends on this interface, never on
|
|
@@ -33475,14 +33550,253 @@ function buildTerminalInstanceCameraDeclarations(instances) {
|
|
|
33475
33550
|
function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
|
|
33476
33551
|
return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
|
|
33477
33552
|
}
|
|
33553
|
+
//#endregion
|
|
33554
|
+
//#region src/terminal-cell-runs.ts
|
|
33555
|
+
var TERMINAL_DEFAULT_FG = "#d7dce2";
|
|
33556
|
+
var TERMINAL_DEFAULT_BG = "#0b0d10";
|
|
33557
|
+
/**
|
|
33558
|
+
* The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
|
|
33559
|
+
* frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
|
|
33560
|
+
* whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
|
|
33561
|
+
* near-black background at 13px. Index 7 IS the default foreground, so plain
|
|
33562
|
+
* `CSI 37m` text renders identically to unstyled text.
|
|
33563
|
+
*/
|
|
33564
|
+
var TERMINAL_ANSI_PALETTE = [
|
|
33565
|
+
"#282c34",
|
|
33566
|
+
"#e06c75",
|
|
33567
|
+
"#98c379",
|
|
33568
|
+
"#e5c07b",
|
|
33569
|
+
"#61afef",
|
|
33570
|
+
"#c678dd",
|
|
33571
|
+
"#56b6c2",
|
|
33572
|
+
TERMINAL_DEFAULT_FG,
|
|
33573
|
+
"#5c6370",
|
|
33574
|
+
"#ef596f",
|
|
33575
|
+
"#89ca78",
|
|
33576
|
+
"#f0c674",
|
|
33577
|
+
"#6cb6ff",
|
|
33578
|
+
"#d55fde",
|
|
33579
|
+
"#2bbac5",
|
|
33580
|
+
"#ffffff"
|
|
33581
|
+
];
|
|
33582
|
+
/** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
|
|
33583
|
+
var TERMINAL_CUBE_LEVELS = [
|
|
33584
|
+
0,
|
|
33585
|
+
95,
|
|
33586
|
+
135,
|
|
33587
|
+
175,
|
|
33588
|
+
215,
|
|
33589
|
+
255
|
|
33590
|
+
];
|
|
33591
|
+
var TERMINAL_CUBE_FIRST = 16;
|
|
33592
|
+
var TERMINAL_GRAYSCALE_FIRST = 232;
|
|
33593
|
+
var TERMINAL_GRAYSCALE_BASE = 8;
|
|
33594
|
+
var TERMINAL_GRAYSCALE_STEP = 10;
|
|
33595
|
+
/** SGR 2 keeps the foreground legible; it must not become the background. */
|
|
33596
|
+
var TERMINAL_DIM_WEIGHT = .6;
|
|
33597
|
+
function channel(value) {
|
|
33598
|
+
return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
|
|
33599
|
+
}
|
|
33600
|
+
function hex(red, green, blue) {
|
|
33601
|
+
return `#${channel(red)}${channel(green)}${channel(blue)}`;
|
|
33602
|
+
}
|
|
33603
|
+
function parseHex(color) {
|
|
33604
|
+
return [
|
|
33605
|
+
Number.parseInt(color.slice(1, 3), 16),
|
|
33606
|
+
Number.parseInt(color.slice(3, 5), 16),
|
|
33607
|
+
Number.parseInt(color.slice(5, 7), 16)
|
|
33608
|
+
];
|
|
33609
|
+
}
|
|
33610
|
+
/** Resolve an xterm palette index (0-255) to a hex colour. */
|
|
33611
|
+
function terminalPaletteColor(index) {
|
|
33612
|
+
const ansi = TERMINAL_ANSI_PALETTE[index];
|
|
33613
|
+
if (ansi !== void 0) return ansi;
|
|
33614
|
+
if (index >= TERMINAL_GRAYSCALE_FIRST) {
|
|
33615
|
+
const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
|
|
33616
|
+
return hex(level, level, level);
|
|
33617
|
+
}
|
|
33618
|
+
if (index >= TERMINAL_CUBE_FIRST) {
|
|
33619
|
+
const offset = index - TERMINAL_CUBE_FIRST;
|
|
33620
|
+
return hex(TERMINAL_CUBE_LEVELS[Math.floor(offset / 36) % 6] ?? 0, TERMINAL_CUBE_LEVELS[Math.floor(offset / 6) % 6] ?? 0, TERMINAL_CUBE_LEVELS[offset % 6] ?? 0);
|
|
33621
|
+
}
|
|
33622
|
+
return TERMINAL_DEFAULT_FG;
|
|
33623
|
+
}
|
|
33624
|
+
/** Resolve a 0xRRGGBB truecolor value to a hex colour. */
|
|
33625
|
+
function terminalRgbColor(value) {
|
|
33626
|
+
return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
|
|
33627
|
+
}
|
|
33628
|
+
function blend(color, toward, weight) {
|
|
33629
|
+
const [red, green, blue] = parseHex(color);
|
|
33630
|
+
const [targetRed, targetGreen, targetBlue] = parseHex(toward);
|
|
33631
|
+
return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
|
|
33632
|
+
}
|
|
33633
|
+
function resolveForeground(cell) {
|
|
33634
|
+
if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
|
|
33635
|
+
if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
|
|
33636
|
+
return TERMINAL_DEFAULT_FG;
|
|
33637
|
+
}
|
|
33638
|
+
function resolveBackground(cell) {
|
|
33639
|
+
if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
|
|
33640
|
+
if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
|
|
33641
|
+
return TERMINAL_DEFAULT_BG;
|
|
33642
|
+
}
|
|
33643
|
+
/**
|
|
33644
|
+
* Resolve one cell's attributes into concrete colours.
|
|
33645
|
+
*
|
|
33646
|
+
* Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
|
|
33647
|
+
* defaults is still a visible swap rather than a no-op — that is how a selected
|
|
33648
|
+
* or highlighted row in Glances reads. Invisible is then conceal-by-equality
|
|
33649
|
+
* (foreground painted in its own background): the cell keeps its columns, which
|
|
33650
|
+
* a dropped cell would not, and dropping it would shift the whole rest of the
|
|
33651
|
+
* row left.
|
|
33652
|
+
*/
|
|
33653
|
+
function resolveCellStyle(cell) {
|
|
33654
|
+
const inverse = cell.isInverse() !== 0;
|
|
33655
|
+
const plainFg = resolveForeground(cell);
|
|
33656
|
+
const plainBg = resolveBackground(cell);
|
|
33657
|
+
const background = inverse ? plainFg : plainBg;
|
|
33658
|
+
let foreground = inverse ? plainBg : plainFg;
|
|
33659
|
+
if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
|
|
33660
|
+
if (cell.isInvisible() !== 0) foreground = background;
|
|
33661
|
+
return {
|
|
33662
|
+
fg: foreground === "#d7dce2" ? null : foreground,
|
|
33663
|
+
bg: background === "#0b0d10" ? null : background,
|
|
33664
|
+
bold: cell.isBold() !== 0
|
|
33665
|
+
};
|
|
33666
|
+
}
|
|
33667
|
+
function sameStyle(left, right) {
|
|
33668
|
+
return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
|
|
33669
|
+
}
|
|
33670
|
+
/**
|
|
33671
|
+
* Merge adjacent same-style cells into runs, then drop the trailing run of
|
|
33672
|
+
* default-styled whitespace so a row costs what it draws — the same trim
|
|
33673
|
+
* `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
|
|
33674
|
+
* a green bar of spaces out to the right margin is a pixel Glances drew.
|
|
33675
|
+
*/
|
|
33676
|
+
function buildCellRuns(cells) {
|
|
33677
|
+
const runs = [];
|
|
33678
|
+
let text = "";
|
|
33679
|
+
let style = null;
|
|
33680
|
+
for (const cell of cells) {
|
|
33681
|
+
if (style !== null && sameStyle(style, cell.style)) {
|
|
33682
|
+
text += cell.text;
|
|
33683
|
+
continue;
|
|
33684
|
+
}
|
|
33685
|
+
if (style !== null) runs.push({
|
|
33686
|
+
text,
|
|
33687
|
+
...style
|
|
33688
|
+
});
|
|
33689
|
+
text = cell.text;
|
|
33690
|
+
style = cell.style;
|
|
33691
|
+
}
|
|
33692
|
+
if (style !== null) runs.push({
|
|
33693
|
+
text,
|
|
33694
|
+
...style
|
|
33695
|
+
});
|
|
33696
|
+
while (runs.length > 0) {
|
|
33697
|
+
const last = runs[runs.length - 1];
|
|
33698
|
+
if (last === void 0 || last.bg !== null) break;
|
|
33699
|
+
const trimmed = last.text.replace(/\s+$/u, "");
|
|
33700
|
+
if (trimmed === last.text) break;
|
|
33701
|
+
if (trimmed === "") {
|
|
33702
|
+
runs.pop();
|
|
33703
|
+
continue;
|
|
33704
|
+
}
|
|
33705
|
+
runs[runs.length - 1] = {
|
|
33706
|
+
...last,
|
|
33707
|
+
text: trimmed
|
|
33708
|
+
};
|
|
33709
|
+
break;
|
|
33710
|
+
}
|
|
33711
|
+
return runs;
|
|
33712
|
+
}
|
|
33713
|
+
/**
|
|
33714
|
+
* Monospace families to try, in order — NOT one family and a generic.
|
|
33715
|
+
*
|
|
33716
|
+
* A terminal screen is mostly box-drawing and block characters, and a font
|
|
33717
|
+
* without them renders the frame as noise rather than as missing detail.
|
|
33718
|
+
* `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
|
|
33719
|
+
* DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
|
|
33720
|
+
* coverage is not, and its Glances camera came out unreadable while the hub's
|
|
33721
|
+
* was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
|
|
33722
|
+
*
|
|
33723
|
+
* `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
|
|
33724
|
+
* present on every install, and derived from DejaVu Sans Mono — the same glyph
|
|
33725
|
+
* coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
|
|
33726
|
+
* generic stays last so a host with none of them still draws something.
|
|
33727
|
+
*/
|
|
33728
|
+
var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
|
|
33729
|
+
var TERMINAL_FONT_SIZE = 13;
|
|
33730
|
+
var TERMINAL_TEXT_MARGIN_X = 8;
|
|
33731
|
+
var TERMINAL_ROW_HEIGHT = 15;
|
|
33732
|
+
var TERMINAL_BASELINE_Y = 18;
|
|
33733
|
+
/**
|
|
33734
|
+
* Distance from a row's baseline up to the top of its cell box. Chosen so
|
|
33735
|
+
* consecutive rows tile exactly: row N's box runs from `baseline - this` for
|
|
33736
|
+
* `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
|
|
33737
|
+
* bar that stopped short would draw as stripes across a `CSI 42m` panel.
|
|
33738
|
+
*/
|
|
33739
|
+
var TERMINAL_CELL_ASCENT = 11.5;
|
|
33740
|
+
var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
|
|
33741
|
+
/** Two decimals is under a tenth of a pixel and keeps the SVG small. */
|
|
33742
|
+
function coordinate(value) {
|
|
33743
|
+
return String(Number(value.toFixed(2)));
|
|
33744
|
+
}
|
|
33478
33745
|
function escapeXml(value) {
|
|
33479
33746
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
33480
33747
|
}
|
|
33481
|
-
/**
|
|
33482
|
-
|
|
33483
|
-
|
|
33484
|
-
|
|
33485
|
-
|
|
33748
|
+
/**
|
|
33749
|
+
* Render already-interpreted terminal rows into a compact MJPEG frame.
|
|
33750
|
+
*
|
|
33751
|
+
* `xml:space="preserve"` is load-bearing, not tidiness. SVG `<text>` collapses
|
|
33752
|
+
* runs of whitespace by default, and a terminal's entire column alignment IS
|
|
33753
|
+
* runs of whitespace — Glances pads every field with spaces. Without it the
|
|
33754
|
+
* frame drew each line at roughly half its true width, crammed into the
|
|
33755
|
+
* top-left of a mostly-black image, while the SAME session over `attach`
|
|
33756
|
+
* looked perfect — which is exactly how the operator reported it. Measured in
|
|
33757
|
+
* the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
|
|
33758
|
+
* collapsed against 178 px preserved.
|
|
33759
|
+
*
|
|
33760
|
+
* Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
|
|
33761
|
+
* never appended to the one before it, so the background rects and the glyphs
|
|
33762
|
+
* are placed off the same grid and cannot drift apart. `textLength` is emitted
|
|
33763
|
+
* with it because it is the correct declaration and renderers that honour it
|
|
33764
|
+
* get an exact grid — but it is not what makes this work: librsvg, which sharp
|
|
33765
|
+
* uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
|
|
33766
|
+
* 600 px still drew its natural 937 px). The anchoring is the guarantee.
|
|
33767
|
+
*/
|
|
33768
|
+
function renderTerminalSvg(rows) {
|
|
33769
|
+
const backgrounds = [];
|
|
33770
|
+
const texts = [];
|
|
33771
|
+
rows.slice(0, 40).forEach((row, index) => {
|
|
33772
|
+
const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
|
|
33773
|
+
const top = baseline - TERMINAL_CELL_ASCENT;
|
|
33774
|
+
let column = 0;
|
|
33775
|
+
for (const run of row) {
|
|
33776
|
+
if (column >= 120) break;
|
|
33777
|
+
const clipped = clipRun(run, 120 - column);
|
|
33778
|
+
const columns = [...clipped].length;
|
|
33779
|
+
if (columns === 0) continue;
|
|
33780
|
+
const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
|
|
33781
|
+
const width = columns * TERMINAL_CELL_WIDTH;
|
|
33782
|
+
if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
|
|
33783
|
+
if (clipped.trim() !== "") {
|
|
33784
|
+
const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
|
|
33785
|
+
const weight = run.bold ? " font-weight=\"bold\"" : "";
|
|
33786
|
+
texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
|
|
33787
|
+
}
|
|
33788
|
+
column += columns;
|
|
33789
|
+
}
|
|
33790
|
+
});
|
|
33791
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${String(960)}" height="${String(640)}"><rect width="100%" height="100%" fill="${TERMINAL_DEFAULT_BG}"/>${backgrounds.join("")}<g fill="${TERMINAL_DEFAULT_FG}" font-family="${TERMINAL_FONT_STACK}" font-size="${String(TERMINAL_FONT_SIZE)}">${texts.join("")}</g></svg>`;
|
|
33792
|
+
}
|
|
33793
|
+
/** Cut a run to the columns still left in the row, by code point not unit. */
|
|
33794
|
+
function clipRun(run, remaining) {
|
|
33795
|
+
const points = [...run.text];
|
|
33796
|
+
return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
|
|
33797
|
+
}
|
|
33798
|
+
async function renderTerminalJpeg(rows) {
|
|
33799
|
+
return (0, sharp.default)(Buffer.from(renderTerminalSvg(rows))).jpeg({
|
|
33486
33800
|
quality: 82,
|
|
33487
33801
|
chromaSubsampling: "4:2:0"
|
|
33488
33802
|
}).toBuffer();
|
|
@@ -38388,6 +38702,40 @@ function createXtermScreen(cols, rows) {
|
|
|
38388
38702
|
for (let row = 0; row < term.rows; row += 1) lines.push(buffer.getLine(buffer.viewportY + row)?.translateToString(true) ?? "");
|
|
38389
38703
|
return lines;
|
|
38390
38704
|
},
|
|
38705
|
+
/**
|
|
38706
|
+
* The colour-bearing read. `translateToString` — the only cell read this
|
|
38707
|
+
* screen used to offer — keeps characters and discards every attribute,
|
|
38708
|
+
* which is why the terminal camera rendered a fully-coloured Glances in
|
|
38709
|
+
* grey. Walking cells is the only way to get the attributes back.
|
|
38710
|
+
*
|
|
38711
|
+
* A zero-width cell is the tail half of a wide glyph (CJK, some emoji);
|
|
38712
|
+
* its characters already came back with the leading cell, so skipping it
|
|
38713
|
+
* keeps the text right. `getNullCell()` is reused across the whole screen
|
|
38714
|
+
* because `getCell` allocates otherwise — 4800 objects per frame.
|
|
38715
|
+
*/
|
|
38716
|
+
styledLines: () => {
|
|
38717
|
+
const buffer = term.buffer.active;
|
|
38718
|
+
const scratch = buffer.getNullCell();
|
|
38719
|
+
const styled = [];
|
|
38720
|
+
for (let row = 0; row < term.rows; row += 1) {
|
|
38721
|
+
const line = buffer.getLine(buffer.viewportY + row);
|
|
38722
|
+
if (!line) {
|
|
38723
|
+
styled.push([]);
|
|
38724
|
+
continue;
|
|
38725
|
+
}
|
|
38726
|
+
const cells = [];
|
|
38727
|
+
for (let column = 0; column < line.length; column += 1) {
|
|
38728
|
+
const cell = line.getCell(column, scratch);
|
|
38729
|
+
if (!cell || cell.getWidth() === 0) continue;
|
|
38730
|
+
cells.push({
|
|
38731
|
+
text: cell.getChars() || " ",
|
|
38732
|
+
style: resolveCellStyle(cell)
|
|
38733
|
+
});
|
|
38734
|
+
}
|
|
38735
|
+
styled.push(buildCellRuns(cells));
|
|
38736
|
+
}
|
|
38737
|
+
return styled;
|
|
38738
|
+
},
|
|
38391
38739
|
dispose: () => {
|
|
38392
38740
|
term.dispose();
|
|
38393
38741
|
}
|
|
@@ -38398,6 +38746,9 @@ function createXtermScreen(cols, rows) {
|
|
|
38398
38746
|
var MJPEG_BOUNDARY = "camstack-terminal-frame";
|
|
38399
38747
|
var SESSION_IDLE_MS = 3e4;
|
|
38400
38748
|
var SNAPSHOT_STARTUP_WAIT_MS = 1500;
|
|
38749
|
+
var SNAPSHOT_SETTLE_QUIET_MS = 300;
|
|
38750
|
+
var SNAPSHOT_SETTLE_BUDGET_MS = 3e3;
|
|
38751
|
+
var SNAPSHOT_SETTLE_MAX_PULLS = 20;
|
|
38401
38752
|
var CLOSE_RETRY_BASE_MS = 50;
|
|
38402
38753
|
var CLOSE_RETRY_MAX_MS = 1e3;
|
|
38403
38754
|
var CLOSE_ATTEMPTS_PER_PASS = 3;
|
|
@@ -38522,20 +38873,36 @@ var TerminalCameraRelay = class {
|
|
|
38522
38873
|
if (state.openPromise === opening) state.openPromise = null;
|
|
38523
38874
|
}
|
|
38524
38875
|
}
|
|
38876
|
+
/** Replay one output batch into the relay screen; true = the session exited. */
|
|
38877
|
+
applyBatch(state, batch) {
|
|
38878
|
+
if (batch.reset) {
|
|
38879
|
+
state.screen.dispose();
|
|
38880
|
+
state.screen = createXtermScreen(120, 40);
|
|
38881
|
+
if (batch.snapshot) state.screen.write(batch.snapshot);
|
|
38882
|
+
}
|
|
38883
|
+
let exited = false;
|
|
38884
|
+
for (const event of batch.events) if (event.kind === "data") state.screen.write(event.data);
|
|
38885
|
+
else {
|
|
38886
|
+
state.sessionId = null;
|
|
38887
|
+
exited = true;
|
|
38888
|
+
}
|
|
38889
|
+
state.cursor = exited ? 0 : batch.cursor;
|
|
38890
|
+
return exited;
|
|
38891
|
+
}
|
|
38525
38892
|
async nextFrame(state, forceRender = false, waitForInitialOutput = false) {
|
|
38526
38893
|
if (state.framePromise) {
|
|
38527
38894
|
await state.framePromise;
|
|
38528
38895
|
return forceRender ? this.nextFrame(state, true, waitForInitialOutput) : state.jpeg ?? this.nextFrame(state, false, waitForInitialOutput);
|
|
38529
38896
|
}
|
|
38530
38897
|
const render = async () => {
|
|
38531
|
-
const
|
|
38898
|
+
const initialPull = state.sessionId === null && waitForInitialOutput;
|
|
38532
38899
|
const sessionId = await this.ensureSession(state);
|
|
38533
38900
|
let batch;
|
|
38534
38901
|
try {
|
|
38535
38902
|
batch = await this.api.pullOutput(state.nodeId, {
|
|
38536
38903
|
sessionId,
|
|
38537
38904
|
afterSeq: state.cursor,
|
|
38538
|
-
...
|
|
38905
|
+
...initialPull ? {
|
|
38539
38906
|
waitMs: SNAPSHOT_STARTUP_WAIT_MS,
|
|
38540
38907
|
waitForOutput: true
|
|
38541
38908
|
} : {}
|
|
@@ -38557,21 +38924,29 @@ var TerminalCameraRelay = class {
|
|
|
38557
38924
|
state.cursor = 0;
|
|
38558
38925
|
throw error;
|
|
38559
38926
|
}
|
|
38560
|
-
|
|
38561
|
-
|
|
38562
|
-
|
|
38563
|
-
|
|
38564
|
-
|
|
38565
|
-
|
|
38566
|
-
|
|
38567
|
-
|
|
38568
|
-
|
|
38569
|
-
|
|
38927
|
+
let exited = this.applyBatch(state, batch);
|
|
38928
|
+
if (initialPull && !exited) {
|
|
38929
|
+
const deadline = Date.now() + SNAPSHOT_SETTLE_BUDGET_MS;
|
|
38930
|
+
for (let pulls = 0; pulls < SNAPSHOT_SETTLE_MAX_PULLS && Date.now() < deadline; pulls += 1) {
|
|
38931
|
+
let settle;
|
|
38932
|
+
try {
|
|
38933
|
+
settle = await this.api.pullOutput(state.nodeId, {
|
|
38934
|
+
sessionId,
|
|
38935
|
+
afterSeq: state.cursor,
|
|
38936
|
+
waitMs: SNAPSHOT_SETTLE_QUIET_MS,
|
|
38937
|
+
waitForOutput: true
|
|
38938
|
+
});
|
|
38939
|
+
} catch {
|
|
38940
|
+
break;
|
|
38941
|
+
}
|
|
38942
|
+
const active = settle.reset || settle.events.some((event) => event.kind === "data");
|
|
38943
|
+
exited = this.applyBatch(state, settle);
|
|
38944
|
+
if (exited || !active) break;
|
|
38945
|
+
}
|
|
38570
38946
|
}
|
|
38571
|
-
state.cursor = exited ? 0 : batch.cursor;
|
|
38572
38947
|
await state.screen.flush();
|
|
38573
38948
|
if (forceRender || state.jpeg === null || state.renderedCursor !== state.cursor) {
|
|
38574
|
-
state.jpeg = await renderTerminalJpeg(state.screen.
|
|
38949
|
+
state.jpeg = await renderTerminalJpeg(state.screen.styledLines());
|
|
38575
38950
|
state.renderedCursor = state.cursor;
|
|
38576
38951
|
}
|
|
38577
38952
|
return state.jpeg;
|
|
@@ -39020,7 +39395,8 @@ function buildProfiles(options) {
|
|
|
39020
39395
|
"-m",
|
|
39021
39396
|
"glances",
|
|
39022
39397
|
...options.glancesArgs ?? []
|
|
39023
|
-
]
|
|
39398
|
+
],
|
|
39399
|
+
...options.glancesEnv && Object.keys(options.glancesEnv).length > 0 ? { env: options.glancesEnv } : {}
|
|
39024
39400
|
});
|
|
39025
39401
|
}
|
|
39026
39402
|
if (options.allowShell) profiles.push({
|
|
@@ -39105,6 +39481,7 @@ var TerminalSessionManager = class {
|
|
|
39105
39481
|
glancesPath: opts.glancesPath,
|
|
39106
39482
|
glancesArgs: opts.glancesArgs,
|
|
39107
39483
|
glancesPythonPath: opts.glancesPythonPath,
|
|
39484
|
+
glancesEnv: opts.glancesEnv,
|
|
39108
39485
|
allowShell: opts.allowShell,
|
|
39109
39486
|
shellPath: opts.shellPath,
|
|
39110
39487
|
customProfiles: opts.customProfiles,
|
|
@@ -39402,6 +39779,18 @@ var TerminalSessionManager = class {
|
|
|
39402
39779
|
};
|
|
39403
39780
|
//#endregion
|
|
39404
39781
|
//#region src/addon.ts
|
|
39782
|
+
/**
|
|
39783
|
+
* Terminal addon — hosts interactive pty sessions running an allowlisted
|
|
39784
|
+
* profile (`monitor` → `btm`), streamed to the Admin UI over the data plane.
|
|
39785
|
+
*
|
|
39786
|
+
* Phase 1 of docs/design/2026-07-26-terminal-session-design.md: a real TTY good
|
|
39787
|
+
* enough to run a full-screen program. Phase 2 (streaming a session as a camera)
|
|
39788
|
+
* is a separate device-provider concern and does not change this addon's cap.
|
|
39789
|
+
*
|
|
39790
|
+
* Its own package (not a builtin) because it carries a native dependency
|
|
39791
|
+
* (`node-pty`) and should update via `camstack deploy` independently of the
|
|
39792
|
+
* framework, exactly like the node-av decoder addon.
|
|
39793
|
+
*/
|
|
39405
39794
|
var DEFAULTS = {
|
|
39406
39795
|
btmPath: "",
|
|
39407
39796
|
btmEnabled: true,
|
|
@@ -39434,12 +39823,21 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39434
39823
|
instanceMutationQueue = new TerminalInstanceMutationQueue();
|
|
39435
39824
|
terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
|
|
39436
39825
|
glancesPythonPath = "";
|
|
39826
|
+
glancesEnv;
|
|
39437
39827
|
constructor() {
|
|
39438
39828
|
super({ ...DEFAULTS });
|
|
39439
39829
|
}
|
|
39440
39830
|
async onInitialize() {
|
|
39441
39831
|
await warmNodePty();
|
|
39442
39832
|
this.glancesPythonPath = await this.ctx.deps.ensurePython() ?? "";
|
|
39833
|
+
const shimDir = cursesShimDir();
|
|
39834
|
+
this.glancesEnv = resolveGlancesCursesShimEnv({
|
|
39835
|
+
platform: process.platform,
|
|
39836
|
+
shimDir,
|
|
39837
|
+
shimDirExists: cursesShimDirExists(shimDir),
|
|
39838
|
+
existingPythonPath: process.env["PYTHONPATH"]
|
|
39839
|
+
});
|
|
39840
|
+
this.probeGlancesCursesColours();
|
|
39443
39841
|
const manager = new TerminalSessionManager({
|
|
39444
39842
|
spawn: createNodePtySpawner(),
|
|
39445
39843
|
screenFactory: createXtermScreen,
|
|
@@ -39455,6 +39853,7 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39455
39853
|
glancesPath: this.config.glancesPath,
|
|
39456
39854
|
glancesArgs: this.config.glancesArgs,
|
|
39457
39855
|
glancesPythonPath: this.glancesPythonPath,
|
|
39856
|
+
glancesEnv: this.glancesEnv,
|
|
39458
39857
|
allowShell: this.config.allowShell,
|
|
39459
39858
|
shellPath: this.config.shellPath,
|
|
39460
39859
|
maxSessions: this.config.maxSessions,
|
|
@@ -39500,6 +39899,39 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39500
39899
|
provider: manager
|
|
39501
39900
|
}];
|
|
39502
39901
|
}
|
|
39902
|
+
/**
|
|
39903
|
+
* Fire-and-forget canary: the managed python must see a colour-capable
|
|
39904
|
+
* curses, or glances paints its B&W theme (SGR conceal/reverse) and the
|
|
39905
|
+
* terminal camera frames lose their text. The shim filename is ABI-locked
|
|
39906
|
+
* (cpython-312), so a managed-python bump silently disables it — this probe
|
|
39907
|
+
* is what makes that failure loud instead of a garbled camera.
|
|
39908
|
+
*/
|
|
39909
|
+
probeGlancesCursesColours() {
|
|
39910
|
+
if (!this.glancesPythonPath) return;
|
|
39911
|
+
const probeEnv = {
|
|
39912
|
+
...process.env,
|
|
39913
|
+
...this.glancesEnv,
|
|
39914
|
+
TERM: "xterm-256color"
|
|
39915
|
+
};
|
|
39916
|
+
(0, node_child_process.execFile)(this.glancesPythonPath, ["-c", "import curses\ncurses.setupterm('xterm-256color')\nprint(curses.tigetnum('colors'))"], {
|
|
39917
|
+
env: probeEnv,
|
|
39918
|
+
timeout: 1e4
|
|
39919
|
+
}, (error, stdout) => {
|
|
39920
|
+
const colours = error ? NaN : Number.parseInt(stdout.trim(), 10);
|
|
39921
|
+
if (!Number.isFinite(colours) || colours <= 0) {
|
|
39922
|
+
this.ctx.logger.warn("glances curses reports no colours — terminal camera frames will render the B&W theme", { meta: {
|
|
39923
|
+
colours: Number.isFinite(colours) ? colours : null,
|
|
39924
|
+
shimApplied: this.glancesEnv !== void 0,
|
|
39925
|
+
error: error ? error instanceof Error ? error.message : String(error) : void 0
|
|
39926
|
+
} });
|
|
39927
|
+
return;
|
|
39928
|
+
}
|
|
39929
|
+
this.ctx.logger.debug("glances curses colours available", { meta: {
|
|
39930
|
+
colours,
|
|
39931
|
+
shimApplied: this.glancesEnv !== void 0
|
|
39932
|
+
} });
|
|
39933
|
+
});
|
|
39934
|
+
}
|
|
39503
39935
|
async onConfigChanged() {
|
|
39504
39936
|
this.replaceTerminalCameraTombstones(this.config.terminalCameraTombstones);
|
|
39505
39937
|
this.manager?.reconfigureProfiles({
|
|
@@ -39513,6 +39945,7 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39513
39945
|
glancesPath: this.config.glancesPath,
|
|
39514
39946
|
glancesArgs: this.config.glancesArgs,
|
|
39515
39947
|
glancesPythonPath: this.glancesPythonPath,
|
|
39948
|
+
glancesEnv: this.glancesEnv,
|
|
39516
39949
|
allowShell: this.config.allowShell,
|
|
39517
39950
|
shellPath: this.config.shellPath,
|
|
39518
39951
|
maxSessions: this.config.maxSessions,
|
|
@@ -39608,12 +40041,46 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39608
40041
|
this.migratedTerminalCameraConfigIds.add(outcome.device.id);
|
|
39609
40042
|
}
|
|
39610
40043
|
}
|
|
40044
|
+
if (outcome.created) await this.silenceAnalysisFor(outcome.device.id);
|
|
39611
40045
|
const nodeId = outcome.device.config.get("nodeId");
|
|
39612
40046
|
const profileId = outcome.device.config.get("profileId");
|
|
39613
40047
|
const profileAvailable = this.cameraProfilesByNode.get(nodeId)?.some((profile) => profile.profileId === profileId) ?? false;
|
|
39614
40048
|
outcome.device.setNodeOnline((onlineByNode.get(nodeId) ?? false) && profileAvailable);
|
|
39615
40049
|
}
|
|
39616
40050
|
}
|
|
40051
|
+
/**
|
|
40052
|
+
* A Terminal camera is a rendered screen. Object detection on it finds
|
|
40053
|
+
* nothing, forever, at full cost.
|
|
40054
|
+
*
|
|
40055
|
+
* Measured on the hub 2026-08-11, for ONE terminal camera: 19 frames per 10 s
|
|
40056
|
+
* through the detection pipeline at ~61 ms of inference each, plus 115
|
|
40057
|
+
* capture-scheduler requests a minute — against `detections=0`. Multiply by
|
|
40058
|
+
* one terminal per node and it is a standing tax on a hub that was already
|
|
40059
|
+
* shedding 86 % of its capture queue.
|
|
40060
|
+
*
|
|
40061
|
+
* Written through `setCameraSwitch`, which is the authority that already owns
|
|
40062
|
+
* this function — [D62] forbids a second store that disagrees with it. And
|
|
40063
|
+
* written ONLY on creation: an operator who deliberately turns detection back
|
|
40064
|
+
* on for a terminal must win, and a reconcile that re-asserted every pass
|
|
40065
|
+
* would silently overrule them once a minute.
|
|
40066
|
+
*/
|
|
40067
|
+
async silenceAnalysisFor(deviceId) {
|
|
40068
|
+
for (const switchId of ["object-detection", "audio-analysis"]) try {
|
|
40069
|
+
await this.ctx.api.pipelineOrchestrator.setCameraSwitch.mutate({
|
|
40070
|
+
deviceId,
|
|
40071
|
+
switchId,
|
|
40072
|
+
enabled: false
|
|
40073
|
+
});
|
|
40074
|
+
} catch (err) {
|
|
40075
|
+
this.ctx.logger.warn("could not switch off analysis for a Terminal camera", {
|
|
40076
|
+
tags: { deviceId },
|
|
40077
|
+
meta: {
|
|
40078
|
+
switchId,
|
|
40079
|
+
error: err instanceof Error ? err.message : String(err)
|
|
40080
|
+
}
|
|
40081
|
+
});
|
|
40082
|
+
}
|
|
40083
|
+
}
|
|
39617
40084
|
terminalInstanceControl() {
|
|
39618
40085
|
return {
|
|
39619
40086
|
listInstances: async () => this.terminalInstances().map((instance) => this.instanceInfo(instance)),
|
|
@@ -39925,11 +40392,17 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39925
40392
|
}
|
|
39926
40393
|
};
|
|
39927
40394
|
//#endregion
|
|
40395
|
+
exports.TERMINAL_DEFAULT_BG = TERMINAL_DEFAULT_BG;
|
|
40396
|
+
exports.TERMINAL_DEFAULT_FG = TERMINAL_DEFAULT_FG;
|
|
39928
40397
|
exports.TerminalAddon = TerminalAddon;
|
|
39929
40398
|
exports.TerminalSessionManager = TerminalSessionManager;
|
|
40399
|
+
exports.buildCellRuns = buildCellRuns;
|
|
39930
40400
|
exports.buildProfiles = buildProfiles;
|
|
39931
40401
|
exports.createNodePtySpawner = createNodePtySpawner;
|
|
39932
40402
|
exports.createTerminalDataPlaneHandler = createTerminalDataPlaneHandler;
|
|
39933
40403
|
exports.createXtermScreen = createXtermScreen;
|
|
39934
40404
|
exports.findProfile = findProfile;
|
|
40405
|
+
exports.resolveCellStyle = resolveCellStyle;
|
|
40406
|
+
exports.terminalPaletteColor = terminalPaletteColor;
|
|
40407
|
+
exports.terminalRgbColor = terminalRgbColor;
|
|
39935
40408
|
exports.warmNodePty = warmNodePty;
|