P1:
- settingsStore: back up an unparseable settings.json to .bak before
falling back to defaults, so the next mutation can no longer silently
wipe custom themes/highlight rules
- ptyDispatcher: fan out per-session data/exit handlers (Set instead of
a single slot) so SSH split panes stop stealing each other's stream
- FilePanel: monotonic refresh token keeps stale listings from painting
over a newer navigation; upload finish no longer yanks the panel back
- settings.css: active settings-tab label derives from --chrome-fg so it
stays visible on the shipped light themes
P2 (main/renderer):
- paste guard: a paste ending in a newline always confirms
- Workspace: closing an SSH pane no longer seeds the local cwd with a
remote path
- tray: skip close-dialog continuation on a destroyed window
- commands: close zombie 'in-progress' session logs at hydrate
- zmodem: clear the stale offer timer before arming a new one
- connectionsStore: coerce/validate renderer input before persisting
- sftp: OperationError marker class keeps translated errors out of the
transport-retry classifier
- CommandsPanel: surface save failures inside the dialog
- ConnectionSidebar: drop a tautological tooltip condition
P2 (i18n/tooling/tests):
- localize the 16 ANSI color labels and the highlight sample text
(21 new keys across zh-CN/zh-TW/en/ja)
- sync-changelog: keep ### subheadings, normalize CRLF notes
- release.cjs: GitHub release reuse-by-tag (idempotent re-runs); fail
loudly on a failed Gitea asset listing
- commands-store test: absent historyEnabled now truly tests absence
The renderer-side guard added in v1.0.17 called preventDefault on every
keydown while locked. A keydown's default action IS inserting the
character into the focused field, so the lock screen's password box
received nothing and a locked app could never be unlocked. Menu
accelerators are already stopped in main (before-input-event); the
renderer guard now just skips its own logic.
Also: Ctrl+L locks the screen from anywhere in the app, terminals
included. The chord is only taken when a lock actually engages, so an
unconfigured app keeps Ctrl+L for the shell's clear-screen.
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
WER records AppHangTransient with no stack, so measure lag in both
processes and log it on recovery: [main] event loop stalled / [renderer]
main thread stalled. Tells a main-process block from a renderer freeze
the next time the app 'freezes then recovers'.
- upload: per-chunk buffer (ssh2 re-reads the overflow tail after the ACK;
a reused buffer silently corrupted every file >= ~254KB)
- close the cached SFTP channel on eviction, attach an 'error' handler,
close the download handle, time out execQuiet, fail partial deletes
- per-session StringDecoder for the ssh data plane (CJK mojibake), real
exit codes, safe replay truncation, zmodem abort/counter/timer fixes
- sysinfo: idempotent poll end, error routing, per-poll watchdog, proc(5)
CPU total; expand cd ~/$HOME/%USERPROFILE% paths; log sanitizer fixes
- security: will-navigate guard, central IPC sender check, scheme
allowlist for openExternal, single-instance else branch, layout id and
log-name whitelists, custom theme sanitizing, atomic JSON writes with
EPERM retry in store.writeJson
- updater: per-attempt feed choice, quitAndInstall relaunch, dev guard,
update-state getter
i18n
- shared/i18n: dependency-free t() with flat per-namespace dictionaries
(common/settings/workspace/terminal/ssh/panels/main), zh-CN fallback
- language picker in Settings -> System; antd ConfigProvider locale follows it
- main process tracks the language too: tray menu, close prompt, ssh/sftp/
zmodem errors and the log TUI marker are translated; tray rebuilds on change
changelog
- CHANGELOG.md (zh-CN canonical) + .zh-TW/.en/.ja, bundled via ?raw and read
per interface language with per-version fallback to zh-CN (no network)
- sync-changelog.cjs merges RELEASE_NOTES[.<lang>].md per release; release.cjs
refuses to publish without a zh-CN entry for the version
settings robustness
- closeAction 'ask' survives the sanitizer (was silently coerced to 'tray',
which made the 'ask every time' option dead)
- highlight rules are repaired instead of dropped: string priority, 0/1
enabled, missing fg colour; unknown fields preserved
- load-time warnings are written to settings-warnings.log (deduped, capped)
terminal/UI
- configurable terminal toolbar: open working directory (default on), session
log recording (off), open logs folder (off)
- global shortcut field records key combos (modifier or F-key required)
- input suggestions + command history default to off, with a one-time reset
migration for existing installs
- settings dialog scrolling fixed (antd v6 renamed the tabs container), theme
gallery nested scrollbar removed, joined segmented pickers with readable
selected-state text
tests: settings-store.mjs (15 checks) added; commands-store.mjs updated for
the new off-by-default history setting
- Global shortcut setting: press-to-record input (Esc cancels, Backspace
clears); requires a modifier or F-key so plain typing can't be hijacked
- Terminal toolbar: three settings-gated buttons — session-log record
(default off), open logs dir (default off), open working directory
(default on, new; local sessions only, cwd tracked via cd/OSC 7)
- Input suggestions + command history now default off, with a one-time
migration that resets persisted true values for existing installs
- Settings dialog: fix broken scrolling — antd v6 renamed the Tabs scroll
container to .ant-tabs-body-holder; theme gallery drops its nested
scroll (single outer scrollbar)
- Offline changelog: CHANGELOG.md at repo root bundled via ?raw; About tab
reads it first, network sources stay as fallback; scripts/sync-changelog.cjs
merges RELEASE_NOTES.md per release (release.cjs fails without an entry)
- commands.ts history prefs default aligned with new off-by-default
- Derive tab-bar/chrome palette by background luminance: light themes keep
a near-background bar with black-tinted tab overlays instead of a muddy
gray strip; dark themes unchanged
- Theme dockview tabs via the group-scoped --dv-*-tab-* vars its own rules
consume (they outspecify our .dv-tab rules and leaked abyss navy onto
light tabs); bump inactive-tab hover specificity to match
- Convert settings dialog + highlight editor hardcoded white text/border
tints to color-mix over --chrome-fg so panes stay readable on light themes
- commands.ts: per-file log write buffer with a single drain loop per file
(burst output coalesces into one appendFile per IO tick, chain no longer
grows per logWrite); stop-tail rides the same buffer
- settingsStore: serialize all writers through mutateSettings() queue that
re-reads latest state per mutation (tray close-action vs settings UI full
saves no longer clobber each other)
- tests: burst ordering + stop-tail case for the log buffer
A dev instance shared the installed build's userData directory and lock, so
starting it demanded killing the real app and it wrote test settings and
session snapshots into the live profile. Dev now uses OpenTerminal-dev and
tags its window title '(dev)'; both instances run side by side.
Recorded logs carried the raw stream: SGR colors, cursor moves,
synchronized-output markers, and every TUI redraw frame — unreadable in an
editor and far larger than the visible output (a short kimi session logged
21.5KB of which 4.4KB is text). A per-session sanitizer now strips ANSI
statefully across chunk boundaries, collapses carriage-return overwrites
(progress bars keep only their final text), and suppresses alternate-screen
frames with a marker line. Ink-style inline TUIs redraw in the normal
buffer and cannot be frame-collapsed without screen emulation; their
committed lines are preserved.
path.isAbsolute('d:') is false on Windows, so a drive-relative cd resolved
against the current base, produced a nonexistent path, and the pane kept
its creation directory in the snapshot. Map a bare drive-letter argument
to the drive root (the fresh-shell answer; we cannot know the drive's
remembered directory).
- updater: dual-feed state machine, manual check/download/install IPC,
changelog from Gitea releases API with GitHub fallback
- settings: new About tab (version, channel, auto-check toggle, progress)
- system.autoCheckUpdate setting gates the startup check