@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.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");
@@ -7044,6 +7047,18 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7044
7047
  action: string().min(1),
7045
7048
  input: unknown()
7046
7049
  }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7050
+ //#endregion
7051
+ //#region ../types/dist/err-msg-IQTHeDzc.mjs
7052
+ /**
7053
+ import { errMsg } from '@camstack/types'
7054
+ * Extract a human-readable message from an unknown error value.
7055
+ * Replaces the ubiquitous `errMsg(err)` pattern.
7056
+ */
7057
+ function errMsg(err) {
7058
+ if (err instanceof Error) return err.message;
7059
+ if (typeof err === "string") return err;
7060
+ return String(err);
7061
+ }
7047
7062
  var EncodeProfileSchema = object({
7048
7063
  video: object({
7049
7064
  codec: _enum([
@@ -7281,8 +7296,31 @@ var AdoptionJobSchema = object({
7281
7296
  error: string().nullable()
7282
7297
  });
7283
7298
  /**
7284
- * Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
7285
- * pipeline functions an operator thinks in terms of.
7299
+ * Per-camera FUNCTION SWITCHES.
7300
+ *
7301
+ * ## The aggregate group is being withdrawn — the BADGE is not (D113)
7302
+ *
7303
+ * This file shipped as "the one coherent on/off surface over the pipeline
7304
+ * functions an operator thinks in terms of". The operator's verdict on
7305
+ * 2026-08-12 was that the coherent surface bought complexity and no clarity:
7306
+ * every function already had a settings page of its own, and a second place to
7307
+ * turn it off is a second place to look. Each switch is going back to its own
7308
+ * component's original options — detection to the detection-pipeline wrapper
7309
+ * binding, audio analysis to its own, recording to `RecordingConfig.enabled`
7310
+ * (which was always first-class; the switch was a veneer over
7311
+ * `recording.setDeviceConfig`), notifications to a notification-center
7312
+ * per-device setting, the two camera planes to their own components.
7313
+ *
7314
+ * What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
7315
+ * thing that lets a status surface say DISABLED instead of BROKEN, recomposed
7316
+ * straight from the authorities with no group in the middle. That rule was
7317
+ * never about a control panel.
7318
+ *
7319
+ * Everything else here — {@link CAMERA_SWITCH_CATALOG}, {@link CameraSwitch},
7320
+ * {@link deriveCameraSwitches}, the `pipelineOrchestrator.getCameraSwitches` /
7321
+ * `setCameraSwitch` pair — is a COMPATIBILITY surface for as long as deployed
7322
+ * viewers (v1.0.305) and the admin UI still call it. It is deleted when they
7323
+ * stop; nothing new may be built on it.
7286
7324
  *
7287
7325
  * ## This file adds no state
7288
7326
  *
@@ -7444,6 +7482,14 @@ var CameraSwitchGroupSchema = object({
7444
7482
  fetchedAt: number()
7445
7483
  });
7446
7484
  /**
7485
+ * The wrapper capability each wrapper-backed switch controls. Named constants
7486
+ * because the same strings appear in `legacy-migrations.ts`, in
7487
+ * `isCapActiveForDevice` call sites and in the fake harness — a typo in any of
7488
+ * them is a switch that silently writes a binding nobody reads.
7489
+ */
7490
+ var DETECTION_PIPELINE_CAP_NAME = "detection-pipeline";
7491
+ var AUDIO_ANALYSIS_CAP_NAME = "audio-analysis";
7492
+ /**
7447
7493
  * Ops-log — the durable, append-only operations audit shared by the
7448
7494
  * recordings and events management surfaces.
7449
7495
  *
@@ -17819,9 +17865,16 @@ var CameraStatusSchema = object({
17819
17865
  audio: CameraAudioStatusSchema.nullable(),
17820
17866
  recording: CameraRecordingStatusSchema.nullable(),
17821
17867
  /**
17822
- * Per-camera function switches an OPERATOR has turned off
17868
+ * Per-camera functions an OPERATOR has turned off
17823
17869
  * ([D61](../../../../docs/decisions/adr-0067.md)).
17824
17870
  *
17871
+ * Composed from the AUTHORITIES themselves — the wrapper bindings,
17872
+ * `RecordingConfig.enabled`, the notification mute, the broker's audio
17873
+ * policy, the camera's own microphone — via `composeSwitchedOff`, not from
17874
+ * the deprecated `getCameraSwitches` group ([D113](../../../../docs/decisions/adr-0113.md)).
17875
+ * The badge outlives the control panel: the panel was a convenience, this is
17876
+ * the difference between a camera being off and a camera being dead.
17877
+ *
17825
17878
  * This is the difference between DISABLED and BROKEN. A camera whose
17826
17879
  * `detection` block reports zero fps and whose `switchedOff` contains
17827
17880
  * `'object-detection'` was switched off by a person; the same camera with an
@@ -24908,10 +24961,42 @@ method(object({
24908
24961
  */
24909
24962
  /** Playback-speed multiplier for the render (1 = realtime). */
24910
24963
  var ExportSpeedSchema = number().min(.25).max(32);
24964
+ /**
24965
+ * One dense interval, in SECONDS FROM THE EXPORT'S OWN `fromMs`.
24966
+ *
24967
+ * Relative and not absolute epoch on purpose: the renderer's frame-select
24968
+ * expression sees ffmpeg's `t`, which starts at 0 for the export's source
24969
+ * playlist. Handing it absolute epochs would make every call site responsible
24970
+ * for the same subtraction, and the one that forgot would emit a filter that
24971
+ * selects nothing — silently, as a uniform timelapse.
24972
+ */
24973
+ var ExportDenseRangeSchema = object({
24974
+ fromSec: number().nonnegative(),
24975
+ toSec: number().nonnegative()
24976
+ }).refine((r) => r.toSec > r.fromSec, { message: "dense range must have toSec > fromSec" });
24977
+ /**
24978
+ * Dense-interval overlay for a timelapse: sample at `dense.everyMs` INSIDE the
24979
+ * listed ranges and at the base `everyMs` everywhere else.
24980
+ *
24981
+ * `everyMs` must be strictly smaller than the base cadence — a dense rate that
24982
+ * is not denser renders a uniform timelapse the operator believes is two-rate.
24983
+ */
24984
+ var ExportDenseSchema = object({
24985
+ everyMs: number().int().positive(),
24986
+ ranges: array(ExportDenseRangeSchema).min(1).max(200)
24987
+ });
24911
24988
  /** Timelapse cadence — sample one source frame per `everyMs`, output at `outputFps`. */
24912
24989
  var ExportTimelapseSchema = object({
24913
24990
  everyMs: number().int().positive(),
24914
- outputFps: number().int().min(1).max(60).optional()
24991
+ outputFps: number().int().min(1).max(60).optional(),
24992
+ /** Optional second, FASTER rate over the intervals that matter. */
24993
+ dense: ExportDenseSchema.optional()
24994
+ }).superRefine((v, ctx) => {
24995
+ if (v.dense !== void 0 && v.dense.everyMs >= v.everyMs) ctx.addIssue({
24996
+ code: ZodIssueCode.custom,
24997
+ message: "dense.everyMs must be strictly smaller than the base everyMs",
24998
+ path: ["dense", "everyMs"]
24999
+ });
24915
25000
  });
24916
25001
  /**
24917
25002
  * Render options. `speed` and `timelapse` are mutually exclusive. `includeAudio`
@@ -24969,6 +25054,19 @@ var ExportDownloadSchema = object({
24969
25054
  url: string(),
24970
25055
  endpoints: array(string())
24971
25056
  });
25057
+ /**
25058
+ * A finished export's bytes, inline.
25059
+ *
25060
+ * `bytes` is the DECODED length — the number the caller bounds and logs
25061
+ * against, so nobody has to infer it from the base64 length.
25062
+ */
25063
+ var ExportBytesSchema = object({
25064
+ base64: string(),
25065
+ contentType: string(),
25066
+ /** Suggested filename, extension included. */
25067
+ name: string(),
25068
+ bytes: number().int().nonnegative()
25069
+ });
24972
25070
  method(object({
24973
25071
  deviceId: number(),
24974
25072
  profile: string(),
@@ -24993,6 +25091,9 @@ method(object({
24993
25091
  }), method(object({ exportId: string() }), ExportDownloadSchema, {
24994
25092
  kind: "query",
24995
25093
  auth: "protected"
25094
+ }), method(object({ exportId: string() }), ExportBytesSchema, {
25095
+ kind: "query",
25096
+ auth: "protected"
24996
25097
  });
24997
25098
  /**
24998
25099
  * scene-monitor — device-scoped reference-region state cap. An operator marks
@@ -31844,6 +31945,12 @@ Object.freeze({
31844
31945
  addonId: null,
31845
31946
  access: "view"
31846
31947
  },
31948
+ "recordingExport.readExportBytes": {
31949
+ capName: "recordingExport",
31950
+ capScope: "system",
31951
+ addonId: null,
31952
+ access: "view"
31953
+ },
31847
31954
  "sceneMonitor.captureReference": {
31848
31955
  capName: "scene-monitor",
31849
31956
  capScope: "device",
@@ -33261,10 +33368,28 @@ TimelapseRuleInputSchema.extend({
33261
33368
  */
33262
33369
  ownerUserId: string().optional(),
33263
33370
  /**
33264
- * Epoch-ms of the last successful generation the 1-hour re-generation
33265
- * guard's durable state (predecessor parity). Absent = never generated.
33371
+ * Epoch-ms of the NEWEST successful generation across every camera of this
33372
+ * rule. What a UI shows, and the compatibility floor for
33373
+ * {@link readTimelapseGeneratedAt}. Absent = never generated.
33266
33374
  */
33267
33375
  lastGeneratedAt: number().optional(),
33376
+ /**
33377
+ * PER-CAMERA generation state, keyed by `String(deviceId)` — the
33378
+ * re-generation guard's real durable state.
33379
+ *
33380
+ * One rule covers several cameras and each renders its own video, so a rule
33381
+ * -wide stamp is wrong in the direction that DESTROYS work: camera A
33382
+ * succeeding at 06:05 tells camera B, whose render failed, that it is
33383
+ * already done — and B's night is gone for good, because the window will not
33384
+ * come back.
33385
+ *
33386
+ * ADDITIVE, so the migration is free: a row written before this field simply
33387
+ * has no map, and {@link readTimelapseGeneratedAt} falls back to
33388
+ * {@link TimelapseRuleSchema.shape.lastGeneratedAt}. Reading an old row as
33389
+ * "never generated" would re-render and re-notify every camera of every rule
33390
+ * once, on the deploy that shipped the map.
33391
+ */
33392
+ generatedByDevice: record(string(), number()).optional(),
33268
33393
  /** userId of the caller who created the rule (server-stamped). */
33269
33394
  createdBy: string(),
33270
33395
  createdAt: number(),
@@ -33377,6 +33502,46 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
33377
33502
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
33378
33503
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
33379
33504
  //#endregion
33505
+ //#region src/curses-shim.ts
33506
+ /**
33507
+ * macOS repair for the managed Python's curses.
33508
+ *
33509
+ * The python-build-standalone distribution the dependency resolver installs on
33510
+ * macOS links ncurses statically with the terminfo database compiled OUT
33511
+ * (`--disable-database`) and an ABI mismatch that misparses every numeric
33512
+ * capability of its five built-in fallback terminals (`colors=-1`,
33513
+ * `cols=524368`). `has_colors()` is therefore False under every TERM value and
33514
+ * `init_pair` is hard-blocked, so glances takes its B&W branch and paints the
33515
+ * whole screen with SGR conceal/reverse/alt-charset — the "text missing,
33516
+ * light bars instead" terminal camera frames. No environment variable can fix
33517
+ * it: the code that would read TERMINFO/TERMINFO_DIRS was never compiled in.
33518
+ *
33519
+ * The repair ships prebuilt `_curses` / `_curses_panel` extension modules
33520
+ * linked against the system `/usr/lib/libncurses.5.4.dylib` (universal2) and
33521
+ * puts their directory on PYTHONPATH at PTY spawn — PYTHONPATH precedes
33522
+ * lib-dynload on sys.path, so the working modules shadow the broken ones with
33523
+ * no change to the managed distribution. The filename is ABI-locked to
33524
+ * cpython-312: a managed-python bump to 3.13 makes the shim silently inert
33525
+ * (python ignores a mismatched ABI tag), which is why the addon probes
33526
+ * `tigetnum("colors")` at startup and logs loudly when colours are missing.
33527
+ */
33528
+ /** `<addonRoot>/assets/curses-shim/py312`, valid from both src/ and dist/. */
33529
+ function cursesShimDir() {
33530
+ return node_path.default.join(node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)), "..", "assets", "curses-shim", "py312");
33531
+ }
33532
+ function cursesShimDirExists(shimDir) {
33533
+ return (0, node_fs.existsSync)(shimDir);
33534
+ }
33535
+ /**
33536
+ * The env to merge into the glances profile, or undefined when the shim does
33537
+ * not apply (non-macOS, or the assets were not shipped).
33538
+ */
33539
+ function resolveGlancesCursesShimEnv(options) {
33540
+ if (options.platform !== "darwin" || !options.shimDirExists) return void 0;
33541
+ const existing = options.existingPythonPath?.trim();
33542
+ return { PYTHONPATH: existing ? `${options.shimDir}:${existing}` : options.shimDir };
33543
+ }
33544
+ //#endregion
33380
33545
  //#region src/pty.ts
33381
33546
  /**
33382
33547
  * Minimal pty abstraction. The manager depends on this interface, never on
@@ -33473,6 +33638,91 @@ async function warmNodePty() {
33473
33638
  await loadNodePty();
33474
33639
  }
33475
33640
  //#endregion
33641
+ //#region src/silence-analysis.ts
33642
+ /**
33643
+ * Every analyzer a Terminal camera is created with switched OFF.
33644
+ *
33645
+ * `motion-detection` sits first deliberately: it is the one that holds the
33646
+ * decode session open, so it is the one whose absence is the difference between
33647
+ * a terminal costing a screen scrape and costing a decode pipeline.
33648
+ */
33649
+ var TERMINAL_SILENCED_CAP_NAMES = [
33650
+ "motion-detection",
33651
+ DETECTION_PIPELINE_CAP_NAME,
33652
+ AUDIO_ANALYSIS_CAP_NAME
33653
+ ];
33654
+ /**
33655
+ * Resolve the addon currently providing `capName` for this device.
33656
+ *
33657
+ * Inlined rather than reached for through a helper because there is exactly one
33658
+ * honest source: the device's own bindings. `listBindableCapsForDeviceType`
33659
+ * answers for the device TYPE and would happily name a wrapper that is not the
33660
+ * one bound here.
33661
+ *
33662
+ * Returns `null` when the cap is not bound at all — which is not an error: a
33663
+ * deployment with no audio analyzer has nothing to switch off, and demanding one
33664
+ * would make every terminal creation fail on a perfectly valid hub.
33665
+ */
33666
+ async function resolveBoundWrapper(api, deviceId, capName) {
33667
+ const bindings = await api.deviceManager.getBindings.query({ deviceId });
33668
+ for (const entry of bindings.entries) {
33669
+ if (entry.capName !== capName) continue;
33670
+ if (entry.kind !== "wrapped") continue;
33671
+ if (entry.providerAddonId === "") return null;
33672
+ return entry.providerAddonId;
33673
+ }
33674
+ return null;
33675
+ }
33676
+ /**
33677
+ * Switch every analyzer in {@link TERMINAL_SILENCED_CAP_NAMES} off for one
33678
+ * Terminal camera.
33679
+ *
33680
+ * Called ONLY on creation. An operator who deliberately turns detection back on
33681
+ * for a terminal must win, and a reconcile that re-asserted every pass would
33682
+ * silently overrule them once a minute.
33683
+ *
33684
+ * @throws if a cap IS bound and the write to its authority failed. The camera
33685
+ * would then be running a full analyzer for a screen recording, which is the
33686
+ * exact cost this exists to remove — and a silent version of that failure is
33687
+ * unfindable.
33688
+ */
33689
+ async function silenceAnalysisFor(deps, deviceId) {
33690
+ const failures = [];
33691
+ for (const capName of TERMINAL_SILENCED_CAP_NAMES) try {
33692
+ const wrapperAddonId = await resolveBoundWrapper(deps.api, deviceId, capName);
33693
+ if (wrapperAddonId === null) {
33694
+ deps.logger.debug("terminal camera: no analyzer bound for this capability", {
33695
+ tags: { deviceId },
33696
+ meta: { capName }
33697
+ });
33698
+ continue;
33699
+ }
33700
+ await deps.api.deviceManager.setWrapperActive.mutate({
33701
+ deviceId,
33702
+ capName,
33703
+ wrapperAddonId,
33704
+ active: false
33705
+ });
33706
+ deps.logger.info("terminal camera: analyzer switched off at its authority", {
33707
+ tags: { deviceId },
33708
+ meta: {
33709
+ capName,
33710
+ wrapperAddonId
33711
+ }
33712
+ });
33713
+ } catch (err) {
33714
+ failures.push(`${capName}: ${errMsg(err)}`);
33715
+ deps.logger.error("terminal camera: could NOT switch an analyzer off", {
33716
+ tags: { deviceId },
33717
+ meta: {
33718
+ capName,
33719
+ error: errMsg(err)
33720
+ }
33721
+ });
33722
+ }
33723
+ 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("; ")})`);
33724
+ }
33725
+ //#endregion
33476
33726
  //#region src/terminal-camera-declarations.ts
33477
33727
  /**
33478
33728
  * Feed DeclaredDevices every live declaration plus one deterministic orphan
@@ -33507,6 +33757,198 @@ function buildTerminalInstanceCameraDeclarations(instances) {
33507
33757
  function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
33508
33758
  return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
33509
33759
  }
33760
+ //#endregion
33761
+ //#region src/terminal-cell-runs.ts
33762
+ var TERMINAL_DEFAULT_FG = "#d7dce2";
33763
+ var TERMINAL_DEFAULT_BG = "#0b0d10";
33764
+ /**
33765
+ * The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
33766
+ * frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
33767
+ * whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
33768
+ * near-black background at 13px. Index 7 IS the default foreground, so plain
33769
+ * `CSI 37m` text renders identically to unstyled text.
33770
+ */
33771
+ var TERMINAL_ANSI_PALETTE = [
33772
+ "#282c34",
33773
+ "#e06c75",
33774
+ "#98c379",
33775
+ "#e5c07b",
33776
+ "#61afef",
33777
+ "#c678dd",
33778
+ "#56b6c2",
33779
+ TERMINAL_DEFAULT_FG,
33780
+ "#5c6370",
33781
+ "#ef596f",
33782
+ "#89ca78",
33783
+ "#f0c674",
33784
+ "#6cb6ff",
33785
+ "#d55fde",
33786
+ "#2bbac5",
33787
+ "#ffffff"
33788
+ ];
33789
+ /** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
33790
+ var TERMINAL_CUBE_LEVELS = [
33791
+ 0,
33792
+ 95,
33793
+ 135,
33794
+ 175,
33795
+ 215,
33796
+ 255
33797
+ ];
33798
+ var TERMINAL_CUBE_FIRST = 16;
33799
+ var TERMINAL_GRAYSCALE_FIRST = 232;
33800
+ var TERMINAL_GRAYSCALE_BASE = 8;
33801
+ var TERMINAL_GRAYSCALE_STEP = 10;
33802
+ /** SGR 2 keeps the foreground legible; it must not become the background. */
33803
+ var TERMINAL_DIM_WEIGHT = .6;
33804
+ function channel(value) {
33805
+ return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
33806
+ }
33807
+ function hex(red, green, blue) {
33808
+ return `#${channel(red)}${channel(green)}${channel(blue)}`;
33809
+ }
33810
+ function parseHex(color) {
33811
+ return [
33812
+ Number.parseInt(color.slice(1, 3), 16),
33813
+ Number.parseInt(color.slice(3, 5), 16),
33814
+ Number.parseInt(color.slice(5, 7), 16)
33815
+ ];
33816
+ }
33817
+ /** Resolve an xterm palette index (0-255) to a hex colour. */
33818
+ function terminalPaletteColor(index) {
33819
+ const ansi = TERMINAL_ANSI_PALETTE[index];
33820
+ if (ansi !== void 0) return ansi;
33821
+ if (index >= TERMINAL_GRAYSCALE_FIRST) {
33822
+ const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
33823
+ return hex(level, level, level);
33824
+ }
33825
+ if (index >= TERMINAL_CUBE_FIRST) {
33826
+ const offset = index - TERMINAL_CUBE_FIRST;
33827
+ 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);
33828
+ }
33829
+ return TERMINAL_DEFAULT_FG;
33830
+ }
33831
+ /** Resolve a 0xRRGGBB truecolor value to a hex colour. */
33832
+ function terminalRgbColor(value) {
33833
+ return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
33834
+ }
33835
+ function blend(color, toward, weight) {
33836
+ const [red, green, blue] = parseHex(color);
33837
+ const [targetRed, targetGreen, targetBlue] = parseHex(toward);
33838
+ return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
33839
+ }
33840
+ function resolveForeground(cell) {
33841
+ if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
33842
+ if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
33843
+ return TERMINAL_DEFAULT_FG;
33844
+ }
33845
+ function resolveBackground(cell) {
33846
+ if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
33847
+ if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
33848
+ return TERMINAL_DEFAULT_BG;
33849
+ }
33850
+ /**
33851
+ * Resolve one cell's attributes into concrete colours.
33852
+ *
33853
+ * Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
33854
+ * defaults is still a visible swap rather than a no-op — that is how a selected
33855
+ * or highlighted row in Glances reads. Invisible is then conceal-by-equality
33856
+ * (foreground painted in its own background): the cell keeps its columns, which
33857
+ * a dropped cell would not, and dropping it would shift the whole rest of the
33858
+ * row left.
33859
+ */
33860
+ function resolveCellStyle(cell) {
33861
+ const inverse = cell.isInverse() !== 0;
33862
+ const plainFg = resolveForeground(cell);
33863
+ const plainBg = resolveBackground(cell);
33864
+ const background = inverse ? plainFg : plainBg;
33865
+ let foreground = inverse ? plainBg : plainFg;
33866
+ if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
33867
+ if (cell.isInvisible() !== 0) foreground = background;
33868
+ return {
33869
+ fg: foreground === "#d7dce2" ? null : foreground,
33870
+ bg: background === "#0b0d10" ? null : background,
33871
+ bold: cell.isBold() !== 0
33872
+ };
33873
+ }
33874
+ function sameStyle(left, right) {
33875
+ return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
33876
+ }
33877
+ /**
33878
+ * Merge adjacent same-style cells into runs, then drop the trailing run of
33879
+ * default-styled whitespace so a row costs what it draws — the same trim
33880
+ * `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
33881
+ * a green bar of spaces out to the right margin is a pixel Glances drew.
33882
+ */
33883
+ function buildCellRuns(cells) {
33884
+ const runs = [];
33885
+ let text = "";
33886
+ let style = null;
33887
+ for (const cell of cells) {
33888
+ if (style !== null && sameStyle(style, cell.style)) {
33889
+ text += cell.text;
33890
+ continue;
33891
+ }
33892
+ if (style !== null) runs.push({
33893
+ text,
33894
+ ...style
33895
+ });
33896
+ text = cell.text;
33897
+ style = cell.style;
33898
+ }
33899
+ if (style !== null) runs.push({
33900
+ text,
33901
+ ...style
33902
+ });
33903
+ while (runs.length > 0) {
33904
+ const last = runs[runs.length - 1];
33905
+ if (last === void 0 || last.bg !== null) break;
33906
+ const trimmed = last.text.replace(/\s+$/u, "");
33907
+ if (trimmed === last.text) break;
33908
+ if (trimmed === "") {
33909
+ runs.pop();
33910
+ continue;
33911
+ }
33912
+ runs[runs.length - 1] = {
33913
+ ...last,
33914
+ text: trimmed
33915
+ };
33916
+ break;
33917
+ }
33918
+ return runs;
33919
+ }
33920
+ /**
33921
+ * Monospace families to try, in order — NOT one family and a generic.
33922
+ *
33923
+ * A terminal screen is mostly box-drawing and block characters, and a font
33924
+ * without them renders the frame as noise rather than as missing detail.
33925
+ * `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
33926
+ * DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
33927
+ * coverage is not, and its Glances camera came out unreadable while the hub's
33928
+ * was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
33929
+ *
33930
+ * `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
33931
+ * present on every install, and derived from DejaVu Sans Mono — the same glyph
33932
+ * coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
33933
+ * generic stays last so a host with none of them still draws something.
33934
+ */
33935
+ var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
33936
+ var TERMINAL_FONT_SIZE = 13;
33937
+ var TERMINAL_TEXT_MARGIN_X = 8;
33938
+ var TERMINAL_ROW_HEIGHT = 15;
33939
+ var TERMINAL_BASELINE_Y = 18;
33940
+ /**
33941
+ * Distance from a row's baseline up to the top of its cell box. Chosen so
33942
+ * consecutive rows tile exactly: row N's box runs from `baseline - this` for
33943
+ * `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
33944
+ * bar that stopped short would draw as stripes across a `CSI 42m` panel.
33945
+ */
33946
+ var TERMINAL_CELL_ASCENT = 11.5;
33947
+ var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
33948
+ /** Two decimals is under a tenth of a pixel and keeps the SVG small. */
33949
+ function coordinate(value) {
33950
+ return String(Number(value.toFixed(2)));
33951
+ }
33510
33952
  function escapeXml(value) {
33511
33953
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
33512
33954
  }
@@ -33521,11 +33963,47 @@ function escapeXml(value) {
33521
33963
  * looked perfect — which is exactly how the operator reported it. Measured in
33522
33964
  * the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
33523
33965
  * collapsed against 178 px preserved.
33524
- */
33525
- async function renderTerminalJpeg(lines) {
33526
- 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("");
33527
- 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>`;
33528
- return (0, sharp.default)(Buffer.from(svg)).jpeg({
33966
+ *
33967
+ * Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
33968
+ * never appended to the one before it, so the background rects and the glyphs
33969
+ * are placed off the same grid and cannot drift apart. `textLength` is emitted
33970
+ * with it because it is the correct declaration and renderers that honour it
33971
+ * get an exact grid — but it is not what makes this work: librsvg, which sharp
33972
+ * uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
33973
+ * 600 px still drew its natural 937 px). The anchoring is the guarantee.
33974
+ */
33975
+ function renderTerminalSvg(rows) {
33976
+ const backgrounds = [];
33977
+ const texts = [];
33978
+ rows.slice(0, 40).forEach((row, index) => {
33979
+ const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
33980
+ const top = baseline - TERMINAL_CELL_ASCENT;
33981
+ let column = 0;
33982
+ for (const run of row) {
33983
+ if (column >= 120) break;
33984
+ const clipped = clipRun(run, 120 - column);
33985
+ const columns = [...clipped].length;
33986
+ if (columns === 0) continue;
33987
+ const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
33988
+ const width = columns * TERMINAL_CELL_WIDTH;
33989
+ if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
33990
+ if (clipped.trim() !== "") {
33991
+ const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
33992
+ const weight = run.bold ? " font-weight=\"bold\"" : "";
33993
+ texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
33994
+ }
33995
+ column += columns;
33996
+ }
33997
+ });
33998
+ 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>`;
33999
+ }
34000
+ /** Cut a run to the columns still left in the row, by code point not unit. */
34001
+ function clipRun(run, remaining) {
34002
+ const points = [...run.text];
34003
+ return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
34004
+ }
34005
+ async function renderTerminalJpeg(rows) {
34006
+ return (0, sharp.default)(Buffer.from(renderTerminalSvg(rows))).jpeg({
33529
34007
  quality: 82,
33530
34008
  chromaSubsampling: "4:2:0"
33531
34009
  }).toBuffer();
@@ -38431,6 +38909,40 @@ function createXtermScreen(cols, rows) {
38431
38909
  for (let row = 0; row < term.rows; row += 1) lines.push(buffer.getLine(buffer.viewportY + row)?.translateToString(true) ?? "");
38432
38910
  return lines;
38433
38911
  },
38912
+ /**
38913
+ * The colour-bearing read. `translateToString` — the only cell read this
38914
+ * screen used to offer — keeps characters and discards every attribute,
38915
+ * which is why the terminal camera rendered a fully-coloured Glances in
38916
+ * grey. Walking cells is the only way to get the attributes back.
38917
+ *
38918
+ * A zero-width cell is the tail half of a wide glyph (CJK, some emoji);
38919
+ * its characters already came back with the leading cell, so skipping it
38920
+ * keeps the text right. `getNullCell()` is reused across the whole screen
38921
+ * because `getCell` allocates otherwise — 4800 objects per frame.
38922
+ */
38923
+ styledLines: () => {
38924
+ const buffer = term.buffer.active;
38925
+ const scratch = buffer.getNullCell();
38926
+ const styled = [];
38927
+ for (let row = 0; row < term.rows; row += 1) {
38928
+ const line = buffer.getLine(buffer.viewportY + row);
38929
+ if (!line) {
38930
+ styled.push([]);
38931
+ continue;
38932
+ }
38933
+ const cells = [];
38934
+ for (let column = 0; column < line.length; column += 1) {
38935
+ const cell = line.getCell(column, scratch);
38936
+ if (!cell || cell.getWidth() === 0) continue;
38937
+ cells.push({
38938
+ text: cell.getChars() || " ",
38939
+ style: resolveCellStyle(cell)
38940
+ });
38941
+ }
38942
+ styled.push(buildCellRuns(cells));
38943
+ }
38944
+ return styled;
38945
+ },
38434
38946
  dispose: () => {
38435
38947
  term.dispose();
38436
38948
  }
@@ -38441,6 +38953,9 @@ function createXtermScreen(cols, rows) {
38441
38953
  var MJPEG_BOUNDARY = "camstack-terminal-frame";
38442
38954
  var SESSION_IDLE_MS = 3e4;
38443
38955
  var SNAPSHOT_STARTUP_WAIT_MS = 1500;
38956
+ var SNAPSHOT_SETTLE_QUIET_MS = 300;
38957
+ var SNAPSHOT_SETTLE_BUDGET_MS = 3e3;
38958
+ var SNAPSHOT_SETTLE_MAX_PULLS = 20;
38444
38959
  var CLOSE_RETRY_BASE_MS = 50;
38445
38960
  var CLOSE_RETRY_MAX_MS = 1e3;
38446
38961
  var CLOSE_ATTEMPTS_PER_PASS = 3;
@@ -38565,20 +39080,36 @@ var TerminalCameraRelay = class {
38565
39080
  if (state.openPromise === opening) state.openPromise = null;
38566
39081
  }
38567
39082
  }
39083
+ /** Replay one output batch into the relay screen; true = the session exited. */
39084
+ applyBatch(state, batch) {
39085
+ if (batch.reset) {
39086
+ state.screen.dispose();
39087
+ state.screen = createXtermScreen(120, 40);
39088
+ if (batch.snapshot) state.screen.write(batch.snapshot);
39089
+ }
39090
+ let exited = false;
39091
+ for (const event of batch.events) if (event.kind === "data") state.screen.write(event.data);
39092
+ else {
39093
+ state.sessionId = null;
39094
+ exited = true;
39095
+ }
39096
+ state.cursor = exited ? 0 : batch.cursor;
39097
+ return exited;
39098
+ }
38568
39099
  async nextFrame(state, forceRender = false, waitForInitialOutput = false) {
38569
39100
  if (state.framePromise) {
38570
39101
  await state.framePromise;
38571
39102
  return forceRender ? this.nextFrame(state, true, waitForInitialOutput) : state.jpeg ?? this.nextFrame(state, false, waitForInitialOutput);
38572
39103
  }
38573
39104
  const render = async () => {
38574
- const openingSession = state.sessionId === null;
39105
+ const initialPull = state.sessionId === null && waitForInitialOutput;
38575
39106
  const sessionId = await this.ensureSession(state);
38576
39107
  let batch;
38577
39108
  try {
38578
39109
  batch = await this.api.pullOutput(state.nodeId, {
38579
39110
  sessionId,
38580
39111
  afterSeq: state.cursor,
38581
- ...openingSession && waitForInitialOutput ? {
39112
+ ...initialPull ? {
38582
39113
  waitMs: SNAPSHOT_STARTUP_WAIT_MS,
38583
39114
  waitForOutput: true
38584
39115
  } : {}
@@ -38600,21 +39131,29 @@ var TerminalCameraRelay = class {
38600
39131
  state.cursor = 0;
38601
39132
  throw error;
38602
39133
  }
38603
- if (batch.reset) {
38604
- state.screen.dispose();
38605
- state.screen = createXtermScreen(120, 40);
38606
- if (batch.snapshot) state.screen.write(batch.snapshot);
38607
- }
38608
- let exited = false;
38609
- for (const event of batch.events) if (event.kind === "data") state.screen.write(event.data);
38610
- else {
38611
- state.sessionId = null;
38612
- exited = true;
39134
+ let exited = this.applyBatch(state, batch);
39135
+ if (initialPull && !exited) {
39136
+ const deadline = Date.now() + SNAPSHOT_SETTLE_BUDGET_MS;
39137
+ for (let pulls = 0; pulls < SNAPSHOT_SETTLE_MAX_PULLS && Date.now() < deadline; pulls += 1) {
39138
+ let settle;
39139
+ try {
39140
+ settle = await this.api.pullOutput(state.nodeId, {
39141
+ sessionId,
39142
+ afterSeq: state.cursor,
39143
+ waitMs: SNAPSHOT_SETTLE_QUIET_MS,
39144
+ waitForOutput: true
39145
+ });
39146
+ } catch {
39147
+ break;
39148
+ }
39149
+ const active = settle.reset || settle.events.some((event) => event.kind === "data");
39150
+ exited = this.applyBatch(state, settle);
39151
+ if (exited || !active) break;
39152
+ }
38613
39153
  }
38614
- state.cursor = exited ? 0 : batch.cursor;
38615
39154
  await state.screen.flush();
38616
39155
  if (forceRender || state.jpeg === null || state.renderedCursor !== state.cursor) {
38617
- state.jpeg = await renderTerminalJpeg(state.screen.lines());
39156
+ state.jpeg = await renderTerminalJpeg(state.screen.styledLines());
38618
39157
  state.renderedCursor = state.cursor;
38619
39158
  }
38620
39159
  return state.jpeg;
@@ -39063,7 +39602,8 @@ function buildProfiles(options) {
39063
39602
  "-m",
39064
39603
  "glances",
39065
39604
  ...options.glancesArgs ?? []
39066
- ]
39605
+ ],
39606
+ ...options.glancesEnv && Object.keys(options.glancesEnv).length > 0 ? { env: options.glancesEnv } : {}
39067
39607
  });
39068
39608
  }
39069
39609
  if (options.allowShell) profiles.push({
@@ -39148,6 +39688,7 @@ var TerminalSessionManager = class {
39148
39688
  glancesPath: opts.glancesPath,
39149
39689
  glancesArgs: opts.glancesArgs,
39150
39690
  glancesPythonPath: opts.glancesPythonPath,
39691
+ glancesEnv: opts.glancesEnv,
39151
39692
  allowShell: opts.allowShell,
39152
39693
  shellPath: opts.shellPath,
39153
39694
  customProfiles: opts.customProfiles,
@@ -39445,6 +39986,18 @@ var TerminalSessionManager = class {
39445
39986
  };
39446
39987
  //#endregion
39447
39988
  //#region src/addon.ts
39989
+ /**
39990
+ * Terminal addon — hosts interactive pty sessions running an allowlisted
39991
+ * profile (`monitor` → `btm`), streamed to the Admin UI over the data plane.
39992
+ *
39993
+ * Phase 1 of docs/design/2026-07-26-terminal-session-design.md: a real TTY good
39994
+ * enough to run a full-screen program. Phase 2 (streaming a session as a camera)
39995
+ * is a separate device-provider concern and does not change this addon's cap.
39996
+ *
39997
+ * Its own package (not a builtin) because it carries a native dependency
39998
+ * (`node-pty`) and should update via `camstack deploy` independently of the
39999
+ * framework, exactly like the node-av decoder addon.
40000
+ */
39448
40001
  var DEFAULTS = {
39449
40002
  btmPath: "",
39450
40003
  btmEnabled: true,
@@ -39477,12 +40030,21 @@ var TerminalAddon = class extends BaseAddon {
39477
40030
  instanceMutationQueue = new TerminalInstanceMutationQueue();
39478
40031
  terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
39479
40032
  glancesPythonPath = "";
40033
+ glancesEnv;
39480
40034
  constructor() {
39481
40035
  super({ ...DEFAULTS });
39482
40036
  }
39483
40037
  async onInitialize() {
39484
40038
  await warmNodePty();
39485
40039
  this.glancesPythonPath = await this.ctx.deps.ensurePython() ?? "";
40040
+ const shimDir = cursesShimDir();
40041
+ this.glancesEnv = resolveGlancesCursesShimEnv({
40042
+ platform: process.platform,
40043
+ shimDir,
40044
+ shimDirExists: cursesShimDirExists(shimDir),
40045
+ existingPythonPath: process.env["PYTHONPATH"]
40046
+ });
40047
+ this.probeGlancesCursesColours();
39486
40048
  const manager = new TerminalSessionManager({
39487
40049
  spawn: createNodePtySpawner(),
39488
40050
  screenFactory: createXtermScreen,
@@ -39498,6 +40060,7 @@ var TerminalAddon = class extends BaseAddon {
39498
40060
  glancesPath: this.config.glancesPath,
39499
40061
  glancesArgs: this.config.glancesArgs,
39500
40062
  glancesPythonPath: this.glancesPythonPath,
40063
+ glancesEnv: this.glancesEnv,
39501
40064
  allowShell: this.config.allowShell,
39502
40065
  shellPath: this.config.shellPath,
39503
40066
  maxSessions: this.config.maxSessions,
@@ -39543,6 +40106,39 @@ var TerminalAddon = class extends BaseAddon {
39543
40106
  provider: manager
39544
40107
  }];
39545
40108
  }
40109
+ /**
40110
+ * Fire-and-forget canary: the managed python must see a colour-capable
40111
+ * curses, or glances paints its B&W theme (SGR conceal/reverse) and the
40112
+ * terminal camera frames lose their text. The shim filename is ABI-locked
40113
+ * (cpython-312), so a managed-python bump silently disables it — this probe
40114
+ * is what makes that failure loud instead of a garbled camera.
40115
+ */
40116
+ probeGlancesCursesColours() {
40117
+ if (!this.glancesPythonPath) return;
40118
+ const probeEnv = {
40119
+ ...process.env,
40120
+ ...this.glancesEnv,
40121
+ TERM: "xterm-256color"
40122
+ };
40123
+ (0, node_child_process.execFile)(this.glancesPythonPath, ["-c", "import curses\ncurses.setupterm('xterm-256color')\nprint(curses.tigetnum('colors'))"], {
40124
+ env: probeEnv,
40125
+ timeout: 1e4
40126
+ }, (error, stdout) => {
40127
+ const colours = error ? NaN : Number.parseInt(stdout.trim(), 10);
40128
+ if (!Number.isFinite(colours) || colours <= 0) {
40129
+ this.ctx.logger.warn("glances curses reports no colours — terminal camera frames will render the B&W theme", { meta: {
40130
+ colours: Number.isFinite(colours) ? colours : null,
40131
+ shimApplied: this.glancesEnv !== void 0,
40132
+ error: error ? error instanceof Error ? error.message : String(error) : void 0
40133
+ } });
40134
+ return;
40135
+ }
40136
+ this.ctx.logger.debug("glances curses colours available", { meta: {
40137
+ colours,
40138
+ shimApplied: this.glancesEnv !== void 0
40139
+ } });
40140
+ });
40141
+ }
39546
40142
  async onConfigChanged() {
39547
40143
  this.replaceTerminalCameraTombstones(this.config.terminalCameraTombstones);
39548
40144
  this.manager?.reconfigureProfiles({
@@ -39556,6 +40152,7 @@ var TerminalAddon = class extends BaseAddon {
39556
40152
  glancesPath: this.config.glancesPath,
39557
40153
  glancesArgs: this.config.glancesArgs,
39558
40154
  glancesPythonPath: this.glancesPythonPath,
40155
+ glancesEnv: this.glancesEnv,
39559
40156
  allowShell: this.config.allowShell,
39560
40157
  shellPath: this.config.shellPath,
39561
40158
  maxSessions: this.config.maxSessions,
@@ -39659,37 +40256,22 @@ var TerminalAddon = class extends BaseAddon {
39659
40256
  }
39660
40257
  }
39661
40258
  /**
39662
- * A Terminal camera is a rendered screen. Object detection on it finds
39663
- * nothing, forever, at full cost.
40259
+ * A Terminal camera is a rendered screen. Every analyzer on it finds nothing,
40260
+ * forever, at full cost — see `silence-analysis.ts` for the measurements, the
40261
+ * reason `motion-detection` is in the list, and why a failure THROWS instead
40262
+ * of warning.
39664
40263
  *
39665
- * Measured on the hub 2026-08-11, for ONE terminal camera: 19 frames per 10 s
39666
- * through the detection pipeline at ~61 ms of inference each, plus 115
39667
- * capture-scheduler requests a minute against `detections=0`. Multiply by
39668
- * one terminal per node and it is a standing tax on a hub that was already
39669
- * shedding 86 % of its capture queue.
39670
- *
39671
- * Written through `setCameraSwitch`, which is the authority that already owns
39672
- * this function — [D62] forbids a second store that disagrees with it. And
39673
- * written ONLY on creation: an operator who deliberately turns detection back
39674
- * on for a terminal must win, and a reconcile that re-asserted every pass
39675
- * would silently overrule them once a minute.
39676
- */
39677
- async silenceAnalysisFor(deviceId) {
39678
- for (const switchId of ["object-detection", "audio-analysis"]) try {
39679
- await this.ctx.api.pipelineOrchestrator.setCameraSwitch.mutate({
39680
- deviceId,
39681
- switchId,
39682
- enabled: false
39683
- });
39684
- } catch (err) {
39685
- this.ctx.logger.warn("could not switch off analysis for a Terminal camera", {
39686
- tags: { deviceId },
39687
- meta: {
39688
- switchId,
39689
- error: err instanceof Error ? err.message : String(err)
39690
- }
39691
- });
39692
- }
40264
+ * Written to the AUTHORITIES (`deviceManager.setWrapperActive`), not through
40265
+ * the deprecated switch cap the previous version used (D113). Written ONLY on
40266
+ * creation: an operator who deliberately turns detection back on for a
40267
+ * terminal must win, and a reconcile that re-asserted every pass would
40268
+ * silently overrule them once a minute.
40269
+ */
40270
+ silenceAnalysisFor(deviceId) {
40271
+ return silenceAnalysisFor({
40272
+ api: this.ctx.api,
40273
+ logger: this.ctx.logger
40274
+ }, deviceId);
39693
40275
  }
39694
40276
  terminalInstanceControl() {
39695
40277
  return {
@@ -40002,11 +40584,17 @@ var TerminalAddon = class extends BaseAddon {
40002
40584
  }
40003
40585
  };
40004
40586
  //#endregion
40587
+ exports.TERMINAL_DEFAULT_BG = TERMINAL_DEFAULT_BG;
40588
+ exports.TERMINAL_DEFAULT_FG = TERMINAL_DEFAULT_FG;
40005
40589
  exports.TerminalAddon = TerminalAddon;
40006
40590
  exports.TerminalSessionManager = TerminalSessionManager;
40591
+ exports.buildCellRuns = buildCellRuns;
40007
40592
  exports.buildProfiles = buildProfiles;
40008
40593
  exports.createNodePtySpawner = createNodePtySpawner;
40009
40594
  exports.createTerminalDataPlaneHandler = createTerminalDataPlaneHandler;
40010
40595
  exports.createXtermScreen = createXtermScreen;
40011
40596
  exports.findProfile = findProfile;
40597
+ exports.resolveCellStyle = resolveCellStyle;
40598
+ exports.terminalPaletteColor = terminalPaletteColor;
40599
+ exports.terminalRgbColor = terminalRgbColor;
40012
40600
  exports.warmNodePty = warmNodePty;