Review on #20379, finding 4 (Medium). Three portability defects in the
visual verification harness:
- `FORCE_COLOR=3 COLORTERM=truecolor tsx ...` POSIX env assignment does not
work under the Windows npm command shell. The script is now a plain Node
launcher (scripts/visual/run.mjs) that sets the env itself and spawns tsx
via require.resolve('tsx/cli') — no cross-env, no shell syntax.
- Hardcoded /tmp/tui-visual.{html,png} resolve to a drive-root path like
C:\tmp on native Windows (and fail when that directory doesn't exist).
Both scripts now derive the output directory from a shared paths.mjs
helper: os.tmpdir()/hermes-tui-visual (created recursively;
HERMES_TUI_VISUAL_DIR overrides for CI or side-by-side runs).
- electron was undeclared by ui-tui and only worked via hoisting luck. The
launcher now resolves it EXPLICITLY from the install tree the desktop
workspace already provides (require('electron') in plain Node returns the
binary path), with an ELECTRON_BIN override and a clear error pointing at
the repo-root install when it's absent — instead of declaring a second
~100MB dependency on a TUI workspace for a dev-only harness.
Also fixes the trailing-whitespace line in render.tsx that `git diff
--check` flags. Verified end-to-end: render writes the HTML scene sheet
and the electron shot step produces the screenshot from the tmpdir path;
the missing-electron error path prints the guidance message.
The desktop color-mix system, ported: a theme is a handful of identity
SEEDS (text, primary, accent, border, status hues); every secondary tone
(muted, label, surfaces, chips, selection) is a color-mix derivative
against the real terminal background. lib/color.ts consolidates the
primitives (parse/mix/luminance/contrast/retone + xterm.js's multiplicative
liftForContrast). Knobs are grid-search fitted so the math reproduces the
classic hand-tuned literals (contract-tested). The display shim renders
authored palettes RAW and only rescues near-invisible colors. Boot reads
the last resolved theme from $HERMES_HOME/tui-theme-boot.json so the first
frame paints in the right palette (no default-dark flash), and the
placeholder cursor follows the bubbles textinput pattern.