@camstack/addon-terminal 0.1.13 → 0.1.16

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.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  import { createRequire } from "node:module";
2
+ import { execFile } from "node:child_process";
2
3
  import { accessSync, chmodSync, constants, existsSync, statSync } from "node:fs";
3
- import { delimiter, dirname, join } from "node:path";
4
+ import path, { delimiter, dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
4
6
  import sharp from "sharp";
5
7
  import { createServer } from "node:http";
6
8
  //#region \0rolldown/runtime.js
@@ -7022,6 +7024,18 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7022
7024
  action: string().min(1),
7023
7025
  input: unknown()
7024
7026
  }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7027
+ //#endregion
7028
+ //#region ../types/dist/err-msg-IQTHeDzc.mjs
7029
+ /**
7030
+ import { errMsg } from '@camstack/types'
7031
+ * Extract a human-readable message from an unknown error value.
7032
+ * Replaces the ubiquitous `errMsg(err)` pattern.
7033
+ */
7034
+ function errMsg(err) {
7035
+ if (err instanceof Error) return err.message;
7036
+ if (typeof err === "string") return err;
7037
+ return String(err);
7038
+ }
7025
7039
  var EncodeProfileSchema = object({
7026
7040
  video: object({
7027
7041
  codec: _enum([
@@ -7259,8 +7273,31 @@ var AdoptionJobSchema = object({
7259
7273
  error: string().nullable()
7260
7274
  });
7261
7275
  /**
7262
- * Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
7263
- * pipeline functions an operator thinks in terms of.
7276
+ * Per-camera FUNCTION SWITCHES.
7277
+ *
7278
+ * ## The aggregate group is being withdrawn — the BADGE is not (D113)
7279
+ *
7280
+ * This file shipped as "the one coherent on/off surface over the pipeline
7281
+ * functions an operator thinks in terms of". The operator's verdict on
7282
+ * 2026-08-12 was that the coherent surface bought complexity and no clarity:
7283
+ * every function already had a settings page of its own, and a second place to
7284
+ * turn it off is a second place to look. Each switch is going back to its own
7285
+ * component's original options — detection to the detection-pipeline wrapper
7286
+ * binding, audio analysis to its own, recording to `RecordingConfig.enabled`
7287
+ * (which was always first-class; the switch was a veneer over
7288
+ * `recording.setDeviceConfig`), notifications to a notification-center
7289
+ * per-device setting, the two camera planes to their own components.
7290
+ *
7291
+ * What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
7292
+ * thing that lets a status surface say DISABLED instead of BROKEN, recomposed
7293
+ * straight from the authorities with no group in the middle. That rule was
7294
+ * never about a control panel.
7295
+ *
7296
+ * Everything else here — {@link CAMERA_SWITCH_CATALOG}, {@link CameraSwitch},
7297
+ * {@link deriveCameraSwitches}, the `pipelineOrchestrator.getCameraSwitches` /
7298
+ * `setCameraSwitch` pair — is a COMPATIBILITY surface for as long as deployed
7299
+ * viewers (v1.0.305) and the admin UI still call it. It is deleted when they
7300
+ * stop; nothing new may be built on it.
7264
7301
  *
7265
7302
  * ## This file adds no state
7266
7303
  *
@@ -7422,6 +7459,14 @@ var CameraSwitchGroupSchema = object({
7422
7459
  fetchedAt: number()
7423
7460
  });
7424
7461
  /**
7462
+ * The wrapper capability each wrapper-backed switch controls. Named constants
7463
+ * because the same strings appear in `legacy-migrations.ts`, in
7464
+ * `isCapActiveForDevice` call sites and in the fake harness — a typo in any of
7465
+ * them is a switch that silently writes a binding nobody reads.
7466
+ */
7467
+ var DETECTION_PIPELINE_CAP_NAME = "detection-pipeline";
7468
+ var AUDIO_ANALYSIS_CAP_NAME = "audio-analysis";
7469
+ /**
7425
7470
  * Ops-log — the durable, append-only operations audit shared by the
7426
7471
  * recordings and events management surfaces.
7427
7472
  *
@@ -17797,9 +17842,16 @@ var CameraStatusSchema = object({
17797
17842
  audio: CameraAudioStatusSchema.nullable(),
17798
17843
  recording: CameraRecordingStatusSchema.nullable(),
17799
17844
  /**
17800
- * Per-camera function switches an OPERATOR has turned off
17845
+ * Per-camera functions an OPERATOR has turned off
17801
17846
  * ([D61](../../../../docs/decisions/adr-0067.md)).
17802
17847
  *
17848
+ * Composed from the AUTHORITIES themselves — the wrapper bindings,
17849
+ * `RecordingConfig.enabled`, the notification mute, the broker's audio
17850
+ * policy, the camera's own microphone — via `composeSwitchedOff`, not from
17851
+ * the deprecated `getCameraSwitches` group ([D113](../../../../docs/decisions/adr-0113.md)).
17852
+ * The badge outlives the control panel: the panel was a convenience, this is
17853
+ * the difference between a camera being off and a camera being dead.
17854
+ *
17803
17855
  * This is the difference between DISABLED and BROKEN. A camera whose
17804
17856
  * `detection` block reports zero fps and whose `switchedOff` contains
17805
17857
  * `'object-detection'` was switched off by a person; the same camera with an
@@ -24886,10 +24938,42 @@ method(object({
24886
24938
  */
24887
24939
  /** Playback-speed multiplier for the render (1 = realtime). */
24888
24940
  var ExportSpeedSchema = number().min(.25).max(32);
24941
+ /**
24942
+ * One dense interval, in SECONDS FROM THE EXPORT'S OWN `fromMs`.
24943
+ *
24944
+ * Relative and not absolute epoch on purpose: the renderer's frame-select
24945
+ * expression sees ffmpeg's `t`, which starts at 0 for the export's source
24946
+ * playlist. Handing it absolute epochs would make every call site responsible
24947
+ * for the same subtraction, and the one that forgot would emit a filter that
24948
+ * selects nothing — silently, as a uniform timelapse.
24949
+ */
24950
+ var ExportDenseRangeSchema = object({
24951
+ fromSec: number().nonnegative(),
24952
+ toSec: number().nonnegative()
24953
+ }).refine((r) => r.toSec > r.fromSec, { message: "dense range must have toSec > fromSec" });
24954
+ /**
24955
+ * Dense-interval overlay for a timelapse: sample at `dense.everyMs` INSIDE the
24956
+ * listed ranges and at the base `everyMs` everywhere else.
24957
+ *
24958
+ * `everyMs` must be strictly smaller than the base cadence — a dense rate that
24959
+ * is not denser renders a uniform timelapse the operator believes is two-rate.
24960
+ */
24961
+ var ExportDenseSchema = object({
24962
+ everyMs: number().int().positive(),
24963
+ ranges: array(ExportDenseRangeSchema).min(1).max(200)
24964
+ });
24889
24965
  /** Timelapse cadence — sample one source frame per `everyMs`, output at `outputFps`. */
24890
24966
  var ExportTimelapseSchema = object({
24891
24967
  everyMs: number().int().positive(),
24892
- outputFps: number().int().min(1).max(60).optional()
24968
+ outputFps: number().int().min(1).max(60).optional(),
24969
+ /** Optional second, FASTER rate over the intervals that matter. */
24970
+ dense: ExportDenseSchema.optional()
24971
+ }).superRefine((v, ctx) => {
24972
+ if (v.dense !== void 0 && v.dense.everyMs >= v.everyMs) ctx.addIssue({
24973
+ code: ZodIssueCode.custom,
24974
+ message: "dense.everyMs must be strictly smaller than the base everyMs",
24975
+ path: ["dense", "everyMs"]
24976
+ });
24893
24977
  });
24894
24978
  /**
24895
24979
  * Render options. `speed` and `timelapse` are mutually exclusive. `includeAudio`
@@ -24947,6 +25031,19 @@ var ExportDownloadSchema = object({
24947
25031
  url: string(),
24948
25032
  endpoints: array(string())
24949
25033
  });
25034
+ /**
25035
+ * A finished export's bytes, inline.
25036
+ *
25037
+ * `bytes` is the DECODED length — the number the caller bounds and logs
25038
+ * against, so nobody has to infer it from the base64 length.
25039
+ */
25040
+ var ExportBytesSchema = object({
25041
+ base64: string(),
25042
+ contentType: string(),
25043
+ /** Suggested filename, extension included. */
25044
+ name: string(),
25045
+ bytes: number().int().nonnegative()
25046
+ });
24950
25047
  method(object({
24951
25048
  deviceId: number(),
24952
25049
  profile: string(),
@@ -24971,6 +25068,9 @@ method(object({
24971
25068
  }), method(object({ exportId: string() }), ExportDownloadSchema, {
24972
25069
  kind: "query",
24973
25070
  auth: "protected"
25071
+ }), method(object({ exportId: string() }), ExportBytesSchema, {
25072
+ kind: "query",
25073
+ auth: "protected"
24974
25074
  });
24975
25075
  /**
24976
25076
  * scene-monitor — device-scoped reference-region state cap. An operator marks
@@ -31822,6 +31922,12 @@ Object.freeze({
31822
31922
  addonId: null,
31823
31923
  access: "view"
31824
31924
  },
31925
+ "recordingExport.readExportBytes": {
31926
+ capName: "recordingExport",
31927
+ capScope: "system",
31928
+ addonId: null,
31929
+ access: "view"
31930
+ },
31825
31931
  "sceneMonitor.captureReference": {
31826
31932
  capName: "scene-monitor",
31827
31933
  capScope: "device",
@@ -33239,10 +33345,28 @@ TimelapseRuleInputSchema.extend({
33239
33345
  */
33240
33346
  ownerUserId: string().optional(),
33241
33347
  /**
33242
- * Epoch-ms of the last successful generation the 1-hour re-generation
33243
- * guard's durable state (predecessor parity). Absent = never generated.
33348
+ * Epoch-ms of the NEWEST successful generation across every camera of this
33349
+ * rule. What a UI shows, and the compatibility floor for
33350
+ * {@link readTimelapseGeneratedAt}. Absent = never generated.
33244
33351
  */
33245
33352
  lastGeneratedAt: number().optional(),
33353
+ /**
33354
+ * PER-CAMERA generation state, keyed by `String(deviceId)` — the
33355
+ * re-generation guard's real durable state.
33356
+ *
33357
+ * One rule covers several cameras and each renders its own video, so a rule
33358
+ * -wide stamp is wrong in the direction that DESTROYS work: camera A
33359
+ * succeeding at 06:05 tells camera B, whose render failed, that it is
33360
+ * already done — and B's night is gone for good, because the window will not
33361
+ * come back.
33362
+ *
33363
+ * ADDITIVE, so the migration is free: a row written before this field simply
33364
+ * has no map, and {@link readTimelapseGeneratedAt} falls back to
33365
+ * {@link TimelapseRuleSchema.shape.lastGeneratedAt}. Reading an old row as
33366
+ * "never generated" would re-render and re-notify every camera of every rule
33367
+ * once, on the deploy that shipped the map.
33368
+ */
33369
+ generatedByDevice: record(string(), number()).optional(),
33246
33370
  /** userId of the caller who created the rule (server-stamped). */
33247
33371
  createdBy: string(),
33248
33372
  createdAt: number(),
@@ -33355,6 +33479,46 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
33355
33479
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
33356
33480
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
33357
33481
  //#endregion
33482
+ //#region src/curses-shim.ts
33483
+ /**
33484
+ * macOS repair for the managed Python's curses.
33485
+ *
33486
+ * The python-build-standalone distribution the dependency resolver installs on
33487
+ * macOS links ncurses statically with the terminfo database compiled OUT
33488
+ * (`--disable-database`) and an ABI mismatch that misparses every numeric
33489
+ * capability of its five built-in fallback terminals (`colors=-1`,
33490
+ * `cols=524368`). `has_colors()` is therefore False under every TERM value and
33491
+ * `init_pair` is hard-blocked, so glances takes its B&W branch and paints the
33492
+ * whole screen with SGR conceal/reverse/alt-charset — the "text missing,
33493
+ * light bars instead" terminal camera frames. No environment variable can fix
33494
+ * it: the code that would read TERMINFO/TERMINFO_DIRS was never compiled in.
33495
+ *
33496
+ * The repair ships prebuilt `_curses` / `_curses_panel` extension modules
33497
+ * linked against the system `/usr/lib/libncurses.5.4.dylib` (universal2) and
33498
+ * puts their directory on PYTHONPATH at PTY spawn — PYTHONPATH precedes
33499
+ * lib-dynload on sys.path, so the working modules shadow the broken ones with
33500
+ * no change to the managed distribution. The filename is ABI-locked to
33501
+ * cpython-312: a managed-python bump to 3.13 makes the shim silently inert
33502
+ * (python ignores a mismatched ABI tag), which is why the addon probes
33503
+ * `tigetnum("colors")` at startup and logs loudly when colours are missing.
33504
+ */
33505
+ /** `<addonRoot>/assets/curses-shim/py312`, valid from both src/ and dist/. */
33506
+ function cursesShimDir() {
33507
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets", "curses-shim", "py312");
33508
+ }
33509
+ function cursesShimDirExists(shimDir) {
33510
+ return existsSync(shimDir);
33511
+ }
33512
+ /**
33513
+ * The env to merge into the glances profile, or undefined when the shim does
33514
+ * not apply (non-macOS, or the assets were not shipped).
33515
+ */
33516
+ function resolveGlancesCursesShimEnv(options) {
33517
+ if (options.platform !== "darwin" || !options.shimDirExists) return void 0;
33518
+ const existing = options.existingPythonPath?.trim();
33519
+ return { PYTHONPATH: existing ? `${options.shimDir}:${existing}` : options.shimDir };
33520
+ }
33521
+ //#endregion
33358
33522
  //#region src/pty.ts
33359
33523
  /**
33360
33524
  * Minimal pty abstraction. The manager depends on this interface, never on
@@ -33451,6 +33615,91 @@ async function warmNodePty() {
33451
33615
  await loadNodePty();
33452
33616
  }
33453
33617
  //#endregion
33618
+ //#region src/silence-analysis.ts
33619
+ /**
33620
+ * Every analyzer a Terminal camera is created with switched OFF.
33621
+ *
33622
+ * `motion-detection` sits first deliberately: it is the one that holds the
33623
+ * decode session open, so it is the one whose absence is the difference between
33624
+ * a terminal costing a screen scrape and costing a decode pipeline.
33625
+ */
33626
+ var TERMINAL_SILENCED_CAP_NAMES = [
33627
+ "motion-detection",
33628
+ DETECTION_PIPELINE_CAP_NAME,
33629
+ AUDIO_ANALYSIS_CAP_NAME
33630
+ ];
33631
+ /**
33632
+ * Resolve the addon currently providing `capName` for this device.
33633
+ *
33634
+ * Inlined rather than reached for through a helper because there is exactly one
33635
+ * honest source: the device's own bindings. `listBindableCapsForDeviceType`
33636
+ * answers for the device TYPE and would happily name a wrapper that is not the
33637
+ * one bound here.
33638
+ *
33639
+ * Returns `null` when the cap is not bound at all — which is not an error: a
33640
+ * deployment with no audio analyzer has nothing to switch off, and demanding one
33641
+ * would make every terminal creation fail on a perfectly valid hub.
33642
+ */
33643
+ async function resolveBoundWrapper(api, deviceId, capName) {
33644
+ const bindings = await api.deviceManager.getBindings.query({ deviceId });
33645
+ for (const entry of bindings.entries) {
33646
+ if (entry.capName !== capName) continue;
33647
+ if (entry.kind !== "wrapped") continue;
33648
+ if (entry.providerAddonId === "") return null;
33649
+ return entry.providerAddonId;
33650
+ }
33651
+ return null;
33652
+ }
33653
+ /**
33654
+ * Switch every analyzer in {@link TERMINAL_SILENCED_CAP_NAMES} off for one
33655
+ * Terminal camera.
33656
+ *
33657
+ * Called ONLY on creation. An operator who deliberately turns detection back on
33658
+ * for a terminal must win, and a reconcile that re-asserted every pass would
33659
+ * silently overrule them once a minute.
33660
+ *
33661
+ * @throws if a cap IS bound and the write to its authority failed. The camera
33662
+ * would then be running a full analyzer for a screen recording, which is the
33663
+ * exact cost this exists to remove — and a silent version of that failure is
33664
+ * unfindable.
33665
+ */
33666
+ async function silenceAnalysisFor(deps, deviceId) {
33667
+ const failures = [];
33668
+ for (const capName of TERMINAL_SILENCED_CAP_NAMES) try {
33669
+ const wrapperAddonId = await resolveBoundWrapper(deps.api, deviceId, capName);
33670
+ if (wrapperAddonId === null) {
33671
+ deps.logger.debug("terminal camera: no analyzer bound for this capability", {
33672
+ tags: { deviceId },
33673
+ meta: { capName }
33674
+ });
33675
+ continue;
33676
+ }
33677
+ await deps.api.deviceManager.setWrapperActive.mutate({
33678
+ deviceId,
33679
+ capName,
33680
+ wrapperAddonId,
33681
+ active: false
33682
+ });
33683
+ deps.logger.info("terminal camera: analyzer switched off at its authority", {
33684
+ tags: { deviceId },
33685
+ meta: {
33686
+ capName,
33687
+ wrapperAddonId
33688
+ }
33689
+ });
33690
+ } catch (err) {
33691
+ failures.push(`${capName}: ${errMsg(err)}`);
33692
+ deps.logger.error("terminal camera: could NOT switch an analyzer off", {
33693
+ tags: { deviceId },
33694
+ meta: {
33695
+ capName,
33696
+ error: errMsg(err)
33697
+ }
33698
+ });
33699
+ }
33700
+ if (failures.length > 0) throw new Error(`terminal camera ${deviceId}: could not switch off ${failures.length} analyzer(s) — it will run at full detection cost (${failures.join("; ")})`);
33701
+ }
33702
+ //#endregion
33454
33703
  //#region src/terminal-camera-declarations.ts
33455
33704
  /**
33456
33705
  * Feed DeclaredDevices every live declaration plus one deterministic orphan
@@ -33485,6 +33734,198 @@ function buildTerminalInstanceCameraDeclarations(instances) {
33485
33734
  function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
33486
33735
  return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
33487
33736
  }
33737
+ //#endregion
33738
+ //#region src/terminal-cell-runs.ts
33739
+ var TERMINAL_DEFAULT_FG = "#d7dce2";
33740
+ var TERMINAL_DEFAULT_BG = "#0b0d10";
33741
+ /**
33742
+ * The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
33743
+ * frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
33744
+ * whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
33745
+ * near-black background at 13px. Index 7 IS the default foreground, so plain
33746
+ * `CSI 37m` text renders identically to unstyled text.
33747
+ */
33748
+ var TERMINAL_ANSI_PALETTE = [
33749
+ "#282c34",
33750
+ "#e06c75",
33751
+ "#98c379",
33752
+ "#e5c07b",
33753
+ "#61afef",
33754
+ "#c678dd",
33755
+ "#56b6c2",
33756
+ TERMINAL_DEFAULT_FG,
33757
+ "#5c6370",
33758
+ "#ef596f",
33759
+ "#89ca78",
33760
+ "#f0c674",
33761
+ "#6cb6ff",
33762
+ "#d55fde",
33763
+ "#2bbac5",
33764
+ "#ffffff"
33765
+ ];
33766
+ /** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
33767
+ var TERMINAL_CUBE_LEVELS = [
33768
+ 0,
33769
+ 95,
33770
+ 135,
33771
+ 175,
33772
+ 215,
33773
+ 255
33774
+ ];
33775
+ var TERMINAL_CUBE_FIRST = 16;
33776
+ var TERMINAL_GRAYSCALE_FIRST = 232;
33777
+ var TERMINAL_GRAYSCALE_BASE = 8;
33778
+ var TERMINAL_GRAYSCALE_STEP = 10;
33779
+ /** SGR 2 keeps the foreground legible; it must not become the background. */
33780
+ var TERMINAL_DIM_WEIGHT = .6;
33781
+ function channel(value) {
33782
+ return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
33783
+ }
33784
+ function hex(red, green, blue) {
33785
+ return `#${channel(red)}${channel(green)}${channel(blue)}`;
33786
+ }
33787
+ function parseHex(color) {
33788
+ return [
33789
+ Number.parseInt(color.slice(1, 3), 16),
33790
+ Number.parseInt(color.slice(3, 5), 16),
33791
+ Number.parseInt(color.slice(5, 7), 16)
33792
+ ];
33793
+ }
33794
+ /** Resolve an xterm palette index (0-255) to a hex colour. */
33795
+ function terminalPaletteColor(index) {
33796
+ const ansi = TERMINAL_ANSI_PALETTE[index];
33797
+ if (ansi !== void 0) return ansi;
33798
+ if (index >= TERMINAL_GRAYSCALE_FIRST) {
33799
+ const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
33800
+ return hex(level, level, level);
33801
+ }
33802
+ if (index >= TERMINAL_CUBE_FIRST) {
33803
+ const offset = index - TERMINAL_CUBE_FIRST;
33804
+ 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);
33805
+ }
33806
+ return TERMINAL_DEFAULT_FG;
33807
+ }
33808
+ /** Resolve a 0xRRGGBB truecolor value to a hex colour. */
33809
+ function terminalRgbColor(value) {
33810
+ return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
33811
+ }
33812
+ function blend(color, toward, weight) {
33813
+ const [red, green, blue] = parseHex(color);
33814
+ const [targetRed, targetGreen, targetBlue] = parseHex(toward);
33815
+ return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
33816
+ }
33817
+ function resolveForeground(cell) {
33818
+ if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
33819
+ if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
33820
+ return TERMINAL_DEFAULT_FG;
33821
+ }
33822
+ function resolveBackground(cell) {
33823
+ if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
33824
+ if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
33825
+ return TERMINAL_DEFAULT_BG;
33826
+ }
33827
+ /**
33828
+ * Resolve one cell's attributes into concrete colours.
33829
+ *
33830
+ * Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
33831
+ * defaults is still a visible swap rather than a no-op — that is how a selected
33832
+ * or highlighted row in Glances reads. Invisible is then conceal-by-equality
33833
+ * (foreground painted in its own background): the cell keeps its columns, which
33834
+ * a dropped cell would not, and dropping it would shift the whole rest of the
33835
+ * row left.
33836
+ */
33837
+ function resolveCellStyle(cell) {
33838
+ const inverse = cell.isInverse() !== 0;
33839
+ const plainFg = resolveForeground(cell);
33840
+ const plainBg = resolveBackground(cell);
33841
+ const background = inverse ? plainFg : plainBg;
33842
+ let foreground = inverse ? plainBg : plainFg;
33843
+ if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
33844
+ if (cell.isInvisible() !== 0) foreground = background;
33845
+ return {
33846
+ fg: foreground === "#d7dce2" ? null : foreground,
33847
+ bg: background === "#0b0d10" ? null : background,
33848
+ bold: cell.isBold() !== 0
33849
+ };
33850
+ }
33851
+ function sameStyle(left, right) {
33852
+ return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
33853
+ }
33854
+ /**
33855
+ * Merge adjacent same-style cells into runs, then drop the trailing run of
33856
+ * default-styled whitespace so a row costs what it draws — the same trim
33857
+ * `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
33858
+ * a green bar of spaces out to the right margin is a pixel Glances drew.
33859
+ */
33860
+ function buildCellRuns(cells) {
33861
+ const runs = [];
33862
+ let text = "";
33863
+ let style = null;
33864
+ for (const cell of cells) {
33865
+ if (style !== null && sameStyle(style, cell.style)) {
33866
+ text += cell.text;
33867
+ continue;
33868
+ }
33869
+ if (style !== null) runs.push({
33870
+ text,
33871
+ ...style
33872
+ });
33873
+ text = cell.text;
33874
+ style = cell.style;
33875
+ }
33876
+ if (style !== null) runs.push({
33877
+ text,
33878
+ ...style
33879
+ });
33880
+ while (runs.length > 0) {
33881
+ const last = runs[runs.length - 1];
33882
+ if (last === void 0 || last.bg !== null) break;
33883
+ const trimmed = last.text.replace(/\s+$/u, "");
33884
+ if (trimmed === last.text) break;
33885
+ if (trimmed === "") {
33886
+ runs.pop();
33887
+ continue;
33888
+ }
33889
+ runs[runs.length - 1] = {
33890
+ ...last,
33891
+ text: trimmed
33892
+ };
33893
+ break;
33894
+ }
33895
+ return runs;
33896
+ }
33897
+ /**
33898
+ * Monospace families to try, in order — NOT one family and a generic.
33899
+ *
33900
+ * A terminal screen is mostly box-drawing and block characters, and a font
33901
+ * without them renders the frame as noise rather than as missing detail.
33902
+ * `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
33903
+ * DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
33904
+ * coverage is not, and its Glances camera came out unreadable while the hub's
33905
+ * was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
33906
+ *
33907
+ * `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
33908
+ * present on every install, and derived from DejaVu Sans Mono — the same glyph
33909
+ * coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
33910
+ * generic stays last so a host with none of them still draws something.
33911
+ */
33912
+ var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
33913
+ var TERMINAL_FONT_SIZE = 13;
33914
+ var TERMINAL_TEXT_MARGIN_X = 8;
33915
+ var TERMINAL_ROW_HEIGHT = 15;
33916
+ var TERMINAL_BASELINE_Y = 18;
33917
+ /**
33918
+ * Distance from a row's baseline up to the top of its cell box. Chosen so
33919
+ * consecutive rows tile exactly: row N's box runs from `baseline - this` for
33920
+ * `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
33921
+ * bar that stopped short would draw as stripes across a `CSI 42m` panel.
33922
+ */
33923
+ var TERMINAL_CELL_ASCENT = 11.5;
33924
+ var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
33925
+ /** Two decimals is under a tenth of a pixel and keeps the SVG small. */
33926
+ function coordinate(value) {
33927
+ return String(Number(value.toFixed(2)));
33928
+ }
33488
33929
  function escapeXml(value) {
33489
33930
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
33490
33931
  }
@@ -33499,11 +33940,47 @@ function escapeXml(value) {
33499
33940
  * looked perfect — which is exactly how the operator reported it. Measured in
33500
33941
  * the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
33501
33942
  * collapsed against 178 px preserved.
33502
- */
33503
- async function renderTerminalJpeg(lines) {
33504
- const renderedLines = lines.slice(0, 40).map((line, index) => `<text xml:space="preserve" x="8" y="${String(18 + index * 15)}">${escapeXml(line.slice(0, 120))}</text>`).join("");
33505
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${String(960)}" height="${String(640)}"><rect width="100%" height="100%" fill="#0b0d10"/><g fill="#d7dce2" font-family="DejaVu Sans Mono,monospace" font-size="13">${renderedLines}</g></svg>`;
33506
- return sharp(Buffer.from(svg)).jpeg({
33943
+ *
33944
+ * Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
33945
+ * never appended to the one before it, so the background rects and the glyphs
33946
+ * are placed off the same grid and cannot drift apart. `textLength` is emitted
33947
+ * with it because it is the correct declaration and renderers that honour it
33948
+ * get an exact grid — but it is not what makes this work: librsvg, which sharp
33949
+ * uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
33950
+ * 600 px still drew its natural 937 px). The anchoring is the guarantee.
33951
+ */
33952
+ function renderTerminalSvg(rows) {
33953
+ const backgrounds = [];
33954
+ const texts = [];
33955
+ rows.slice(0, 40).forEach((row, index) => {
33956
+ const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
33957
+ const top = baseline - TERMINAL_CELL_ASCENT;
33958
+ let column = 0;
33959
+ for (const run of row) {
33960
+ if (column >= 120) break;
33961
+ const clipped = clipRun(run, 120 - column);
33962
+ const columns = [...clipped].length;
33963
+ if (columns === 0) continue;
33964
+ const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
33965
+ const width = columns * TERMINAL_CELL_WIDTH;
33966
+ if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
33967
+ if (clipped.trim() !== "") {
33968
+ const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
33969
+ const weight = run.bold ? " font-weight=\"bold\"" : "";
33970
+ texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
33971
+ }
33972
+ column += columns;
33973
+ }
33974
+ });
33975
+ 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>`;
33976
+ }
33977
+ /** Cut a run to the columns still left in the row, by code point not unit. */
33978
+ function clipRun(run, remaining) {
33979
+ const points = [...run.text];
33980
+ return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
33981
+ }
33982
+ async function renderTerminalJpeg(rows) {
33983
+ return sharp(Buffer.from(renderTerminalSvg(rows))).jpeg({
33507
33984
  quality: 82,
33508
33985
  chromaSubsampling: "4:2:0"
33509
33986
  }).toBuffer();
@@ -38409,6 +38886,40 @@ function createXtermScreen(cols, rows) {
38409
38886
  for (let row = 0; row < term.rows; row += 1) lines.push(buffer.getLine(buffer.viewportY + row)?.translateToString(true) ?? "");
38410
38887
  return lines;
38411
38888
  },
38889
+ /**
38890
+ * The colour-bearing read. `translateToString` — the only cell read this
38891
+ * screen used to offer — keeps characters and discards every attribute,
38892
+ * which is why the terminal camera rendered a fully-coloured Glances in
38893
+ * grey. Walking cells is the only way to get the attributes back.
38894
+ *
38895
+ * A zero-width cell is the tail half of a wide glyph (CJK, some emoji);
38896
+ * its characters already came back with the leading cell, so skipping it
38897
+ * keeps the text right. `getNullCell()` is reused across the whole screen
38898
+ * because `getCell` allocates otherwise — 4800 objects per frame.
38899
+ */
38900
+ styledLines: () => {
38901
+ const buffer = term.buffer.active;
38902
+ const scratch = buffer.getNullCell();
38903
+ const styled = [];
38904
+ for (let row = 0; row < term.rows; row += 1) {
38905
+ const line = buffer.getLine(buffer.viewportY + row);
38906
+ if (!line) {
38907
+ styled.push([]);
38908
+ continue;
38909
+ }
38910
+ const cells = [];
38911
+ for (let column = 0; column < line.length; column += 1) {
38912
+ const cell = line.getCell(column, scratch);
38913
+ if (!cell || cell.getWidth() === 0) continue;
38914
+ cells.push({
38915
+ text: cell.getChars() || " ",
38916
+ style: resolveCellStyle(cell)
38917
+ });
38918
+ }
38919
+ styled.push(buildCellRuns(cells));
38920
+ }
38921
+ return styled;
38922
+ },
38412
38923
  dispose: () => {
38413
38924
  term.dispose();
38414
38925
  }
@@ -38419,6 +38930,9 @@ function createXtermScreen(cols, rows) {
38419
38930
  var MJPEG_BOUNDARY = "camstack-terminal-frame";
38420
38931
  var SESSION_IDLE_MS = 3e4;
38421
38932
  var SNAPSHOT_STARTUP_WAIT_MS = 1500;
38933
+ var SNAPSHOT_SETTLE_QUIET_MS = 300;
38934
+ var SNAPSHOT_SETTLE_BUDGET_MS = 3e3;
38935
+ var SNAPSHOT_SETTLE_MAX_PULLS = 20;
38422
38936
  var CLOSE_RETRY_BASE_MS = 50;
38423
38937
  var CLOSE_RETRY_MAX_MS = 1e3;
38424
38938
  var CLOSE_ATTEMPTS_PER_PASS = 3;
@@ -38543,20 +39057,36 @@ var TerminalCameraRelay = class {
38543
39057
  if (state.openPromise === opening) state.openPromise = null;
38544
39058
  }
38545
39059
  }
39060
+ /** Replay one output batch into the relay screen; true = the session exited. */
39061
+ applyBatch(state, batch) {
39062
+ if (batch.reset) {
39063
+ state.screen.dispose();
39064
+ state.screen = createXtermScreen(120, 40);
39065
+ if (batch.snapshot) state.screen.write(batch.snapshot);
39066
+ }
39067
+ let exited = false;
39068
+ for (const event of batch.events) if (event.kind === "data") state.screen.write(event.data);
39069
+ else {
39070
+ state.sessionId = null;
39071
+ exited = true;
39072
+ }
39073
+ state.cursor = exited ? 0 : batch.cursor;
39074
+ return exited;
39075
+ }
38546
39076
  async nextFrame(state, forceRender = false, waitForInitialOutput = false) {
38547
39077
  if (state.framePromise) {
38548
39078
  await state.framePromise;
38549
39079
  return forceRender ? this.nextFrame(state, true, waitForInitialOutput) : state.jpeg ?? this.nextFrame(state, false, waitForInitialOutput);
38550
39080
  }
38551
39081
  const render = async () => {
38552
- const openingSession = state.sessionId === null;
39082
+ const initialPull = state.sessionId === null && waitForInitialOutput;
38553
39083
  const sessionId = await this.ensureSession(state);
38554
39084
  let batch;
38555
39085
  try {
38556
39086
  batch = await this.api.pullOutput(state.nodeId, {
38557
39087
  sessionId,
38558
39088
  afterSeq: state.cursor,
38559
- ...openingSession && waitForInitialOutput ? {
39089
+ ...initialPull ? {
38560
39090
  waitMs: SNAPSHOT_STARTUP_WAIT_MS,
38561
39091
  waitForOutput: true
38562
39092
  } : {}
@@ -38578,21 +39108,29 @@ var TerminalCameraRelay = class {
38578
39108
  state.cursor = 0;
38579
39109
  throw error;
38580
39110
  }
38581
- if (batch.reset) {
38582
- state.screen.dispose();
38583
- state.screen = createXtermScreen(120, 40);
38584
- if (batch.snapshot) state.screen.write(batch.snapshot);
38585
- }
38586
- let exited = false;
38587
- for (const event of batch.events) if (event.kind === "data") state.screen.write(event.data);
38588
- else {
38589
- state.sessionId = null;
38590
- exited = true;
39111
+ let exited = this.applyBatch(state, batch);
39112
+ if (initialPull && !exited) {
39113
+ const deadline = Date.now() + SNAPSHOT_SETTLE_BUDGET_MS;
39114
+ for (let pulls = 0; pulls < SNAPSHOT_SETTLE_MAX_PULLS && Date.now() < deadline; pulls += 1) {
39115
+ let settle;
39116
+ try {
39117
+ settle = await this.api.pullOutput(state.nodeId, {
39118
+ sessionId,
39119
+ afterSeq: state.cursor,
39120
+ waitMs: SNAPSHOT_SETTLE_QUIET_MS,
39121
+ waitForOutput: true
39122
+ });
39123
+ } catch {
39124
+ break;
39125
+ }
39126
+ const active = settle.reset || settle.events.some((event) => event.kind === "data");
39127
+ exited = this.applyBatch(state, settle);
39128
+ if (exited || !active) break;
39129
+ }
38591
39130
  }
38592
- state.cursor = exited ? 0 : batch.cursor;
38593
39131
  await state.screen.flush();
38594
39132
  if (forceRender || state.jpeg === null || state.renderedCursor !== state.cursor) {
38595
- state.jpeg = await renderTerminalJpeg(state.screen.lines());
39133
+ state.jpeg = await renderTerminalJpeg(state.screen.styledLines());
38596
39134
  state.renderedCursor = state.cursor;
38597
39135
  }
38598
39136
  return state.jpeg;
@@ -39041,7 +39579,8 @@ function buildProfiles(options) {
39041
39579
  "-m",
39042
39580
  "glances",
39043
39581
  ...options.glancesArgs ?? []
39044
- ]
39582
+ ],
39583
+ ...options.glancesEnv && Object.keys(options.glancesEnv).length > 0 ? { env: options.glancesEnv } : {}
39045
39584
  });
39046
39585
  }
39047
39586
  if (options.allowShell) profiles.push({
@@ -39126,6 +39665,7 @@ var TerminalSessionManager = class {
39126
39665
  glancesPath: opts.glancesPath,
39127
39666
  glancesArgs: opts.glancesArgs,
39128
39667
  glancesPythonPath: opts.glancesPythonPath,
39668
+ glancesEnv: opts.glancesEnv,
39129
39669
  allowShell: opts.allowShell,
39130
39670
  shellPath: opts.shellPath,
39131
39671
  customProfiles: opts.customProfiles,
@@ -39423,6 +39963,18 @@ var TerminalSessionManager = class {
39423
39963
  };
39424
39964
  //#endregion
39425
39965
  //#region src/addon.ts
39966
+ /**
39967
+ * Terminal addon — hosts interactive pty sessions running an allowlisted
39968
+ * profile (`monitor` → `btm`), streamed to the Admin UI over the data plane.
39969
+ *
39970
+ * Phase 1 of docs/design/2026-07-26-terminal-session-design.md: a real TTY good
39971
+ * enough to run a full-screen program. Phase 2 (streaming a session as a camera)
39972
+ * is a separate device-provider concern and does not change this addon's cap.
39973
+ *
39974
+ * Its own package (not a builtin) because it carries a native dependency
39975
+ * (`node-pty`) and should update via `camstack deploy` independently of the
39976
+ * framework, exactly like the node-av decoder addon.
39977
+ */
39426
39978
  var DEFAULTS = {
39427
39979
  btmPath: "",
39428
39980
  btmEnabled: true,
@@ -39455,12 +40007,21 @@ var TerminalAddon = class extends BaseAddon {
39455
40007
  instanceMutationQueue = new TerminalInstanceMutationQueue();
39456
40008
  terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
39457
40009
  glancesPythonPath = "";
40010
+ glancesEnv;
39458
40011
  constructor() {
39459
40012
  super({ ...DEFAULTS });
39460
40013
  }
39461
40014
  async onInitialize() {
39462
40015
  await warmNodePty();
39463
40016
  this.glancesPythonPath = await this.ctx.deps.ensurePython() ?? "";
40017
+ const shimDir = cursesShimDir();
40018
+ this.glancesEnv = resolveGlancesCursesShimEnv({
40019
+ platform: process.platform,
40020
+ shimDir,
40021
+ shimDirExists: cursesShimDirExists(shimDir),
40022
+ existingPythonPath: process.env["PYTHONPATH"]
40023
+ });
40024
+ this.probeGlancesCursesColours();
39464
40025
  const manager = new TerminalSessionManager({
39465
40026
  spawn: createNodePtySpawner(),
39466
40027
  screenFactory: createXtermScreen,
@@ -39476,6 +40037,7 @@ var TerminalAddon = class extends BaseAddon {
39476
40037
  glancesPath: this.config.glancesPath,
39477
40038
  glancesArgs: this.config.glancesArgs,
39478
40039
  glancesPythonPath: this.glancesPythonPath,
40040
+ glancesEnv: this.glancesEnv,
39479
40041
  allowShell: this.config.allowShell,
39480
40042
  shellPath: this.config.shellPath,
39481
40043
  maxSessions: this.config.maxSessions,
@@ -39521,6 +40083,39 @@ var TerminalAddon = class extends BaseAddon {
39521
40083
  provider: manager
39522
40084
  }];
39523
40085
  }
40086
+ /**
40087
+ * Fire-and-forget canary: the managed python must see a colour-capable
40088
+ * curses, or glances paints its B&W theme (SGR conceal/reverse) and the
40089
+ * terminal camera frames lose their text. The shim filename is ABI-locked
40090
+ * (cpython-312), so a managed-python bump silently disables it — this probe
40091
+ * is what makes that failure loud instead of a garbled camera.
40092
+ */
40093
+ probeGlancesCursesColours() {
40094
+ if (!this.glancesPythonPath) return;
40095
+ const probeEnv = {
40096
+ ...process.env,
40097
+ ...this.glancesEnv,
40098
+ TERM: "xterm-256color"
40099
+ };
40100
+ execFile(this.glancesPythonPath, ["-c", "import curses\ncurses.setupterm('xterm-256color')\nprint(curses.tigetnum('colors'))"], {
40101
+ env: probeEnv,
40102
+ timeout: 1e4
40103
+ }, (error, stdout) => {
40104
+ const colours = error ? NaN : Number.parseInt(stdout.trim(), 10);
40105
+ if (!Number.isFinite(colours) || colours <= 0) {
40106
+ this.ctx.logger.warn("glances curses reports no colours — terminal camera frames will render the B&W theme", { meta: {
40107
+ colours: Number.isFinite(colours) ? colours : null,
40108
+ shimApplied: this.glancesEnv !== void 0,
40109
+ error: error ? error instanceof Error ? error.message : String(error) : void 0
40110
+ } });
40111
+ return;
40112
+ }
40113
+ this.ctx.logger.debug("glances curses colours available", { meta: {
40114
+ colours,
40115
+ shimApplied: this.glancesEnv !== void 0
40116
+ } });
40117
+ });
40118
+ }
39524
40119
  async onConfigChanged() {
39525
40120
  this.replaceTerminalCameraTombstones(this.config.terminalCameraTombstones);
39526
40121
  this.manager?.reconfigureProfiles({
@@ -39534,6 +40129,7 @@ var TerminalAddon = class extends BaseAddon {
39534
40129
  glancesPath: this.config.glancesPath,
39535
40130
  glancesArgs: this.config.glancesArgs,
39536
40131
  glancesPythonPath: this.glancesPythonPath,
40132
+ glancesEnv: this.glancesEnv,
39537
40133
  allowShell: this.config.allowShell,
39538
40134
  shellPath: this.config.shellPath,
39539
40135
  maxSessions: this.config.maxSessions,
@@ -39637,37 +40233,22 @@ var TerminalAddon = class extends BaseAddon {
39637
40233
  }
39638
40234
  }
39639
40235
  /**
39640
- * A Terminal camera is a rendered screen. Object detection on it finds
39641
- * nothing, forever, at full cost.
39642
- *
39643
- * Measured on the hub 2026-08-11, for ONE terminal camera: 19 frames per 10 s
39644
- * through the detection pipeline at ~61 ms of inference each, plus 115
39645
- * capture-scheduler requests a minute — against `detections=0`. Multiply by
39646
- * one terminal per node and it is a standing tax on a hub that was already
39647
- * shedding 86 % of its capture queue.
40236
+ * A Terminal camera is a rendered screen. Every analyzer on it finds nothing,
40237
+ * forever, at full cost — see `silence-analysis.ts` for the measurements, the
40238
+ * reason `motion-detection` is in the list, and why a failure THROWS instead
40239
+ * of warning.
39648
40240
  *
39649
- * Written through `setCameraSwitch`, which is the authority that already owns
39650
- * this function [D62] forbids a second store that disagrees with it. And
39651
- * written ONLY on creation: an operator who deliberately turns detection back
39652
- * on for a terminal must win, and a reconcile that re-asserted every pass
39653
- * would silently overrule them once a minute.
39654
- */
39655
- async silenceAnalysisFor(deviceId) {
39656
- for (const switchId of ["object-detection", "audio-analysis"]) try {
39657
- await this.ctx.api.pipelineOrchestrator.setCameraSwitch.mutate({
39658
- deviceId,
39659
- switchId,
39660
- enabled: false
39661
- });
39662
- } catch (err) {
39663
- this.ctx.logger.warn("could not switch off analysis for a Terminal camera", {
39664
- tags: { deviceId },
39665
- meta: {
39666
- switchId,
39667
- error: err instanceof Error ? err.message : String(err)
39668
- }
39669
- });
39670
- }
40241
+ * Written to the AUTHORITIES (`deviceManager.setWrapperActive`), not through
40242
+ * the deprecated switch cap the previous version used (D113). Written ONLY on
40243
+ * creation: an operator who deliberately turns detection back on for a
40244
+ * terminal must win, and a reconcile that re-asserted every pass would
40245
+ * silently overrule them once a minute.
40246
+ */
40247
+ silenceAnalysisFor(deviceId) {
40248
+ return silenceAnalysisFor({
40249
+ api: this.ctx.api,
40250
+ logger: this.ctx.logger
40251
+ }, deviceId);
39671
40252
  }
39672
40253
  terminalInstanceControl() {
39673
40254
  return {
@@ -39980,4 +40561,4 @@ var TerminalAddon = class extends BaseAddon {
39980
40561
  }
39981
40562
  };
39982
40563
  //#endregion
39983
- export { TerminalAddon, createXtermScreen as a, createTerminalDataPlaneHandler as i, buildProfiles as n, createNodePtySpawner as o, findProfile as r, warmNodePty as s, TerminalSessionManager as t };
40564
+ export { TerminalAddon, createXtermScreen as a, buildCellRuns as c, terminalRgbColor as d, createNodePtySpawner as f, createTerminalDataPlaneHandler as i, resolveCellStyle as l, buildProfiles as n, TERMINAL_DEFAULT_BG as o, warmNodePty as p, findProfile as r, TERMINAL_DEFAULT_FG as s, TerminalSessionManager as t, terminalPaletteColor as u };