fix(ime): actually ship @xterm/xterm 6.1.0-beta.304 (v1.0.21 built from stale
node_modules with 6.0.0); add predist dependency-consistency gate
(scripts/verify-deps.cjs) and rename-retry shim for the AV scan EPERM race
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.
Renderer code is fully bundled by Vite into out/renderer; externalizeDepsPlugin
only applies to main/preload, so renderer packages in 'dependencies' were
shipped twice. Moved to devDependencies: @xterm/*, antd, dockview-react,
react, react-dom, zustand. Kept in dependencies (imported by src/main):
@lydell/node-pty, ssh2, zmodem.js, font-list, electron-updater. undici is
now explicit (release.cjs/download-stats.cjs require it). Also dropped the
dead @xterm/addon-serialize (zero imports anywhere).
Verified: npm test 17/17 green, dist:dir builds, asar node_modules contains
only main-process deps, asarUnpack natives intact, win-unpacked smoke
(window, theme, pty spawn) passes.
- 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
- 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
Skip assets a previous partial run already uploaded (the POST 422s on
duplicates, so a retry could never get past them), and retry transient
network errors on large proxied uploads.
checkWithFallback now probes api.github.com through a dedicated system-proxy
session with a 20s AbortSignal timeout before choosing the feed: reachable ->
GitHub releases (with Gitea as the error fallback), unreachable/timeout ->
straight to the domestic Gitea channel (forced direct), so proxy-less users
never hang on a dead proxy. Feed is decided before touching the updater, so
there is never a raced concurrent checkForUpdates. Docs updated: AGENTS.md
update-mechanism section and the release flow (GitHub assets ship per version
again, release.cjs runs with no skip flags).
The hardening-round edit left `(): Promise<Response> =>` in a plain .cjs
file; every run since that commit died at parse time before reaching any flag
handling.
- 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
- compileRules precomputes the full SGR open sequence per rule and per band;
wrap() is now pure concatenation, bandOpen() a table lookup (output bytes
unchanged, bg keeps its unvalidated white-fallback)
- claim() replaces the linear overlaps() scan: claimed spans stay sorted by
start, O(1) append on the common left-to-right sweep plus one binary search
otherwise (match-dense 200KB payload: -19% one-shot, -56% streamed)
- HighlightStream: bufferHasEsc flag skips the O(buffer) escape rescans when
neither the held text nor the chunk contains ESC, fixing quadratic rescan on
escape-free floods (plain 200KB streamed: -54%)
- applyHighlights tracks the consumed match budget incrementally instead of
two budgets.reduce() passes per text token
- ESCAPE_RE now covers DCS/APC/PM/SOS (BEL or ST terminated) and single-char
Fe escapes (DECSC/DECRC/NEL/RI/RIS, orphan ST); isIncompleteEscape holds cut
tails of the new families; split-smoke exercises every cut point through them
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)
.hl-master carried margin-right:auto to push the buttons right when the
toolbar held a single control; with four it flex-shrank each one and the
captions wrapped one character per line. Group the toolbar into a controls
cluster and a right-aligned actions cluster (margin-left:auto so the buttons
stay on the right edge on the line they wrap onto), let .hl-master never
shrink or wrap, box the per-host profile section in its own bordered card
with a left-aligned hint, and drop the fixed 56px editor label width that
broke "包含的规则" onto two lines.
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
- website/: zero-dependency static landing page (HTML+CSS), corporate
light theme, real app screenshots, download links pointing to the
git.codingplan.site release channel (exe/msi) with GitHub mirror
- .github/workflows/deploy-website.yml: deploy website/ to GitHub Pages
on push to main (paths: website/**)
- package.json: author -> CodingPlan.Site
- README: link to the site and its source folder
antd's runtime-injected .ant-tabs-tab-active .ant-tabs-tab-btn rule
(colorPrimary blue) out-ranks a same-specificity stylesheet rule, so the
active label stayed blue on the accent-tinted row; add the .ant-tabs-tab
class to out-specify it.
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'.
latest.yml and release-notes.md change every release, but the atomic
publish no longer wipes the channel up front, so their PUTs now hit the
previous release's stored file. Swap them in place (delete + put, one
small file); version-named payloads keep the same-size keep rule.
setLanguage() during App's render fired onLanguageChange, which made the
same component's useSyncExternalStore schedule an update mid-render
(React: cannot update a component while rendering a different component).
syncLanguage() updates the module silently; re-renders flow through the
settings store, which is the only path a renderer language change takes.
version 1.0.13, M10 milestone, release steps matching release.cjs
(incl. the sync-changelog pre-step), removed QuickInputPanel mentions,
real test mechanism instead of vitest, full test list
- 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
upload the payload first and latest.yml last, prune the previous version
only afterwards, reuse an existing release, add --channel-only; sync-
changelog uses a function replacement so $-sequences in notes survive