# Developers' guide ## Module header - Purpose: Summarize local development practices and validation commands for the Velocetty repository. - Invariants: Keep Makefile targets and validation steps aligned with Continuous Integration (CI) and tooling changes. - Cross-links: [Testing with Bun](testing-with-bun.md), [ADR 001](adr-001-replace-ava-with-bun-test.md), [ADR 002](adr-002-replace-webpack-babel-with-esbuild.md), and [Roadmap](roadmap.md). This guide captures the development practices specific to the Velocetty repository. It is intentionally concise and focused on the steps developers must follow locally. ## Tooling expectations - Use Bun for JavaScript and TypeScript scripts. - Prefer Makefile targets for validation commands. - Use `tsgo` (`@typescript/native-preview`) for TypeScript compilation and type-checking tasks. - Use `build/esbuild/build.ts` as the canonical JavaScript bundling entrypoint for renderer, CLI, and app copy artefacts. - Keep documentation wrapped to 80 columns and code blocks to 120 columns. ## Spelling gate Run `make spelling` to enforce en-GB-oxendict spelling in tracked Markdown and the shared phrase policy across eligible tracked text. The gate uses Typos 1.48.0 and the repository's generated `typos.toml`. The tracked configuration is built from the shared estate dictionary and the narrow `typos.local.toml` overlay. Run `make spelling-config-write` after an intentional policy change, and run `make spelling-config` to verify that the tracked output is current. The pinned builder refreshes the untracked local cache only when the authoritative dictionary is newer, so an already populated cache remains usable offline. Do not edit `typos.toml` directly. Preserve public APIs, serialized schema keys, CSS syntax, upstream action inputs, dependency names and formal product terms through narrow local policy. Keep quoted identifiers in inline code or fenced blocks. The exact phrase gate rejects the hyphenated variant in favour of `handwritten`, including in hidden tracked source. `make nixie` validates the repository's Mermaid diagrams. Continuous Integration installs Nixie 1.1.0 and its Merman 0.7.0 dependency on the Linux build leg without changing the product's cross-platform toolchain matrix. ## CSS modules conventions - Import local classes with typed CSS modules from component directories: `import * as styles from './.module.css';` - Use `styles.` through `className={styles.value}` and keep dynamic class composition in JS/TS; avoid ad hoc string concatenation across unrelated modules. - Prefer static imports for `.module.css` inside the renderer pipeline, and keep them co-located with the component that owns the styles. - For PostCSS + Tailwind + daisyUI integration, keep Tailwind entrypoints in `tailwind.config.ts` and preserve existing variable pipelines in `tokens/` and `src/` style exports where still used. - In esbuild, keep renderer loaders split between `'.module.css': 'local-css'` and `'.css': 'css'`, and ensure `plugins` compose PostCSS transforms before CSS module scoping so shared token imports remain stable. - Preserve legacy global selectors only when an upstream plugin path depends on them, such as `:global(.tabs_list)` in `tabs.module.css`; document every new compatibility class at usage site. - Centralize reusable token values in component-level custom properties (for example `--search-*`, `--tab-*`, `--header-*`) and avoid magic literals in `.module.css` declarations. - Keep `.module.css` scoped styles for component ownership, and use global CSS files only for app shell, base resets, and third-party plugin interop. ## Package boundaries Roadmap item `1.1.1` establishes explicit package boundaries for new development: - `frontend/`: frontend package boundary for renderer-facing code. - `backend/`: backend package boundary for privileged main-process code. - `shared/`: shared contracts (types, constants, and schemas). This split is currently an architectural boundary with incremental migration. Runtime modules still live primarily in `lib/` (frontend) and `app/` (backend). Treat those folders as the active implementation roots until follow-on migration work moves modules under `frontend/src` and `backend/src`. Dependency direction must remain one-way: - `frontend -> shared` - `backend -> shared` `frontend` must not import `backend`, and `backend` must not import `frontend`. Approved exception: - `lib/components/term.tsx` may import `../../app/utils/renderer-utils` to reuse `createRuntimeLatencyMetrics` as the single latency-metrics factory. This exception is explicitly allow-listed in `scripts/check-package-boundaries.mjs`. Use shared imports via TypeScript path aliases: - `@frontend/*` - `@backend/*` - `@shared/*` For `app/` main-process runtime modules compiled into `dist/app/` via `tsgo`, TypeScript path aliases are type-checking conveniences only. Do not add bare runtime `@shared/*` value imports in `app/` modules unless the build pipeline also materializes runtime-resolvable modules under `dist/app/`. Use `import type` for shared contracts and prefer app-local runtime adapters/constants for main-process runtime dependencies. Boundary validation is enforced by `bun run check:boundaries`, which runs as part of `bun run lint` and therefore `make lint`. Follow-up hardening tracked in `docs/tracking-issues.md`: - `BOUNDARY-001`: include CommonJS `require(...)` imports in boundary checks. - `CONTRACT-001`: make `shared/` schema generation independent of legacy `typings/` compatibility re-exports. ## Transport abstraction practice - All internal Velocetty renderer code (command-layer modules **and** component-level UI modules) must use `lib/transport/` adapters instead of direct Electron inter-process communication (IPC) imports or `window.rpc` calls. - `window.rpc` remains available as a global for backward-compatible plugin API access only; internal Velocetty modules must not depend on it. - For command execution and renderer event streams, use `RendererCommandTransport` from `@shared/types/transport` and `transport` from `lib/transport` (barrel module). Do not import the Electron-specific adapter directly. - Keep host-specific IPC details inside `lib/transport` adapters and keep command modules only concerned with transport contracts. - IPC responses are validated at the transport boundary via zod schemas in `lib/transport/ipc-schemas.ts`. When adding a new `IpcCommands` entry, add a corresponding schema to the registry so responses are validated before reaching application code. - Add or update transport adapter tests when changing invocation or subscription paths (for example, `test/unit/electron-ipc-transport.test.ts`). For component-level transport usage, mock `lib/transport/electron-ipc-transport` and verify `on`/`off`/`emit` calls to confirm correct wiring without coupling to internal component rendering behaviour. - Track progress against `lib/TRANSPORT_MIGRATION_MAP.md` and keep all transport-facing command and bootstrap-path follow-ups visible before any PR. Package-local build checks are available via: - `bun run build:shared` - `bun run build:frontend` - `bun run build:backend` - `bun run build:packages` ## Command registry practice Roadmap item `1.2.1` introduces shared command contracts and deterministic registry APIs. Follow these rules for command-system changes: - Define command contracts in `shared/src/types/commands.ts` and import them via `@shared/types/commands` from runtime modules. - Keep registry implementations deterministic by returning `list()` output in a stable order (`CommandId` lexical ordering). - When a command includes `argsSchema`, validate arguments via registry helpers and return structured `CommandValidationError` objects with `code`, `commandId`, and schema `issues`. - Preserve compatibility surfaces used by current runtime/plugin paths (`registerCommandHandlers`, `getCommandHandler`, and `getRegisteredKeys`) until dispatcher migration milestones replace those entry points. - Use `detectKeybindingConflicts(...)` in `app/runtime/plugin-runtime.ts` when testing exact shortcut collisions after precedence merges; the helper reports deterministic conflicts for remaining duplicate shortcuts. - Add or update unit coverage in `test/unit/command-registry.test.ts` and `test/unit/command-registry-validation.test.ts` for create, read, update, and delete (CRUD) semantics, deterministic ordering, compatibility aliases, invalid-schema handling, validator-cache invalidation, and validation error behaviour. - Add or update precedence/conflict coverage in `test/unit/runtime-plugin-settings.test.ts` when changing runtime keybinding merge or conflict-detection behaviour. ## Configuration format practice Roadmap item `1.3.1` moves repository config handling to JSON5-only semantics for active configuration files: - Parse and write `config.json5` as JSON5 (including comments and trailing commas). - Do not rely on `.hyper.js` migration; legacy migration paths were removed. - Persist runtime plugin settings under `config.plugins.` in `config.json5`. - Keep config-validation diagnostics structured with required fields: `path`, `message`, and `suggestedFix` (optional `docHint` and `defaultHint` may be included when available). - Preserve user-authored comments and formatting when roundtripping `config.json5`; avoid full-file canonical rewrites for targeted runtime settings updates. ## Configuration layering and reloadability practice Roadmap item `3.1.2` defines configuration layering rules and hot-reload semantics. Follow these rules when adding or changing configuration settings: ### Layering rules Configuration merges in the following precedence order (highest to lowest): 1. **Runtime overrides** (ephemeral, in-memory only, not persisted) 2. **User config** (`config.json5`) 3. **Built-in defaults** (bundled with the app) Workspace-level overrides are deferred to a future milestone. Merge semantics: - **Objects**: deep merge (nested properties recursively merged). - **Arrays**: replace (user array completely replaces default array). Use `resolveConfigLayers()` from `app/config/layering.ts` to resolve the effective configuration: ```typescript import {resolveConfigLayers} from './config/layering'; import type {configOptions} from '@shared/types/config'; const defaults = {} as configOptions; const userConfig = {} as Partial; const runtimeOverrides = {} as Partial; const effectiveConfig = resolveConfigLayers(defaults, userConfig, runtimeOverrides); ``` ### Reloadability classification Every configuration key must have a reloadability classification: - **`live`**: Changes apply immediately without restart (theme, fonts, keybindings). - **`restart`**: Changes require application restart (shell settings, update channel). Classify new settings in `shared/src/constants/config-reloadability.ts`: ```typescript // In profileConfigReloadability (or rootConfigReloadability for root keys) newSetting: { classification: 'live', // or 'restart' rationale: 'Brief explanation of why this classification was chosen' } ``` Classification guidelines: | Live-reloadable | Restart-required | | ---------------------------------------- | --------------------------------------- | | Theme/UI appearance (colours, padding) | Backend transport (shell, shellArgs) | | Font settings (family, size, weight) | Update channel / auto-update settings | | Cursor appearance (shape, blink, colour) | Environment variables (env) | | Keybindings | WebGL renderer (deferred to CONFIG-001) | | Custom CSS (css, termCSS) | Process-level configuration | ### Detecting and handling config changes Use `createReloadHandler()` from `app/config/reload-handler.ts` to process config reloads with automatic classification: ```typescript import {createReloadHandler} from './config/reload-handler'; import type {configOptions} from '@shared/types/config'; const currentConfig = {} as configOptions; const newConfig = {} as configOptions; const handler = createReloadHandler({ getCurrentConfig: () => currentConfig, applyLiveConfig: (config) => { /* apply live changes */ }, emitRestartWarning: (diagnostics) => { /* notify user */ } }); const result = handler.processReload(newConfig); // result.appliedLive: keys applied immediately // result.restartRequired: diagnostics for keys requiring restart ``` ### Settings UI integration When building settings UI components: 1. Use `useConfigReloadability({configKey})` to obtain `requiresRestart` and `classification` for a setting. 2. Display restart-required indicators using `RestartRequiredIndicator` component, passing `requiresRestart` from the hook. 3. Show inline warnings when users modify non-reloadable settings using `InlineRestartWarning` component, passing `classification` from the hook. 4. Use `keyRequiresRestart(key)` for imperative checks outside React render. Example: ```tsx import {RestartRequiredIndicator, InlineRestartWarning} from '../components/restart-required-indicator'; import {useConfigReloadability} from '../hooks/use-config-reloadability'; function ShellSetting() { const hasChanged = true; // example: derived from form state const {requiresRestart, classification} = useConfigReloadability({configKey: 'shell'}); return (
); } ``` ### Testing requirements Add unit tests when adding new configuration settings: - Verify reloadability classification via `test/unit/config-reloadability.test.ts`. - Validate merge semantics in `test/unit/config-layering.test.ts`. - Test hot-reload detection in `test/unit/config-hot-reload.test.ts`. ### Deferred features The following features are explicitly deferred: - **WebGL renderer hot-reload**: Tracked under CONFIG-001 in `docs/tracking-issues.md`. - **Workspace-level overrides**: Will be implemented in a future milestone. ## Visible-only WebGL rendering practice Roadmap item `2.1.1` introduces visible-only WebGL allocation. Follow these rules when changing terminal rendering behaviour: - Use the pane visibility model in `lib/utils/pane-visibility.ts`, which treats a pane as visible only when it is on the active tab, has non-zero bounds, and is not occluded. - Use `lib/utils/webgl-context-pool.ts` for WebGL allocation bookkeeping. Do not add ad hoc per-component context counters. - Keep renderer switching in `lib/components/term.tsx` and continue reporting renderer changes through `Term.reportRenderer(...)` so diagnostics and About dialog reporting stay consistent. - Respect the `webGLRendererMaxContexts` config value (default `16`, positive integer). Runtime changes require active terminals to restart before the new pool size takes effect. ## WebGL context-loss recovery practice Roadmap item `2.1.2` introduces context-loss fallback and retry behaviour. Roadmap item `2.2.1` adds WebGL allocation and fallback metrics. Follow these rules when changing context-loss handling and renderer instrumentation: - Keep context-loss handling in `lib/components/term.tsx` wired through `onWebGLContextLoss`. - On context-loss or pool-eviction events, immediately detach the WebGL addon and attach Canvas. - Keep retry behaviour bounded via existing failure controls: `webglFailureCount`, `webglLastFailureAt`, `webglCooldownMs`, `webglFailureThreshold`, and `webglFailureDecayMs`. - Keep deterministic retry scheduling in `scheduleDeterministicRendererRetry()` and preserve timer cleanup in `componentWillUnmount()` to avoid leaked retries. - Keep retry entry through visibility/pool coordination (`syncRendererForVisibility` and `getWebGLContextPool(...)`) so retries occur only when pane visibility and pool capacity allow a safe WebGL reattach. - Keep fallback instrumentation observable through: `console.warn('WebGL context lost. Falling back to canvas-based rendering.')` and renderer mode events from `Term.reportRenderer(...)` (`info renderer`, with optional `reason` values such as `context-loss`, `pool-evicted`, and `webgl-init-failed`), which feed renderer summaries in the About dialog. - Keep allocation metrics aligned with renderer telemetry from `Term.reportRenderer(...)` and main-process aggregation in `app/utils/renderer-utils.ts`: - Current WebGL context count (`current`) tracks active WebGL renderer assignments. - Peak WebGL context count (`peak`) tracks the highest observed `current` value during the process lifetime. - Keep diagnostics visibility in the About dialog detail output by including: - `WebGL contexts: current , peak ` - `Renderer fallbacks: total ; reasons: ...` - When tuning context-loss retry thresholds or fallback behaviour, update `docs/roadmap.md`, this guide, and the related renderer/pool unit tests in the same change to avoid docs/runtime drift. - When adding or renaming fallback reasons or allocation metric fields, update this guide, `docs/roadmap.md`, and diagnostics tests in the same change. ## Pseudo-terminal (PTY) batching and frame-timing benchmark practice Roadmap item `2.2.2` adds a synthetic-load benchmark command for validation and evidence capture. - Run `bun run benchmark:pty-frame-timing` to generate deterministic benchmark evidence. - The default evidence path is in the system temporary directory (`os.tmpdir()`): `/benchmark---pty-frame-timing-synthetic-load.json`. - Override the output location with `bun run benchmark:pty-frame-timing -- --evidence-path /tmp/.json`. - The benchmark verifies the current runtime batching contract from `shared/src/constants/runtime-telemetry.ts` mirrored in `app/constants/runtime-telemetry.ts` (`PTY_BATCH_DURATION_MS = 16` and `PTY_BATCH_MAX_BYTES = 200 * 1024`) as used by `app/session.ts`. - Treat benchmark output as valid only when all checks are `true` and `passed` is `true`. - Runtime telemetry for this milestone is emitted through `Term.reportRenderer(...)` (`info renderer.runtimeMetrics`) and consumed in `app/utils/renderer-utils.ts`; keep renderer-side and main-process contracts in sync when adding or renaming metric fields. - Verify diagnostics after instrumentation changes via the About dialog output: keydown-to-send latency, send-to-write latency, frame timing, long-frame counts, and PTY batching-parity status. ## Tab decoration provider practice Roadmap item `1.3.1` introduces the golden-path tab-decoration provider seam. When adding or changing provider behaviour: - Register providers through renderer plugin hooks (`getTabDecorationProviders`) rather than ad hoc tab polling logic. - Keep provider ordering deterministic by sorting with this precedence: `priority` descending, then provider `id` lexicographically, then stable registration index. - Keep list-slot output bounded and deterministic: `badges` max 3 entries and `widgets` max 2 entries after deduplication. - Trigger tab-decoration refreshes from explicit events (`subscribe` callbacks or provider registration lifecycle), never from `setInterval` polling loops. - Add or update focused coverage in `test/unit/tab-decoration-providers.test.ts` and `test/unit/tabs-decoration-updates.test.ts` whenever merge or update logic changes. ## Context key and `when` practice Roadmap item `1.2.2` introduces shared context-key contracts and deterministic `when` expression evaluation. Follow these rules when changing context-aware command or keybinding behaviour: - Define context-key and `when` AST contracts in `shared/src/types/context-keys.ts` and import those contracts through `@shared/types/context-keys`. - Keep parser grammar constrained to the documented operators in `docs/velocetty-design.md` (`&&`, `||`, `!`, `==`, `!=`, `<`, `<=`, `>`, `>=`, and parentheses). - Use `lib/context-key-service.ts` for runtime context-key management and expression evaluation, and reuse compiled expressions (`compile(...)`) when evaluating the same expression repeatedly. - Treat context values as explicit primitives (`boolean`, `string`, `number`, or `null`) and avoid ad-hoc component booleans outside the context-key service. - Add or update unit coverage in `test/unit/context-key-service.test.ts` for all operators, precedence/grouping, parse failure indices, deterministic repeated evaluation, and parser edge cases such as empty expressions, stray operators, identifier variants, and string-escape handling. ## esbuild build pipeline and safeguards The repository now bundles with esbuild by default: - `bun run dev`: esbuild watch mode plus `tsgo --build --watch` - `bun run build`: production esbuild bundles plus `tsgo --build` - `bun run build:hyper-app`: copy-only app artefact pipeline via esbuild support scripts ADR 002 still requires test-first discipline for any follow-on changes to bundler scripts, bundler configuration, or custom esbuild plugin logic. Contract suites must stay in place and green before changing default build paths or plugin behaviour. Required coverage categories: - Translation outcomes: verify CSS Modules bundling, externals mapping, source maps, and production minification output. - Packaging outcomes: verify copied artefacts under `dist/app/` and CLI artefact shape (including shebang integrity). - Bespoke plugin validation: add deterministic unit tests for each custom esbuild plugin path (resolve/load/copy/ignore behaviour and diagnostics). Follow `docs/execplans/replace-webpack-babel-with-esbuild.md` for migration ordering and milestone gates. ## Electron runtime alignment When upgrading Electron, keep runtime and native-module rebuild settings aligned in the same change: - Update `devDependencies.electron` in `package.json`. - Align `@types/node` in `package.json` to the bundled Node.js major for the target Electron release. - Bump the fallback target version in `bin/rebuild-node-pty.cjs`. - Adjust `app/package.json` if runtime dependencies (for example, `node-pty`) need a compatibility bump for the new Electron Application Binary Interface (ABI). - If CI rebuilds native modules, align `.github/workflows/nodejs.yml` `NODE_VERSION` and any architecture-specific Node bootstrap downloads to the same Node.js major family. - Run `bun install` to validate snapshot generation, `install-app-deps`, and `node-pty` rebuilding before running the remaining gates. - The installation pipeline intentionally invokes `node bin/copy-node-modules.mjs` during postinstall. Bun remains the default runner elsewhere, but Node's native copy path is currently the stable option for mirroring large `node_modules` trees on Linux/Windows Subsystem for Linux (WSL) after `install-app-deps`. Current repository runtime baseline after roadmap item `1.4.13`: - `electron` and `electron-mksnapshot`: `^40.2.1` - `@types/node`: `^24.10.12` - CI workflow `NODE_VERSION`: `24.11.1` CI Python/node-gyp baseline after roadmap item `1.4.15` macOS scope: - In CI jobs that prepare native-module builds, create a per-job Python virtual environment for node-gyp bootstrap packages instead of running system-level `pip install`. - Install `pip`, `packaging`, and `setuptools` inside that virtual environment, then set `PYTHON` and `npm_config_python` to the virtual-environment interpreter path for the install/rebuild steps that need node-gyp. - Keep `npm_config_node_gyp` aligned to the workspace `node-gyp` entrypoint in the same job, so Python and node-gyp resolution stay deterministic. - Run this isolated Python bootstrap before `bun install`; this keeps hosted macOS lanes compliant with Python Enhancement Proposal (PEP) 668 (`externally-managed-environment`) and avoids host-level Python mutation. Linux runtime reliability baseline after roadmap item `1.4.15` Linux scope: - Linux ARM CI coverage is Linux aarch64 only; do not reintroduce armv7 (`armv7l`) lanes or release artefact targets. - Prefer native ARM runners for Linux aarch64 CI lanes instead of emulated copy-to-image flows, which previously failed with disk-exhaustion errors. - Keep Linux dependency installation shared via `.github/actions/install-linux-e2e-runtime-deps/action.yml` so fast-lane and deep-lane jobs stay in sync. - Before running `bun install` on Linux aarch64 CI lanes, provision `qemu-x86_64-static`, add the `amd64` dpkg architecture, and install the required x86_64 runtime libraries (`libc6`, `libstdc++6`, `libgcc-s1`, `libglib2.0-0`, `libexpat1`, and `libpcre2-8-0`) so Electron's x64 `mksnapshot` and `v8_context_snapshot_generator` binaries can run. - On Ubuntu arm runners that default to `ports.ubuntu.com`, use explicit apt source entries (`ports` for `arm64` and `archive.ubuntu.com` plus `security.ubuntu.com` for `amd64`) before installing `:amd64` packages. Otherwise, apt tries to resolve `amd64` indexes from `ports` and fails with `404 Not Found`. Keep this source pinning for all later apt invocations in the same job after adding `amd64`; the Linux dependency installation action now applies it automatically when it detects an Ubuntu arm64 host with `amd64` multiarch enabled. - The shared Linux dependency installation action resolves the Advanced Linux Sound Architecture (ALSA) runtime package by availability (`libasound2t64` on newer Ubuntu releases, `libasound2` on Ubuntu 22.04) so the same workflow configuration works across Jammy and Noble runners. - After provisioning amd64 runtime packages on Linux aarch64 CI lanes, export `QEMU_LD_PREFIX=/` so Quick Emulator (QEMU) resolves x86_64 shared libraries from the host multiarch rootfs. - For Linux aarch64 CI lanes, set `SKIP_V8_SNAPSHOT=1` during `bun install` so snapshot generation cannot stall install for hours under emulation. - For Linux aarch64 CI lanes, set `SKIP_NODE_PTY_REBUILD=1` during `bun install` so the lane does not hang in long-running `node-gyp` Electron header extraction for `node-pty`; keep `npm_config_node_gyp` and Python toolchain wiring in place for the remaining native-module install steps. - When Linux aarch64 CI lanes package artefacts after install-time snapshot skipping, set `SKIP_V8_SNAPSHOT_COPY=1` for the packaging step, so CI uses Electron's default snapshots instead of waiting on arm64 custom snapshot generation that can stall under QEMU. - For local Linux aarch64 validation where snapshots are still required, set `SKIP_X64_V8_SNAPSHOT=1` to avoid generating the additional x64 snapshot pass under QEMU. - For Linux aarch64 native-module rebuild reliability, run `bun install` before other gates and keep `npm_config_node_gyp` pointed at the workspace `node-gyp` entrypoint in CI jobs that rebuild native modules. Windows runtime reliability baseline after roadmap item `1.4.15` Windows scope: - Keep Windows CI on `windows-2022` (x64) in the shared build matrix and run Windows install in a dedicated workflow step. This retains Visual Studio 2022 until Electron's node-gyp recognizes the Visual Studio 2026 toolchain used by `windows-latest`. - Run `.github/scripts/setup-node-gyp-python.sh` `"$RUNNER_TEMP/node-gyp-python"` before `bun install`, then set `PYTHON`, `npm_config_python`, and `npm_config_node_gyp` from step outputs for the install step. - Windows CI run `22405749378` (2026-02-25) exposed a native rebuild failure in `Install (Windows)`: `bun install` can fail with `Executable not found in $PATH: "node-gyp.cmd"`. - Mitigate this Windows-only failure mode by running `bun install --ignore-scripts` before the full `bun install`, then prepending `node_modules/.bin` to `PATH` so `node-gyp.cmd` is available during native rebuild. - Do not use `npm install` for this bootstrap in this repository: npm can fail early on the repository override graph (`Override without name`) before Bun install starts. - After the scriptless Bun bootstrap, ensure `node_modules/.bin/node-gyp.cmd` exists. Bun's Windows package shim can expose `node-gyp` without the `.cmd` wrapper; create a minimal `node-gyp.cmd` launcher that delegates to `..\node-gyp\bin\node-gyp.js` before running `bun install`. - For Windows install, map `TMP`, `TEMP`, and `npm_config_tmp` to `${{ runner.temp }}` so `node-gyp` extraction uses a deterministic writable path instead of the short-name `%LOCALAPPDATA%` alias. - `bin/download-mksnapshot.js` now retries transient artefact download failures (for example, `ECONNRESET`, timeout/DNS errors, and 5xx/429 responses) with exponential backoff before failing the installation. - Tune retry behaviour with `MKSNAPSHOT_DOWNLOAD_RETRY_ATTEMPTS` (default `4`) and `MKSNAPSHOT_DOWNLOAD_RETRY_DELAY_MS` (default `1000`) when debugging unstable network environments. - Keep `npm_config_node_gyp` in the forward-slash form used by the `Install (Windows)` step in `.github/workflows/nodejs.yml`: `${{ github.workspace }}/node_modules/node-gyp/bin/node-gyp.js`. Preserve this forward-slash `npm_config_node_gyp` value to avoid introducing path-separator regressions. - Keep `bin/rebuild-node-pty.cjs` running node-gyp through the Node executable (`NODE` environment variable when present, otherwise `node` on `PATH`), not `process.execPath`. CI runs this script via Bun; invoking node-gyp with Bun can trigger Windows header-extraction `EINVAL` failures. - Keep repository script/config text files normalized to LF in `.gitattributes` for extensions checked by Biome (`*.json`, `*.jsonc`, `*.js`, `*.cjs`, `*.mjs`, `*.ts`, and `*.tsx`). Windows checkout can otherwise convert those files to CRLF and trip `make lint`/Biome formatting checks. - Windows aarch64 CI is currently blocked by upstream Bun distribution support. Evidence (captured 2026-02-25): latest Bun release `bun-v1.3.9` (published 2026-02-08) includes `bun-windows-x64*` assets and no Windows arm64 asset. - Mitigation and ownership are tracked in `WINARM64-001` in `docs/tracking-issues.md` (issue: [#35](https://github.com/leynos/velocetty/issues/35), owner: `@leynos`). Re-evaluate lane enablement when a Bun release publishes a Windows arm64 artefact and `setup-bun` supports it. When preparing future Electron upgrades, update these anchors together and avoid merging partial baseline updates. ## GPU fallback launch switch Use the `VELOCETTY_DISABLE_GPU` environment variable when debugging renderer issues caused by problematic GPU drivers: ```bash VELOCETTY_DISABLE_GPU=1 bun run app ``` When this switch is enabled, the main process disables hardware acceleration before Electron readiness and applies software-rendering Chromium flags. ## Chromium startup log noise suppression Electron 40 on Linux can emit repetitive Chromium DBus startup alerts during local runs, including transient `StartTransientUnit` scope collisions. The app now sets Chromium `log-level=3` at startup by default to suppress this known noise. Use environment variables to adjust this behaviour: - `VELOCETTY_SUPPRESS_CHROMIUM_ERROR_LOGS=0 bun run app` keeps Chromium error logs enabled. - `VELOCETTY_CHROMIUM_LOG_LEVEL=2 bun run app` overrides the default Chromium log level used for suppression. - `VELOCETTY_GPU_DIAGNOSTICS=1 bun run app` emits GPU launch diagnostics in stdout. ## React version alignment The renderer runtime depends on React in both the root `package.json` and the packaged app manifest in `app/package.json`. When upgrading React, update both manifests in the same change and keep the app manifest on exact versions to avoid duplicate React instances in plugins. React 19 requires aligning `react-redux` 9.x with `redux` 5.x, plus matching `@types/react` and `@types/react-dom` versions in `package.json`. ## React component composition and translation patterns ## Formatting and linting Run the standard gates before opening a pull request: - `make check-fmt` - `make lint` When changes affect command/transport runtime seams or dependency baselines, run the full release gate set in this order: - `bun install` - `make build` - `make check-fmt` - `make lint` - `make test` When documentation changes, also run: - `bunx markdownlint-cli2 "docs/**/*.md"` - `nixie --no-sandbox` ## Vulnerability auditing practice Supply-chain changes must include a vulnerability scan pass before merge: - Run `bun install` first, so audit output reflects the current lockfile and postinstall build graph. - Generate a human-readable advisory report with `bun audit`. - Verify there are no `critical`, `high`, or `moderate` advisories with `bun audit --audit-level=moderate`. - Produce machine-readable evidence with `bun audit --json --audit-level=moderate` when logs or follow-up automation require it. Roadmap item `1.4.2` is satisfied only when the moderate-threshold audit run is clean. If remediation requires dependency overrides, keep overrides in `package.json` and re-run the full gate sequence before marking work complete. For the current Electron 40 toolchain (`electron-builder@24.x`), keep `ajv` aligned with `@develar/schema-utils` and `ajv-keywords@3` by pinning it to `6.14.0`; moving back to Ajv 8 without upgrading that stack will break `bun install` during postinstall. Follow the [Electron runtime alignment](#electron-runtime-alignment) section above for the canonical packaged dependency mirror command: `node bin/copy-node-modules.mjs`. ## Type checking Type checking runs via `tsgo` and the shared `tsconfig.typecheck.json` project: - `make typecheck` - `bun run check:types` `tsgo` 7.0.0-dev.20260128.1 supports `--build`, `--watch`, `--pretty`, `--preserveWatchOutput`, and `--project`; the `dev` and `build` scripts rely on those flags. ## Tests ### Unit tests (Bun) Unit tests run under Bun's built-in test runner. Use one of the following: - `bun run test:unit` - `bun test test/unit` - `bun run test:coverage` (writes text output and an LCOV (line coverage) report under `coverage/`) - `make coverage` - For roadmap item `9.1.1`, run focused coverage with: ```bash bun test --coverage \ test/unit/command-registry.test.ts \ test/unit/command-registry-validation.test.ts \ test/unit/context-key-service.test.ts \ test/unit/runtime-plugin-settings.test.ts ``` Until Bun branch-threshold enforcement is wired for this repository, use the current codebase target from `docs/velocetty-hyper-codebase.md` §6.6.7.2 as the local pass/fail basis for touched core modules: at least 60% line coverage and 50% function coverage as the Bun-reported proxy for the documented 50% branch target. `make test` executes the shared unit suite through `bun run test:unit:run`, which runs with `--concurrent` (maximum concurrency) as the default. Roadmap item `9.3.1` removed the dedicated bootstrap-process quarantine by moving renderer bootstrap assertions behind injected seams instead of file-scope module mocks; roadmap item `9.3.2` removed the serialized guardrail from the default local and CI unit gate; and roadmap items `9.3.3` through `9.3.7` hardened test-suite isolation so explicit concurrency can be the default. When checking for order-dependent regressions, replay the unit suite with fixed seeds and explicit concurrency: ```bash bun test --concurrent --randomize --seed 2444615283 test/unit bun test --concurrent --randomize --seed 1337 test/unit bun test --concurrent --randomize --seed 20260306 test/unit ``` To obtain a serialized reproduction path while diagnosing a suspected cross-file race, use one of the explicit diagnostic scripts instead of changing the default gate: ```bash bun run test:unit:serialized bun run test:unit:serialized:shuffled ``` For focused stress testing of specific suites under explicit concurrency, use targeted runs: ```bash bun test --concurrent \ test/unit/rpc-client.test.ts \ test/unit/term-report-renderer.test.ts ``` This command supplements the default `make test` path and the seeded randomized reruns above. For roadmap item `9.3.4` and similar filesystem-fixture isolation work, use a focused explicit-concurrency stress run against the directory-bootstrap helper suite: ```bash bun test --concurrent test/unit/ensure-directory-path.test.ts ``` Keep temporary-directory ownership and teardown scoped to each test, either directly in the test or via a helper that returns per-test cleanup, so ownership is not shared. Do not use a shared file-scope cleanup queue for temporary roots in suites that must survive explicit `--concurrent` runs. For roadmap item `9.3.5` and similar snapshot/bootstrap or CLI-config isolation work, use a focused explicit-concurrency stress run against the snapshot and CLI behaviour suites: ```bash bun test --concurrent \ test/unit/v8-snapshot-util.test.ts \ test/unit/cli-api-behaviour.test.ts ``` Keep snapshot/bootstrap state isolated from `globalThis` during unit tests. Prefer explicit bootstrap helpers that accept a test-owned runtime host and return a restoration handle for any patched module loader state, so each test can clean up the loader it installed without relying on file-scope teardown. For CLI-config tests, do not share file-scope mutable mock state, shared `process.env` mutation, or module-scope config-path capture across tests. Prefer a per-test API factory with injected filesystem, registry, and environment state so explicit `--concurrent` runs keep request history, config-path resolution, and parsed-plugin state isolated per test instance. For roadmap item `9.3.6` and similar concurrency-hotspot cleanup, use a focused explicit-concurrency stress run against the remaining long-lived mock and global hotspots: ```bash bun test --concurrent \ test/unit/runtime-tab-provider-registration.test.ts \ test/unit/command-registry-compat.test.ts \ test/unit/config-import-json5.test.ts ``` Suites that must survive that probe must not rely on `afterAll(...)` to tear down `mock.module(...)` registrations, temporary `window` installs, or other process-global shims. Prefer per-test harness helpers that either return a cleanup callback or accept injected dependencies directly. When a module under test captures transport, config, or filesystem state at module scope, add the smallest behaviour-preserving factory seam needed, so tests can provide test-owned dependencies without long-lived module mocks. For roadmap item 9.3.7 and similar timer/logger-dependent module work, use injected seams instead of process-global mutations: - Pass timer implementations (`setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`) through component props or function options rather than replacing `globalThis` methods. - Pass logger implementations (`console.error`, etc.) through function options rather than replacing `console` methods. - Keep global fallbacks for production code when seams are not provided. - Test with explicit `--concurrent` stress runs to verify isolation: ```bash bun test --concurrent test/unit/notification.test.ts bun test --concurrent test/unit/updater.test.ts ``` Suites that rely on timer or logger seams must not mutate global state during test execution; instead, provide test-doubles through the module's public interface. ### End-to-end (E2E) tests (layered strategy) End-to-end tests are split into two lanes and require packaged binaries in `dist/`. Fast lane (required on pull requests): - Run `bun run test:e2e:fast` (or `bun run test:e2e`). - Executes Bun-driven smoke checks in `test/e2e/`. - Asserts renderer readiness and fails on critical renderer console errors. - Supports `E2E_DRIVER=playwright|spawn` overrides; CI defaults to spawn-mode markers, with a macOS packaged-launch fallback that accepts missing renderer-ready marker output only when the process remains alive through an additional stability window bounded by remaining test-timeout budget. - Supports `E2E_DEBUG=1` for verbose launch logs and `E2E_CAPTURE=1` for screenshot capture. Deep lane (scheduled and release validation): - Run `bun run test:e2e:deep`. - Executes Playwright Test under Node.js using `playwright.e2e.config.ts` and `test/e2e-deep/`. - Installs Playwright Chromium on demand before execution. - Validates the first interaction-path scenario (terminal input and rendered output). - Retains full diagnostics on failures: stdout/stderr logs, renderer console logs, screenshots, and traces. - Runs in CI on Linux for scheduled checks, manual `workflow_dispatch`, and pushes to `master` and `canary`. - Deep-lane failures on `master` and `canary` are release-blocking. For screen readers: The following sequence diagram shows fast-lane execution, including main-process readiness/error markers consumed by Bun E2E assertions. ```mermaid sequenceDiagram actor Dev participant BunTest as Bun_test_runner participant ElectronMain as Electron_main_process participant Renderer as Electron_renderer participant Console Dev->>BunTest: run bun run test:e2e:fast BunTest->>BunTest: set RUN_E2E=1 BunTest->>ElectronMain: launch packaged Electron app ElectronMain->>Renderer: load renderer URL Renderer-->>ElectronMain: did-finish-load ElectronMain->>ElectronMain: RUN_E2E == 1 ElectronMain->>Console: log [e2e] renderer-ready ElectronMain->>Renderer: send init(uid, profileName) Renderer->>ElectronMain: console-message(level,message,line,sourceId) ElectronMain->>ElectronMain: if level >= error ElectronMain->>Console: log [e2e][renderer-error] sourceId:line message BunTest->>ElectronMain: wait for renderer-ready marker ElectronMain-->>BunTest: renderer-ready observed BunTest->>BunTest: assert readiness and no critical renderer-error logs BunTest-->>Dev: report fast-lane E2E result ``` Figure 1: Fast-lane E2E sequence from Bun invocation to readiness/error assertions. For screen readers: The following sequence diagram shows deep-lane execution through Playwright CLI/Test, including interaction-path assertion and artefact reporting. ```mermaid sequenceDiagram actor Dev participant Bun as Bun_cli participant PWCLI as Playwright_CLI participant PWTest as Playwright_Test_runner participant ElectronMain as Electron_main_process participant Renderer as Electron_renderer participant Console as Console Dev->>Bun: run bun run test:e2e:deep Bun->>Bun: test:e2e:prepare (rimraf dist/tmp/root/test) Bun->>PWCLI: install chromium PWCLI-->>Bun: chromium installed Bun->>PWCLI: test -c playwright.e2e.config.ts PWCLI->>PWTest: run tests in test/e2e-deep PWTest->>ElectronMain: launch packaged Electron app ElectronMain->>Renderer: load renderer URL Renderer-->>ElectronMain: did-finish-load ElectronMain->>Console: log [e2e] renderer-ready ElectronMain->>Renderer: send init(uid, profileName) PWTest->>Renderer: type sentinel command into terminal Renderer-->>ElectronMain: console-message events ElectronMain->>Console: log high severity errors as [e2e][renderer-error] PWTest->>Renderer: wait for rendered output containing sentinel Renderer-->>PWTest: terminal output with sentinel PWTest->>PWTest: assert interaction path PWTest-->>PWCLI: report test result PWCLI-->>Dev: generate report and artefacts on failure ``` Figure 2: Deep-lane E2E sequence from Bun command orchestration to Playwright interaction and reporting. Before either lane, build packaged artefacts with `bun run dist` if they do not already exist. ## Default test gate `make test` runs linting plus the unit test suite. It intentionally omits E2E tests to keep the default loop fast. ### Sub-component extraction When a renderer component grows beyond a single responsibility, extract internal sub-components as module-private `const` declarations in the same file. Only export the public-facing component. This keeps the module API surface small while enabling focused unit tests through the parent's rendered output. Example: `lib/components/searchBox.tsx` defines `SearchResultsCount` and `SearchNavigation` as internal constants and exports only `SearchBox`. ### Translation key conventions Translation keys live in `lib/hooks/use-translation.ts`. Every key must have an English default in `headerLabelDefaults`; partial locale dictionaries fall back to the default for any missing key. When adding new labels: 1. Add the key and its English value to `headerLabelDefaults` in `lib/hooks/use-translation.ts`. 2. Add translations to each locale dictionary in `translationDictionaries` in `lib/hooks/use-translation.ts`. 3. Add the prop to the presentational component's prop type. 4. Wire the label into the logic hook (`useSearchBoxLabels`) and then into any wrapper that threads those hook-provided labels into the presentational component; mention `useTranslation` only as the historical exception where a wrapper still directly calls it. 5. Extend `test/unit/use-translation.test.ts` to assert the key in each supported locale. 6. Extend/verify the SearchBox DOM-wiring suite (`test/unit/search-box-css-modules.test.tsx`) to ensure the label is rendered and attached correctly in the SearchBox component. ### Preserving legacy plugin-targeted class names When migrating styled-jsx blocks to CSS Modules, class names that external plugins or user custom CSS may target must remain attached to the same elements. Apply both the CSS Module token and the legacy string: ```tsx
``` Document intentionally retired legacy class names in the migration ExecPlan under `Decision log`. ### Label-threading with translation wrappers Accessibility labels that vary by locale are threaded via a narrow prop interface rather than read directly inside the presentational component. The pattern has two layers: 1. **Presentational component** (`SearchBox`) accepts every label as an explicit typed prop. This makes labels testable in isolation and keeps the component independent of the translation mechanism. 2. **Translation logic hook** – encapsulate translation lookup in a dedicated logic hook (e.g., `useSearchBoxLabels`) that calls `useTranslation()` and returns the mapped labels. View components receive labels via props and remain pure: ```tsx const useSearchBoxLabels = () => { const t = useTranslation(); return { searchLabel: t('search'), noResultsLabel: t('noResults'), matchCaseLabel: t('matchCase'), matchWholeWordLabel: t('matchWholeWord'), useRegexLabel: t('useRegex'), previousMatchLabel: t('previousMatch'), nextMatchLabel: t('nextMatch'), closeLabel: t('close') }; }; // Usage in a parent component type ParentComponentProps = Omit< SearchBoxProps, keyof ReturnType >; const ParentComponent = (props: ParentComponentProps) => { const labels = useSearchBoxLabels(); return ; }; ``` **Exception:** `TranslatedSearchBox` in `lib/components/term.tsx` is a documented wrapper component that calls `useTranslation()` directly. This is an intentional exception to the hook-based pattern for historical compatibility; new code should prefer the `useSearchBoxLabels` approach.