- 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.
- README: add lock-screen section, refresh test list (13 offline tests),
updater fallback order, data locations, architecture tree
- STATE.md: v1.0.20, current release.cjs flow (no --skip-github), M11-M13
- ci.yml: actions/cache for the ~100MB Electron binary keyed on lockfile
- .gitignore: ignore .env.* but keep committed templates
- scripts/archive-releases.cjs moved in from tmp-test; KEEP_TAG is now a
required CLI arg (a hardcoded default is how the wrong version gets wiped)
- lockShortcuts: isPanicLockChord matches input.code ('KeyL') so Dvorak and
non-Latin layouts trigger Ctrl+L lock correctly
- settings: drop the dead single-option update-channel Select from About tab
- Workspace/App: memo(IconRail/LayoutFlyout), useMemo layoutMenu keyed on
language, useCallback onOpenSettings; drop unused IconRail onSplit prop
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.
- TerminalView: replace whole-settings subscription with five useShallow
field groups; theme writes no longer refit every pane, and unrelated
settings writes no longer touch xterm options at all. useResolvedTheme
is now a shallow subscription + memoized lookup. TerminalHandle was dead
(no caller ever passed a ref) — dropped forwardRef/useImperativeHandle
- FilePanel: fixed-row-height (24px) windowing above 200 entries, no new
dependency (antd 6 ships @rc-component/virtual-list only transitively);
per-row Dropdown kept. NOTE: .sftp-row is now box-sizing:border-box
height:24px, keep in sync with ROW_HEIGHT in FilePanel.tsx
- PanelErrorBoundary wraps each dockview panel so a pane crash no longer
unmounts the whole workspace (i18n: workspace.error.panelTitle/panelRetry)
- SshBottomPanel: memo(FilePanel) — sessionId is the only prop and constant
- TransferPanel: ref-held Map + version counter replaces per-event Map
copies; memo rows skip unchanged entries
- MonitorPanel: ResizeObserver/canvas setup runs once, data updates only
redraw
New terminal.backgroundImageDim setting (0-90%, default 0 = off). A black
scrim layer sits between the background image and the xterm screen, so
bright wallpapers stay readable at any opacity: unlike the opacity slider
(which blends the image toward the dark dock base), the scrim darkens the
image while preserving its saturation, and the raised minimumContrastRatio
(4.5) keeps lifted text legible on top.
- settings: backgroundImageDim with deepMerge type-fallback sanitize
- TerminalView: render .term-bg-dim only when image set and dim > 0
- ThemeSettingsTab: slider follows the existing draft + onChangeComplete
pattern (no settings writes while dragging)
- i18n: settings.theme.backgroundImageDim(+Desc) in zh-CN/zh-TW/en/ja
- AGENTS.md: document the third image-mode invariant
On light themes the pane base is near-white (--chrome-bg), so lowering the
background image opacity washed the wallpaper out to white instead of
darkening it, and the theme's dark foreground text became unreadable.
- terminal.css: in has-bg-image mode the dock gets a fixed dark base
(#0d1117), so the opacity slider always dims toward dark regardless of
theme
- TerminalView: raise xterm minimumContrastRatio from 1 to 4.5 (WCAG AA,
same as VS Code's terminal default) while an image is set; xterm treats
the transparent background as black luminance, so dark foreground colors
are lifted to stay readable over the wallpaper. Also fix the option name
(minContrastRatio is silently ignored; the real key is
minimumContrastRatio)
- i18n: update backgroundImageDesc in zh-CN/zh-TW/en/ja
- AGENTS.md: document the two image-mode invariants
- grouped view actually clusters: drop the priority column's defaultSortOrder
that made antd re-sort the dataSource and undo the category clustering
- replace import is Popconfirm-guarded and the import mode resets to append
each time the dialog opens (it stayed on the destructive choice)
- rule editor exposes the basic flag (basic-mode membership) with a switch;
clearing it on edit now actually removes the flag
- rule/profile writes read the store at call time instead of the render-scoped
array, so two writes in one React batch no longer drop the first
- profile editor lists the rules a non-empty profile leaves out (uses the
previously dead excludedByProfile helper)
- bands editor keeps rows sorted by min; band preview keys by index (duplicate
min no longer collides); clear-background also closes the bg picker
- TerminalView pushes compiled rules into the HighlightStream from an effect
instead of during render
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
- keyPath field no longer tells users to enter a server-side path
- settings.highlight.builtin.* notes, timeout messages in four languages
- prototype-safe dictionary lookup; language as a render-time dependency
- Partial<AppSettings> saveSettings contract, update:stateGet channel
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