Titles are stored display text (layout templates, session snapshot,
broadcast registry), so a pane used to keep the wording of the language it
was opened in, and nextTerminalTitle could not even parse the number back
out of another language's pattern (numbering restarted, duplicates).
New pure module src/shared/terminalTitle.ts: resolution tries all four
languages' patterns, rendering uses the active one; i18n gains tFor(lang)
for off-language rendering. Workspace retitles auto-numbered local tabs in
place on language switch, snapshot restore and template apply; SSH panels
(user-typed connection names) are never touched. dockview's title-change
event propagates the new title to the broadcast registry and the snapshot
save with no extra wiring.
New test terminal-title.mjs (16 writer/reader language pairs, non-auto
title boundaries, gap filling); offline suite grows 17 -> 18.
- updater-fallback.mjs (82 assertions): GitHub probe fallback to Gitea,
timeout budgets, in-flight check never stuck in 'checking'; updater.ts
gains a setUpdateTimeouts test seam, electron-updater aliased to a stub
- ipc-guard.mjs (43): trusted-frame guard exercised through real
registerIpc handlers with forged senderFrames; electron-stub now records
registrations via globalThis so bundle and test share one instance
- log-sanitizer.mjs (71): CSI/OSC/charset state machine, alt-screen fold,
byte-split fuzz equal to whole-chunk output; fixes a wrong comment
- sftp-timeout.mjs (39): per-op timeouts (metadata 30s, transfer chunk 60s,
open 10s) evict half-dead channels with one retry, slow-but-progressing
transfers untouched, late rejections never unhandled
- reservedAccelerators.ts: single pure isReservedAccelerator shared by the
settings recorder and applyGlobalShortcut (the two tables had drifted —
main now also refuses Ctrl+=/-/0/PgUp/PgDn legacy values); 56 assertions
- ssh-loopback.mjs loads the real ssh.ts via a bundle (50 assertions):
TOFU pinning, fail-closed stores, auth gate, connect budget
Offline suite grows 13 -> 17. Renderer test framework evaluated: not
introducing vitest/jsdom; pure logic keeps being extracted and tested
through the existing bundle harness.
New localPathGrants.ts: an in-memory registry of paths the user picked in a
native dialog. Every check resolves realpath + stat at grant and use time;
Windows case folding; missing files, directories-as-files, devices and
symlinked parents can never pass. SFTP upload/download and zmodem send/
receive now refuse renderer-supplied local paths that were never granted,
returning the resolved path so callers never re-traverse a symlink. keyPath
is validated as a regular file <= 1MB before reading (a device node would
have blocked the UI thread forever). Tests inject a stub policy via
setLocalPathPolicy; production always defaults to the real registry.
Also: webPreferences now explicitly pins contextIsolation/nodeIntegration/
webSecurity instead of relying on defaults.
New offline test tests/local-path-grants.mjs (37 assertions incl. symlink
escape); offline suite grows to 13. i18n: 6 main.sftp/main.key error keys
in 4 languages.
- remove the application menu while locked (lockMenu.ts): Alt reveals the
default menu and its mouse-clickable Reload/DevTools/Zoom items bypass
before-input-event entirely; menu is rebuilt from the default template on
unlock, startup-restored locks covered from initLockController
- extract the keyboard classification into pure lockShortcuts.ts
(isLockBlockedShortcut/isPanicLockChord) with a table-driven test
(lock-shortcuts.mjs, 29 cases) — the v1.0.17 lockout escaped CI because
this decision lived inline in createWindow()
- reserve Ctrl+L in the global show/hide shortcut recorder: a global
registration intercepts the chord at OS level and silently disables the
panic lock
- skip auto-repeat keydowns in the panic-lock branch (held Ctrl+L on an
unconfigured app re-read lock.json + settings.json per repeat)
- LockScreen: re-sync the cooldown clock on visibilitychange/focus/pageshow
so a suspended renderer timer cannot leave the password input disabled
past the real deadline
Lock screen (main-window overlay, no second window):
- scrypt password verifier in <userData>/lock.json (per-write salt,
timingSafeEqual); salt/hash/password never leave the main process
- lock now / idle auto-lock / lock at startup, growing failure cooldown,
lock flags persisted so a quit-and-relaunch cannot bypass the lock
- locked shell and body portals go inert while sessions keep running;
menu accelerators (reload, DevTools, zoom) are swallowed while locked
- settings gains a Lock tab; all copy in zh-CN/zh-TW/en/ja
Security and stability:
- packaged builds ignore ELECTRON_RENDERER_URL / OT_UPDATE_URL (devEnv)
- renderer preload runs with sandbox: true
- unreadable known_hosts store fails closed instead of being overwritten
- connect-time secrets gated by the bookmark's auth method (connectPromptFor)
- ssh stream teardown is idempotent: PTY_EXIT broadcasts exactly once
- sysinfo polling is refcounted for split panes (forceStopPolling on close)
- session-log index entries are path-contained; settings store writes
atomically with EPERM/EBUSY retry
- sync-changelog tolerates CRLF checkouts (was a silent no-op)
- retry ssh2 host-key generation (flaky malformed key, ~1/500)
Tests: lock-store + lock-controller suites; transport-death PTY_EXIT e2e;
GitHub Actions CI (typecheck + 10 offline tests + build)
Rules & engine:
- 22 presets (was 11): split status into okstate/warnstate/badstate, add delop,
createop, danger, secret, level, exitcode, percent, http; status words are
case-insensitive and cover the ✓ ✔ ✅ ✗ ✘ ✖ ❌ ⚠ symbol set
- value bands: the first number in a match picks the colour
(percent: <20% red / 20-50% yellow / 50-80% light green / >=80% green)
- optional per-rule `caseInsensitive`, `category`, `bands`; load-time
`refreshBuiltinRules` upgrades untouched built-in patterns in place
Settings page:
- three-way master switch `highlightMode` (all / basic / off); `basic` runs only
the five safety+status rules and `off` empties the rule set rather than
bypassing HighlightStream (which would drop the held tail)
- category column + grouping (`highlightGroupByCategory`), per-rule hit/duration
stats (`highlightStats`, opt-in sink, snapshot once a second), theme-following
colours (`highlightThemeColors`, hue-bucket mapping onto the ANSI palette),
import/export JSON envelope, live preview through the real engine
- named rule subsets bound per host (`highlightPerHost` + `highlightProfiles`
+ `SshConnection.highlightProfileId`); empty ruleIds = every rule
Tests: new tests/hl-rules.mjs (word boundaries, case flag, negative words,
bands, import/export, preview, basic mode, categories, stats, theme colours,
profiles) + profile round-trip in tests/settings-store.mjs
- esbuild alias fixed in the session/sysinfo/sftp test commands (they
could not build at all), .sftp-svc.mjs build documented, real
connection-stability assertions
- tests/build-bundles.cjs builds every bundle fresh; npm test runs the
seven offline suites; esbuild pinned in devDependencies
- remove the assertion-less .exact-inline.mjs; ignore/clean test temp dirs