All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
russh0.61.2 → 0.62.7 (Dependabot alerts #8–#15): fixes four issues, all medium severity — a pre-auth remote panic via an all-zero Curve25519 peer public value (encode_mpintOOB), a post-auth remote panic via apty-reqcarrying more than 130 terminal-mode records, a pre-auth panic from a wrong-length X25519 key inclone_from_slice, and a channel-scoped server callback reachable without an open channel.nexterm-sshstill builds against theringcrypto backend (default-features = false,features = ["ring", "rsa", "flate2"]);aws-lc-rsis not pulled in byrussh(it is a pre-existing, unrelated dependency of the update checker'srustls/reqweststack), so the NASM-free Windows build is unaffected.server_channel_open_forwarded_tcpipgained areply: ChannelOpenHandleparameter that the handler must now call.accept()on to admit the channel —nexterm-ssh's remote-forwarding handler was updated accordingly.
- New built-in High Contrast colour scheme (
colors = "highcontrast"), the first scheme to target WCAG AAA: a pure black ground, pure white text, and ANSI entries lifted until every one clears 7:1 against the chrome surfaces. Measured, every text role lands between 7.00:1 and 21:1 on all four surface levels. Selectable from the settings panel's theme gallery andnexterm-ctl themelike any other scheme (UI/UX v3 P5c). SchemePalette::contrast(ContrastTarget::Aa|Aaa) lets a palette state the contrast ratio its text tokens are corrected toward. AAA cannot be a global floor —text_mutedis the foreground at alpha 0.48, which tops out near 4.55:1 over the surface ramp whatever foreground a palette states — so a scheme opts in and pays for it with a raised alpha instead.window.backdrop(auto|mica|mica-alt|acrylic|none) selects the OS-native window backdrop material. Windows maps it toDWMWA_SYSTEMBACKDROP_TYPE(Windows 11 build 22621+), macOS toNSVisualEffectViewvibrancy viawindow-vibrancy, and Linux to nothing —in_app_blur_enabledis the in-app substitute there. Secondary OS windows now receive the backdrop too; before this they received none.
- Accent-coloured and error-coloured text in the settings panel (search-match
row labels, list entries, invalid values, destructive button labels, the "Open
config.toml" link) took the raw
accent_primary/semantic_errorfill tokens, which are not corrected for legibility — on the Dark scheme accent text measured 1.41–2.26:1. They now take the corrected text variants (UI/UX v3 P5d). - Setting a colour scheme from
nexterm.lua(cfg.colors = "...") recognised only five of the nine built-in names;catppuccin,dracula,nordandonedarkwere silently downgraded todark. The Lua path now shares the same name table as TOML, so a scheme is reachable from both the moment it exists.
- The three top-of-screen banners — update notice, offline, server error — are
now one InfoBar stack. They were three state fields, three vertex
builders and three copies of the stacking arithmetic, with the error banner
re-deriving its own offset by testing the other two; they are now three kinds
of one surface, laid out by the single
bar_rectsfunction. Three visible changes come with it: the stack draws below the tab bar instead of over it, at most two bars are drawn at once (a third is queued but not drawn until P6c gives it a count suffix), andEnteropens the release page only while the update bar is the top one — an error bar above it now takes priority.Escstill clears the error bar before the update bar, and the offline bar still clears itself on connect and offers no[Esc]hint (UI/UX v3 P6b). - The chrome surface ramp now picks its direction with the same predicate the
text correction uses (WCAG relative luminance against the readability
watershed) instead of BT.709 on undecoded sRGB channels, and is capped so no
surface crosses that watershed. The two predicates disagreed for a band of
backgrounds — a saturated
#FF3100was "dark" to one and "light" to the other — and where they disagreed the ramp climbed away from the text, leaving chrome text that reused one colour across the ramp as low as 2.39:1 on custom palettes. No built-in scheme is in either affected band, so none of them changes; a background near the watershed now gets a compressed ramp, which is the accepted cost (UI/UX v3 P5e). - The settings panel's
ensure_readablecontrast helper is retired in favour ofcontrast_correct, exposed ascolor_util::readable_on. The old helper raised alpha and nothing else, so a colour that clashed with its ground in hue or luminance came back unchanged and still failing; the replacement keeps that alpha stage and adds value and saturation behind it. Almost every text run now takes an already-corrected colour fromDesignTokens::text_on(level)instead, leaving the helper for grounds that are not a surface token — a danger button's blended fill, for instance (UI/UX v3 P5d). - Requesting a backdrop now creates the window transparent on its own. Previously
only
background_opacity < 1.0did, so a backdrop on an opaque-configured window could never be visible. - Adding
window-vibrancy(macOS-only) re-resolved several existing crates'windows-sysdependency onto the version already present elsewhere in the lock file (0.59.0/0.48.0→0.61.2fordirs-sys,errno,is-terminal,nu-ansi-term,rustix,tempfile, and others). No new package version was introduced and nothing on Linux behaves differently — this is Cargo consolidating onto a version already in the tree — but it is why unrelated crates move in theCargo.lockdiff for this change. - GPU compositing contract (UI/UX v3 P0): every built-in shader now outputs
premultiplied alpha and every pipeline blends with
PREMULTIPLIED_ALPHA_BLENDING, matching the surface'sCompositeAlphaMode::PreMultiplied. Breaking for[gpu] custom_bg_shader/custom_text_shader: custom shaders must premultiply their fragment output (rgb * a). (#35) - Overlay drop shadows are real soft shadows (UI/UX v3 P2a): the background
shader's vertex layout gains
shadow_softnessandstroke_widthattributes (5 → 7), and every overlay panel derives its shadow from the Fluent elevation scale — dialogs now visibly float above flyouts and tooltips instead of every surface sharing one hard offset quad. Additive for[gpu] custom_bg_shader: existing 5-attribute custom shaders keep working; the new attributes are available at@location(5)/@location(6). - Settings focus rings are drawn as outlines rather than as stacked filled
rectangles (UI/UX v3 P2a follow-up): the ring's two bands now use the
shader's
stroke_widthattribute, so a focused control no longer repaints the area inside the ring twice. Geometry is unchanged; the boundary between the accent and surface bands gains a shared half-pixel of anti-aliasing. russh0.61.1 → 0.61.2, together with the crypto stack it pins (curve25519-dalek,ed25519-dalek, thep256/p384/p521family). Upstream fixes in this range are support for SEC1 EC keys that carry full domain parameters, andSendbounds on dynamicAgentClientreferences.
- The Windows backdrop was documented, named and logged as "Acrylic" while
actually applying Mica Alt:
apply_acrylic_blurpassed4toDWMWA_SYSTEMBACKDROP_TYPE, which isDWMSBT_TABBEDWINDOW. The default (backdrop = "auto") deliberately keeps applying Mica Alt, so no existing Windows window changes appearance — only the naming was wrong. docs/CONFIGURATION.mddocumentedbackground_opacity's default as1.0; the schema default is0.95.- Translucent windows (
window.background_opacity < 1.0) no longer look washed out: the clear color and all blending honor the premultiplied surface. (#35) window.background_image.opacityhad no effect — the image shader ignored the per-vertex tint that carries it. It is now applied.- Shader hot reload rebuilt the background pipeline with the pre-v1.11 2-attribute vertex layout; reloading with the built-in (or any 5-attribute) shader would fail pipeline validation. The reload path now mirrors the startup layout.
window.macos_window_background_blur, which had no reader anywhere in the workspace while bothCONFIGURATION.mdandARCHITECTURE.mddescribed it as a working setting. Config files still carrying the key keep loading — unknown keys are ignored — andwindow.backdropreplaces it.
h20.4.13 → 0.4.18 (RUSTSEC-2026-0258): a peer could send unbounded empty HTTP/2 DATA frames and keep a connection doing work indefinitely.h2backs the web terminal's server stack (axum→hyper) and the update checker's client stack (reqwest→hyper-rustls).memmap20.9.10 → 0.9.11 (RUSTSEC-2026-0186, unsound): an unchecked pointer offset in a transitive dependency of the font stack (cosmic-text→fontdb) and of the Wayland client-side decorations (winit→sctk-adwaita).
window.decorationsnow defaults to"notitle"— the Windows Terminal-style custom title bar (tab bar with window buttons, drag-to-move, double-click maximize, edge resize) is the out-of-box look on Windows and Linux. Setdecorations = "full"inconfig.tomlto restore the native title bar. macOS keeps"full"as its default because winit cannot start an interactive resize there, which would leave a borderless window unresizable. Configs that set the key explicitly are unaffected.- Tab pills and the
+/▾/ Settings buttons default to a 10 px corner radius (was 6 px, which read as barely rounded at 1x DPI); override via[ui] corner_radius_chromeas before. - Every overlay panel (dialogs, pickers, the settings panel) now draws its rounded corners with the SDF primitive used by the tab pills; the previous CPU approximation left square notches at the corners.
- Windows 11 snap layouts: hovering the custom title bar's maximize
button (
decorations = "notitle") now shows the snap-layout flyout. The window procedure is subclassed to answerWM_NCHITTESTwithHTMAXBUTTONover the button, Windows Terminal-style; hover and click keep working through the reconstructed non-client mouse messages. - Custom title bar on Linux/macOS: right-clicking the tab bar with
decorations = "notitle"opens an in-app system menu (maximize or restore / minimize / close — localized in all 8 languages) where winit's nativeshow_window_menuis Windows-only. "Close" honourswindow.close_action.drag_window/drag_resize_windowfailures (some Wayland compositors) are now logged instead of silently ignored. window.decorations = "notitle"now actually works, as a Windows Terminal-style custom title bar: the window is borderless and the tab bar doubles as the title bar — minimize / maximize (restore) / close buttons at the right edge (hover fill; close uses the error colour), double-click on blank chrome toggles maximize, the window outline resizes with directional cursors, and right-clicking the tab bar opens the native system menu (Windows). Previously"notitle"parsed but behaved exactly like"full". Secondary OS windows keep native decorations. The three buttons are exposed to screen readers as AccessKitButtonnodes.
- Terminal text no longer bleeds through the settings panel, command palette, host manager, and every other floating overlay. The main render pass previously drew all background quads and then all glyphs in one go, so grid glyphs painted over overlay backgrounds; the frame is now drawn in two layers (grid, then overlays), each background-before-text.
- Quake mode no longer resurrects the native title bar for
decorations = "none"/"notitle"users: the saved normal-mode state hard-codeddecorations: trueinstead of capturing the actual state. - The IME candidate window now accounts for the tab bar height and the window padding; it used to float one tab-bar height above the cursor.
- The generated config template suggested
decorations = "default", which is not a valid value (now"full", with the choices listed).
Windows-Terminal-inspired new-tab profile dropdown, settings search (row
highlight + collapse), per-category reset, and a keybinding conflict
warning. Requires the standalone nexterm-server to be upgraded together
with clients (protocol bump below); the single-binary nexterm upgrades
both halves atomically.
- Settings panel: the Keybindings category now warns when the selected binding's key chord is also assigned to another binding (non-blocking, Windows Terminal-style). Record-mode captures get immediate feedback.
- Settings panel: while a search query is active, rows whose visible label matches the query are highlighted in the accent colour — and because the match runs against the rendered (localized) label, a Japanese UI can be searched in Japanese.
- Settings panel: in the Window / Security / Blocks categories, a search query now also collapses non-matching rows (Windows Terminal 1.25-style settings search) — matching rows compact to the top and the hit regions follow. A query that matches nothing keeps every row visible.
- Settings panel: a "Reset defaults" link in the footer restores the current category's fields to their default values (applied on Save; Cancel still reverts). Hidden for the list-based SSH / Keybindings / Profiles categories, where a reset would delete user data.
- Settings panel: an "Open config.toml" link in the footer bar opens the configuration file with the OS default editor (Windows Terminal's "Open JSON file" equivalent). Creates the file first when it does not exist yet.
- Windows-Terminal-like new-tab profile dropdown: a
▾button next to the tab-bar+opens a menu listing the configured[[profiles]]and the WSL distros detected at startup; selecting one opens a new tab running that profile's shell, working directory, and environment (newClientToServer::SplitWithShellIPC message). - Context-menu profile entries now actually launch the profile's shell (previously they opened a plain split and only logged the profile name).
- Profiles that set only
working_dir/env(no[profiles.shell]) now open a new tab with the session default shell in that directory.
- PROTOCOL_VERSION 10 → 11 for
SplitWithShell. The single-binarynextermupgrades both halves at once; standalonenexterm-serverdeployments must be updated together with clients.
Windows console-flash fix and a Windows-Terminal-inspired settings-panel overhaul, plus wider live config hot-reload. Fully compatible with 1.13.0 peers (no protocol or snapshot version change).
- Settings panel: 16 previously config-only fields are now editable from the UI (cursor blink, scrollback lines, startup shell, tab-bar toggles, animations, block editing, active profile, window decorations, close action, GPU fps limit, follow-system colors, font ligatures, font fallbacks, leader key).
- Settings panel is now fully localized across all 8 locales (121 new
settings-*keys) with a key-set parity test; non-en/ja strings are a first pass pending native review. - Settings panel gained scrolling (wheel / PageUp-Down) so long categories are no longer capped at 12 rows.
- Live config hot-reload now also covers
language,window.decorations,gpu.present_mode, andscrollback_lines(existing panes resized) — no restart required. Fonts and per-frame settings were already live.
- Settings panel redesigned into a Windows-Terminal-style two-column layout (category sidebar + label/control rows) with dynamic, resize-following widths.
- The panel stays fully opaque while open and its decorations are audited to WCAG 4.5:1 contrast, fixing the previous half-transparent, hard-to-read look.
- Large settings modules were split for maintainability (state and rendering each into focused sub-modules, all under the 800-line guideline).
- Windows: eliminated the brief black console window that could flash from
internal subprocess spawns (cwd probe, URL openers, WSL probe, shell hooks)
by passing
CREATE_NO_WINDOW. - Settings panel text no longer overflows the panel (CJK-aware truncation + a GPU scissor clip around the scroll viewport).
- Fixed a flaky test where two modules shared a process-global env var behind separate mutexes.
Audit round 3 (docs/plans/audit-round3-2026h2.md): reliability, performance,
and encapsulation improvements found by a security / performance / correctness
review of 1.12.0. No protocol or snapshot version change — fully compatible with
1.12.0 peers.
- The PTY reader now applies only the changed rows to its
latest_gridsnapshot each output burst instead of cloning the whole grid. A single-row update drops from ~19 µs to ~0.5 µs in a criterion bench (nexterm-vt/benches/grid_snapshot), cutting memory-bandwidth pressure under heavy output. Screen::take_dirty_rowspre-sizes its result to avoid repeated reallocation on full-screen repaints.
- Recover from a poisoned
std::sync::Mutexinstead of cascading into a process-wide panic (plugin manager, web session / OAuth / TOTP-setup locks). - Resync a client with a full refresh when its broadcast receiver lags, instead of leaving the screen corrupt until an unrelated refresh (both the native IPC and WebSocket forwarders).
- Serial panes no longer render blank: they maintain a live grid snapshot and are included in the attach and lag-resync refresh paths.
- Guard the glyph-atlas LRU capacity math against
u32overflow for a large configuredgpu.atlas_size.
SessionManagernow uses per-session locking (HashMap<String, Arc<Mutex<Session>>>), so an operation on one session no longer blocks every other session behind a single global lock.- Encapsulated the raw bits of
AttrsandModifiersbehindnew/bits/insert/remove(ADR-0009). No wire change.
- Session-token validation is now constant-time (defense-in-depth for the web terminal auth gate).
Phases 1–4 of the competitive-gap roadmap (docs/plans/gap-roadmap-2026h2.md):
debt payoff, low-cost / high-compatibility protocol work, named workspaces,
and the Plugin API v2 → v3 expansion (plugin host end-to-end tests + the
consent-gated read API).
PLUGIN_API_VERSION 2 → 3 (adds the read_pane / read_grid /
read_scrollback host imports; MIN_READ_API_VERSION = 3). Non-breaking:
v1/v2 plugins load and run unchanged, and the read imports stay off until
the operator sets security.plugin_read = "allow".
SNAPSHOT_VERSION 4 → 5 (ServerSnapshot.known_workspaces added so
workspaces without sessions survive restarts). v4 and earlier snapshot files
auto-migrate; the workspace set is reconstructed from the sessions exactly
as before.
PROTOCOL_VERSION 9 → 10 (ServerToClient::PointerShapeChanged +
ServerToClient::PaneColorsChanged + ClientToServer::SetThemeColors).
Single-binary nexterm ships both halves so the upgrade is automatic;
anyone mixing standalone nexterm-server and a separate client across
versions must upgrade both. SNAPSHOT_VERSION = 4 unchanged.
- OSC 4 / 10 / 11 dynamic colors (query & set): the VT layer now answers
color queries (
ESC ] 11 ; ? BELetc.) through the same PTY write-back path as DA/DSR, fixing background auto-detection in vim/neovim. Sets are tracked per pane (palette overrides + dynamic fg/bg) and OSC 104/110/111 reset them. Replies mirror the request terminator (BEL vs ST). - Dynamic colors end-to-end (roadmap #10b): OSC 4/10/11 sets now reach
the renderer — the server broadcasts full override snapshots
(
PaneColorsChanged) and the GPU client applies them ahead of the scheme palette during vertex construction (tools like pywal now restyle live). The client also reports its committed theme defaults (SetThemeColors) so OSC 10/11 queries answer with the colors that are actually rendered (vim/neovim light-dark detection now sees the real theme). - OSC 22 mouse pointer shape: applications can now restyle the mouse
cursor (
ESC ] 22 ; pointer BEL, CSS cursor keywords; empty name resets). The GPU client applies the shape while hovering the grid area; unknown names fall back to the platform default. WezTerm / kitty parity. - OSC 9;4 progress reporting (ConEmu / Windows Terminal parity): shells
and tools reporting progress (
ESC ] 9 ; 4 ; state ; percent) now show a thin per-tab progress bar — green for normal, red for error, amber for paused, full-width dim for indeterminate. State 0 clears the indicator. Ships asServerToClient::ProgressChanged(PROTOCOL_VERSION 10). - OSC 99 desktop notifications (kitty protocol): the practical subset —
i=multi-part accumulation,d=completion,p=title/body,e=1base64 — feeding the same consent-gated notification path as OSC 9/777. In-flight identifiers are bounded (memory-DoS guard). - Named workspaces in the command palette (roadmap Phase 3): the
palette now offers "Workspace: switch to " entries for every
non-active workspace and a "Workspace: create new" entry
(auto-named
workspace-N, switched to immediately). The workspace set is fetched on attach and refreshed on every change, participates in fuzzy ranking and usage history, and is localized in all 8 languages. Empty workspaces now survive restarts (SNAPSHOT_VERSION 5). - kitty drag-and-drop protocol (OSC 72, kitty 0.47 parity): applications
can opt in (
t=a) to receive file drops as proper DnD events instead of path pastes. Supported subset: support query (t=q, echoed reply), opt-in/out, drop delivery (t=Mwithtext/uri-list), chunked base64 data responses (t=r, 4096-byte chunks), error replies (t=R;ENOENT), and completion. Applications that never opt in keep the existing quoted-path paste behavior. Motion negotiation (t=m) and remote-machine drops are not yet implemented. Ships asClientToServer::DndDrop(PROTOCOL_VERSION 10). [scrolling]config section + touchpad momentum:multiplier(rows per wheel notch, default 3.0 — previously hard-coded) andmomentum(kitty-style inertial coasting after the fingers lift, default off because Windows precision touchpads and macOS already synthesize OS-level inertia; mainly benefits Linux/X11). Momentum only ever applies to pixel-precision touchpad events — discrete wheels are unaffected.- Web Terminal auth integration tests (audit G4): 12 axum-level tests drive real HTTP requests through the full router — TOTP login/logout, session-cookie lifecycle, replay rejection (CRITICAL #6 guard), login rate limiting (CRITICAL #2 guard), setup endpoints, disabled-OAuth behavior, and security-header presence. Closes the last HIGH item from audit round 2.
- Lua sandbox fuzz target (audit G6): new
lua_sandboxcargo-fuzz target feeds arbitrary chunks throughsandboxed_lua()andapply_lua_table_to_config()with memory and instruction limits; wired into the nightly fuzz workflow. - Plugin host end-to-end integration tests (roadmap F2): 18 new tests
exercise the WASM plugin host against real modules for the first time —
the previous suite only covered invalid/empty modules and the no-plugin
path. WAT fixtures are compiled to real WASM at test time via the new
watdev-dependency (no wasm32 toolchain required on CI). Coverage: the fullload → on_output/on_command → reload → unloadlifecycle, v2 input sanitization actually reaching the plugin, thewrite_panepane-id allow list (v2 deny / v1 bypass), suppress return values, v1 raw delivery, fuel-exhaustion trapping (host survives an infinite-loop plugin),nexterm_metareadout, and theListPlugins/LoadPlugin/UnloadPlugin/ReloadPluginIPC dispatch handlers driven through a realSessionManager. - Plugin read API —
PLUGIN_API_VERSION2 → 3 (roadmap F3 / ADR-0008): v3 plugins can read terminal contents through three new host imports —read_pane(visible text),read_grid(structured cell dump), andread_scrollback(history text). v3 is a superset of v2, so existing v2 plugins run unchanged. Reads are gated by a newsecurity.plugin_readconsent policy that defaults todeny(fail-safe;promptis treated asdenyfor now since a server-side plugin call has no synchronous prompt path) and bysecurity.plugin_read_max_bytes(default 1 MiB, cap per read). Access is scoped per hook to the pane the plugin is currently handling; out-of-scope panes, wrong ABI, undersized buffers, and disabled policy return distinct negative error codes. To backread_scrollback,nexterm-vtgained a scrollback emit path (take_scrolled_off_lines) and each pane keeps a bounded (10k-line) mirror of scrolled-off lines. New sampleexamples/plugins/screen-digest/demonstratesread_pane. Full design and the grid dump wire format are indocs/adr/0008-plugin-read-api.md; the ABI reference is indocs/plugin-api.md. - Settings panel "Security" category: the
[security]config section is now editable in theCtrl+,panel instead of only by hand-editingconfig.toml. It exposes the four consent policies (external URL, OSC 52 clipboard, OSC 9/777 notification, plugin read) as allow/deny/prompt cyclers and the three byte-size caps as decimal inputs, with keyboard, mouse, and AccessKit support.save_to_tomlwrites the seven keys back viatoml_edit. The panel edits the persistent defaults; the runtime per-session consent overrides are unchanged and independent.
- rustls-pemfile → rustls-pki-types (audit G5): TLS certificate / key
parsing now uses the maintained PemObject API; the RUSTSEC-2025-0134
deny.tomlignore is gone and the archived crate is out of the dependency graph.towerwas aligned to 0.5 (the version axum links). - Advisory triage:
anyhowandquick-xmlwere updated to clear RUSTSEC-2026-0190; the upstream-blocked RUSTSEC-2026-0192 (ttf-parser via cosmic-text) and RUSTSEC-2026-0194/0195 (quick-xml via notify-rust) are documented as reasoned ignores indeny.toml.
- Example plugin
nexterm_on_outputargument order: the four bundled sample plugins declared the hook as(ptr, len, pane_id), but the host calls it as(pane_id, ptr, len); they now match. Each example also gained an empty[workspace]table so it builds standalone forwasm32-unknown-unknown. (The examples are separate wasm crates and are not built in CI, so the mismatch had gone unnoticed.)
cmov0.5.3 → 0.5.4 (CVE-2026-50185, GHSA-3rjw-m598-pq24, medium): a transitive dependency of the SSH stack (russh→rsa/elliptic-curve→ctutils) that backs constant-time comparisons could return wrong results on aarch64 when the input registers' high bits were set. Bumped to the patched release.
MINOR release that lands the UI/UX Modernization v2 plan in full —
Phases 1–6 plus every follow-up (2c, 3b, 4b, 5b, 6b). The plan
document at docs/plans/ui-ux-modernization-v2.md now reads all
shipped.
PROTOCOL_VERSION 8 → 9 (Phase 2c adds ServerToClient::ProcessChanged).
SNAPSHOT_VERSION = 4 remains unchanged. Single-binary nexterm
ships both halves so the upgrade is automatic for released-build
users, but anyone mixing standalone nexterm-server and a
separate client across versions must upgrade both.
Closes the last deferred item on the Phase 2 tab-bar work
(docs/plans/ui-ux-modernization-v2.md). When
tab_bar.show_process_icon = true, each tab label is prefixed with a
Nerd Font glyph derived from the pane's foreground process (e.g.
vim, ssh, git). The server polls each pane's foreground
process at 1 Hz and broadcasts updates only when the name changes;
the client maps the name to a glyph via a pure in-tree lookup table.
The full design and decision rationale is documented in
docs/plans/phase-2c-process-icon.md.
PROTOCOL_VERSION 8 → 9. Adding a new ServerToClient enum
variant is a wire-format break because postcard tags variants
positionally; the single-binary nexterm ships both halves so the
synchronised upgrade is automatic for users running the released
build, but anyone mixing client and server binaries across versions
will need to upgrade both. SNAPSHOT_VERSION = 4 unchanged.
- Server-side process inspection
(
nexterm-server/src/pane.rs): newPane::foreground_process_namewith three OS-specific implementations.- Linux: read
/proc/{shell_pid}/stat, extracttpgid, then/proc/{tpgid}/comm. ReturnsNonewhen the shell owns the terminal (tpgid == pgrp) so the icon clears at the prompt. - macOS: spawn
ps -A -o pid=,ppid=,comm=once per pane, build a(pid, ppid, comm)table, walk down from the shell to the deepest descendant. - Windows:
CreateToolhelp32Snapshot+Process32{First,Next}Wto enumerate every process, then the same parent-map descent. Strips trailing.exeso the glyph map matchesvimrather thanvim.exe. Uses the existingHandleGuardpattern fromread_has_foreground_processto guarantee handle cleanup.
- Linux: read
- Session-wide polling ticker
(
nexterm-server/src/lib.rs,nexterm-server/src/session.rs):run_serverspawns onetokio::time::interval(1s)task that callsSessionManager::poll_foreground_processes. The method snapshots(pane_id, name, broadcast_tx)tuples under the session lock, drops the lock, diffs each name against an in-taskHashMap<pane_id, Option<String>>, and broadcastsServerToClient::ProcessChangedonly on change. Stale pane IDs are pruned each tick so the map stays bounded. Polling is skipped entirely whenruntime_cfg.tab_bar.show_process_icon = false, so the default config pays zero OS-inspection cost. - Runtime config plumbing
(
nexterm-server/src/runtime_config.rs):RuntimeConfiggains anArc<TabBarConfig>field so the polling task can hot-reload theshow_process_iconflag without a server restart. - IPC (
nexterm-proto/src/message.rs,nexterm-proto/src/lib.rs): newServerToClient::ProcessChanged { pane_id, process_name: Option<String> }. Decoupled fromTitleChangedbecause cadence and triggers are independent — bundling them would either resend unchanged titles every second or force process polling to wait for an OSC 0 escape.PROTOCOL_VERSIONbumped 8 → 9. - Client-side storage
(
nexterm-client-gpu/src/state/pane.rs,nexterm-client-gpu/src/state/server_message.rs,nexterm-client-tui/src/state.rs):PaneStategainsprocess_name: Option<String>;apply_server_messagehandlesProcessChangedby writing the field through. The TUI client ignores the message (no tab bar to decorate). - Glyph map
(
nexterm-client-gpu/src/tab_icons.rs, new module): pureglyph_for_process(name) -> Option<&'static str>mapping ≈ 30 common process names to Nerd Font codepoints across editors (vim / nvim / emacs / nano / code), network (ssh / mosh), source control (git / lazygit), runtimes (node / python / cargo / go / ruby), containers (docker / kubectl), system tools (htop / less / man), and shells (bash / zsh / fish / pwsh / cmd). Case-insensitive, whitespace-trimmed. Unknown processes returnNoneso the absence of a glyph keeps its signal value. - Config schema
(
nexterm-config/src/schema/window.rs): newTabBarConfig.show_process_icon: bool(defaultfalse). Defaulted off because Nerd Font is not yet a documented runtime dependency and unknown glyphs render as tofu on regular fonts. - Renderer
(
nexterm-client-gpu/src/renderer/ui_verts.rs): the tab-bar builder readsprocess_namealongsidetitleand prepends the glyph whenshow_process_icon = trueand the lookup returnsSome. The number prefix ([N]) still applies on top of the glyph for parity with Windows Terminal.
nexterm-proto(+1):ProcessChangedpostcard round-trip for bothSome(name)andNonecases.nexterm-client-gpu(+5): every canonical key resolves; case-insensitive match (Code→ same glyph ascode); whitespace trimmed; unknown processes returnNone; matched glyphs are single non-empty codepoints (layout guard).nexterm-config(+0): existing tests cover the new field viaTabBarConfig::default.nexterm-server(+0 unit tests, runtime polling exercised by manual integration testing — see PR description).
cargo test -p nexterm-client-gpu --bins is green (614 tests, +5
from master). cargo test -p nexterm-proto round-trips clean.
cargo clippy --workspace --all-targets -- -D warnings clean.
- Linux: open three panes, run
vim/ssh/ shell; icons appear within 1 s and clear on:q/logout. - Windows: same drill with
pwsh/code/ shell. - macOS: same drill with
vim/ssh/zsh. -
show_process_icon = false→ no icons; toggle via TOML hot-reload → icons appear without restart. - Kill a shell from outside (
taskkill/kill -9); no panic and the pane's icon clears within 1 s.
Closes the last deferred item on the Phase 3 OS-theme work
(docs/plans/ui-ux-modernization-v2.md). When the settings panel is
open and the mouse hovers a colour-scheme dot, the renderer swaps
the active scheme transiently so the user can audition each palette
before committing. Moving the cursor away reverts to the configured
scheme; clicking the dot commits via the existing ThemeColor
hit handler. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 remain
unchanged.
- State (
nexterm-client-gpu/src/settings_panel.rs): newSettingsPanel.theme_hover_preview: Option<usize>field (panel- local UX state, not persisted) plus the pure inverse helperindex_to_builtin_scheme(idx)that pairs with the existingscheme_name_to_index. The Phase 3b commit clears the preview alongside the existing close-resets inSettingsPanel::close. - Mouse hover (
nexterm-client-gpu/src/renderer/event_handler/ mouse.rs):on_cursor_movedruns the existinghit_test_settings_panelwhile the panel is open and updatestheme_hover_previewbased onSettingsPanelHit::ThemeColor(idx)(everything else clears it). Apanel-closed-mid-hoverguard resets the preview so a stale value cannot leak across re-opens. TheThemeColorclick handler now also clears the preview after committing the new index so the renderer drops to the configured scheme on the next frame without a one-frame flicker. - Renderer (
nexterm-client-gpu/src/renderer/event_handler/ window.rs):redraw_requestedbuilds the configured scheme viaConfig::effective_color_schemeexactly as before, then overrides it withColorScheme::Builtin(index_to_builtin_scheme(idx))whentheme_hover_previewisSome(idx)and the panel is open. TheDesignTokens::from_palettederivation runs per frame, so the swap propagates to every overlay surface without an extra invalidation. - Tests (
+5):index_to_builtin_schemeround-trips with the existingscheme_name_to_indexfor slots 0..=8; out-of-range inputs wrap modulo 9 instead of panicking; a fresh panel has no hover preview;close()clears the preview alongside the other reset state;scheme_indexandtheme_hover_previeware independent fields so the click-commit handler's "clear after commit" path is order-safe.cargo test -p nexterm-client-gpu --binsis green (609 tests, +5 from master).
Follow-up to Phase 6 (PR #17). Replaces the Phase 6 grey-alpha overlay
approximation with a real per-cell HSB multiplier transform applied
during vertex construction. No new shader pass / pipeline is needed —
the bg_pipeline already carries per-vertex colour, so the transform
runs on the CPU when each cell's RGB is resolved. PROTOCOL_VERSION = 8
and SNAPSHOT_VERSION = 4 remain unchanged.
nexterm-client-gpu/src/color_util.rs— new pure helpersrgb_to_hsv,hsv_to_rgb,apply_hsb_multiplier(WezTerm-style multiplier semantics: each HSB component is a factor, identity at1.0), andapply_hsb_animated_rgba(lerps each factor toward identity bytso the transition follows the existing spring-driven dim animation).renderer/grid_verts.rs—build_grid_verts_in_rectgains aninactive_hsb: Option<(f32, f32, f32, f32)>parameter ((hue, saturation, brightness, animation_t)). WhenSome, every cell's bg / fg colour is run throughapply_hsb_animated_rgbabefore being pushed into the vertex buffers, so a real hue shift actually moves the cells' hue (previously impossible with a flat alpha overlay). WhenNone, the pre-Phase-6b flat-brightness fallback (* 0.70) is retained byte-for-byte.renderer/render_frame.rs— computesinactive_hsb_for_paneper pane (only for unfocused panes whenconfig.inactive_pane_hsb.is_active() == true) and threads it intobuild_grid_verts_in_rect.PaneRenderCachegains aninactive_hsb_q: u32key field (quantisedanimation_t × 255) so the per-pane cache rebuilds each spring frame instead of replaying a stale dim colour.renderer/ui_verts.rs— whenhsb.is_active(), the legacy per-pane grey-alpha overlay rect is suppressed. Drawing it on top of the new per-cell transform would double-dim. The Phase 6 helperInactivePaneHsbConfig::overlay_rgbastays in the codebase for back-compat / debugging but is no longer rendered.- Tests (
+8): HSV round-trip preserves primary colours; identity multipliers(1, 1, 1)are a no-op; brightness < 1 darkens without touching alpha; saturation 0 collapses to grey; hue multiplier ≠ 1 actually shifts the hue (green× 2→ blue-dominant); animated endpoints match input att = 0andapply_hsb_multiplieratt = 1; the lerp midpoint matches the expected interpolated multiplier; alpha is preserved across every combination. Fullcargo test -p nexterm-client-gpu --binsis green (565 tests, +8 from Phase 6).
Known limitation: the transform runs on the CPU during vertex
construction, so very large panes with high churn pay an additional
~one HSV conversion per cell per cache miss. The cost is bounded by
the existing PaneRenderCache and is below the per-frame cost of
glyph rasterisation, but moving the transform into the bg shader
remains a future option if a real-time hue sweep ever becomes a
requirement.
Picks up after Phases 1–5 in the Sprint 5-15 plan. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are unchanged; entirely client-side.
[inactive_pane_hsb]config (nexterm-config/src/schema/color.rs,nexterm-client-gpu/src/renderer/ui_verts.rs): replaces the hard-coded black-alpha 0.06 dim overlay with a configurable brightness / saturation knob (defaultshue = 1.0,saturation = 0.6,brightness = 0.85matching the WezTerm-style spec from the plan). Spring animation fromAnimationManager::pane_dim_alphastill drives the focus-change transition — the new pureInactivePaneHsbConfig::overlay_rgba(t)helper consumes the spring value asanimation_tin[0, 1]so the existing fade-in/out is preserved.- Approximation honesty: a true HSB transform requires a
post-process shader pass (RGB → HSB → multiply → RGB) which would
mean introducing a render-to-texture step. The v1 implementation
approximates:
brightness→ overlay alpha,saturation→ overlay tint toward neutral grey,hue→ ignored. Documented on the struct so readers know what to expect. A real hue shift lands in Phase 6b. - Tests (
nexterm-config): 8 new tests cover the defaults, default overlay grey/alpha values at fullt, animation_t = 0 → invisible, brightness=1 disables dim, saturation=1 produces pure black (matches v1), clamping of out-of-range inputs,is_activetruthiness, and TOML round-trip.cargo test -p nexterm-configis 135 passed + 1 doctest.cargo test -p nexterm-client-gpu --binsis 557 passed.cargo clippy --all-targets -- -D warningsclean.
Follow-up to Phase 5 (PR #16). Wires the existing
CursorConfig.smooth_motion flag (which Phase 5 added but did not
consume) into the renderer so the cursor interpolates between cells
over 80 ms with an ease-out cubic curve instead of snapping.
PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 remain unchanged.
cursor_motion.rs(new module) — pure helpers (ease_out_cubic,animation_t,interpolate,update_target,visible_position,quantize_visible) and theCursorMotionStatestruct. The duration is centralised asCURSOR_MOTION_DURATION_MS = 80. When the server reports a new cursor cell mid-animation, the current visible position is snapshotted as the newprevso the motion stays continuous (no jump back to the old cell).renderer/mod.rs,renderer/render_frame.rs,renderer/grid_verts.rs—WgpuStategains a per-panecursor_motion: HashMap<u32, CursorMotionState>map and asample_cursor_motionhelper.render_frameadvances the state each frame and passes the interpolated visible position into bothbuild_grid_vertsandbuild_grid_verts_in_rect; the cursor rectangle is now drawn at the interpolated cell coordinate. Whensmooth_motion = falsethe helper short-circuits to the integer cell so the rendered output is byte-identical to the pre-Phase-5b build.- Cache invalidation —
PaneRenderCachegains acursor_visual_q: (u32, u32)key field (sub-cell position quantised to 1/16) so the per-pane vertex cache rebuilds each animation frame instead of re-rendering a stale cursor. The quantisation gives 16 steps per cell, enough to keep all ~5 animation frames at 60 Hz distinct. - Tests (
+9): ease-out cubic endpoint identities;animation_tclamps to[0, 1]and short-circuits on zero duration; interpolation hits endpoints exactly; a fresh state reports its integer cell with zero progress; unchanged-target preserves the state; retargeting mid-animation snapshots the visible position; animation completes at the target after the duration elapses; quantisation is stable at integer positions and distinguishes sub-cell steps. Fullcargo test -p nexterm-client-gpu --binsis green (574 tests, +9 from Phase 5).
Picks up after Phases 1–4 in the Sprint 5-15 plan. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are unchanged; the entire feature lives in the client renderer + config.
- Linear-gradient background (
nexterm-config/src/schema/window.rs,nexterm-client-gpu/src/vertex_util.rs,nexterm-client-gpu/src/renderer/render_frame.rs): new[window.gradient]config (from,to,angle) renders a two-stop CSS-convention linear gradient across the entire window using per-corner vertex colours on the existingbg_pipeline(no new shader). 0° = bottom → top, 90° = left → right, 180° = top → bottom. Mutually exclusive withbackground_image— when both are set, the image wins and the gradient drawcall is skipped. Purecompute_gradient_thelper covers the angle math (8 unit tests over the four cardinal angles, a diagonal, angle wrapping, and NaN guard). - Cursor blink (
nexterm-config/src/schema/window.rs,nexterm-client-gpu/src/vertex_util.rs,nexterm-client-gpu/src/renderer/{grid_verts,render_frame,wgpu_init,mod}.rs): new[cursor]config withblink_enabled(defaulttrue),blink_interval_ms(default 530 — the xterm cadence), andsmooth_motion(defaulttrue; the smooth-motion render path ships in Phase 5b). Visibility is computed once per frame via the pureCursorConfig::is_visible_at, threaded intobuild_grid_verts*, and consumed by the newdraw_cursor_with_visibility(the originaldraw_cursoris kept as a thin always-visible wrapper for callers that don't care about blink). The blink state is part of the per-panePaneRenderCachekey, so toggling visibility correctly invalidates the cached vertex buffers. A safe-floor of 50 ms (20 Hz) guards against hostile / typoblink_interval_msvalues producing photosensitive-seizure-level flicker. 7 new tests cover the defaults, the visibility schedule, the safe-floor, and TOML round-trip. - Smooth cursor motion is deferred to Phase 5b. The config flag exists today (so user TOML doesn't break when 5b lands), but the renderer ignores it for now — the cursor still snaps. Implementing it correctly needs per-pane animation tracking; doing that in the same PR would mix unrelated state into the cache-key path.
Follow-up to Phase 4 (docs/plans/ui-ux-modernization-v2.md). Extends
the Phase 4 category-level fuzzy filter so queries can hit individual
fields (e.g. "opacity", "bash", "padding") instead of only matching the
short curated keyword list. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION =
4 remain unchanged.
- Field catalogue (
nexterm-client-gpu/src/settings_panel.rs): replaces the Phase 4category_keywordstable with a richercategory_fieldsregistry that listsFieldEntry { label, aliases }per category.filter_categoriesnow scores each category asmax(label_score, best_field_score)so a query like "opacity" ranks Window first via itsOpacityfield, and "bash" ranks Profiles first via theCommandfield aliases. - Sidebar hit-count badge
(
nexterm-client-gpu/src/renderer/overlay/settings.rs): when a query is active, each remaining category renders asICON Label (N)whereNis the number of fields matching the query in that category. The badge is suppressed when the query is empty so the sidebar looks identical to Phase 4 in the no-search case. - Tests (
nexterm-client-gpu/src/settings_panel.rs): 7 new tests (every category declares ≥1 field, field-label match reaches Window via "opacity", alias match reaches Profiles via "bash", hit count is zero for empty/blank queries, hit count is positive on the matching category and zero on an unrelated one, hit count aggregates multiple fields for "padding",SettingsPanel::field_hit_countmatches the free helper). Fullcargo test -p nexterm-client-gpu --binsis green (579 tests, +7 from Phase 4).
Picks up the Sprint 5-15 plan (docs/plans/ui-ux-modernization-v2.md)
after Phases 1–3 shipped in commit fd7758f. PROTOCOL_VERSION = 8 and
SNAPSHOT_VERSION = 4 are unchanged; this PR reuses the existing
ClientToServer::ResizeSplit IPC instead of introducing a new message.
- Category search in the settings panel
(
nexterm-client-gpu/src/settings_panel.rs,nexterm-client-gpu/src/renderer/overlay/settings.rs,nexterm-client-gpu/src/renderer/event_handler/{keyboard,settings_panel_hit,mouse}.rs,nexterm-client-gpu/src/renderer/input_handler/mod.rs): the sidebar now hosts a fuzzy-search input at the top./activates it from anywhere in the panel; characters land insearch_queryinstead of panel hotkeys; Backspace shrinks the query; the first Esc clears the query and a second Esc closes the panel. Filtering is driven by the purefilter_categorieshelper (usesSkimMatcherV2against the category label + a curated keyword list per category — searching "color" finds Theme, "shell" finds Profiles). Field-level filtering inside a category is deferred to a follow-up PR (the panel renderer is a single 2.1 kLoC method and refactoring it cleanly is out of scope for Phase 4). - Mouse-driven pane resize
(
nexterm-client-gpu/src/state/mod.rs,nexterm-client-gpu/src/renderer/event_handler/mouse.rs): the cursor now switches toEwResize/NsResizewhen hovering an internal split border, and clicking + dragging the border streamsResizeSplit { delta }messages to the server. The purehit_test_pane_borderhelper walkspane_layoutsand reports the adjacent pane + axis with a 4-pixel tolerance band on either side of the border; the click handler focuses the adjacent pane so the server'sadjust_ratio_for(window/bsp.rs) targets the right split ancestor. Per-frame pixel deltas are normalised to a ratio delta against the parent split's span and clamped to±0.5to match the server-sideclamp 0.1..0.9. No new IPC message —ResizeSplitalready existed since Sprint 5-1. - Tests (
nexterm-client-gpu/src/state/mod.rs,nexterm-client-gpu/src/settings_panel.rs): 7 new tests forfilter_categories(empty query → canonical order, exact-label match, keyword synonym, no-match → empty, struct/helper parity, focus helpers preserve the query, control chars are skipped) and 7 new tests forhit_test_pane_border(side-by-side / stacked detection, tolerance band on both sides, grid-origin offset honoured, partial row-range overlap, empty layouts, zero cell metrics). Fullcargo test -p nexterm-client-gpu --binsis green (572 tests).
docs/plans/ui-ux-modernization-v2.md Phases 4, 4b, 5, 5b, and 6
now read shipped. Phase 6b (true HSB hue shift) remains
pending. The core plan (Phases 1–6) is now complete.
PATCH release whose sole purpose is to ship the Flatpak build that v1.10.0 missed. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 remain unchanged; there are no client-visible behaviour differences from v1.10.0.
- Flatpak vendor manifest (
pkg/flatpak/cargo-sources.json): the v1.10.0 release bumpedquinn-proto0.11.14 → 0.11.15 to patch RUSTSEC-2026-0185 but did not refresh the Flatpak vendor list, so the Flatpak job failed itsCargo.lockconsistency check and no.flatpakartifact was attached to v1.10.0. This release regenerates the manifest from the currentCargo.lock; the only diff is thequinn-proto-0.11.15archive entry replacing the 0.11.14 one. All other releases (Linux tar/AppImage, macOS arm64/x86_64, Windows msi/zip, SBOM) are identical to v1.10.0.
MINOR release. The headline feature is the Warp-style command-blocks
UI that lands across Phases 2c-A through 2c-G plus two follow-ups
(right-click context menu and an interactive Blocks settings page).
PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are unchanged — the
feature reuses the existing OSC 133 SemanticMark IPC stream from
Sprint 5-2 / B1 and persists named blocks to a separate file that did
not exist before.
Folds the existing OSC 133 SemanticMark stream (Sprint 5-2 / B1) into
addressable command blocks and surfaces them in the UI. The feature is
gated by the new [blocks] section in config.toml; with OSC 133
disabled or unconfigured the renderer no-ops as before.
CommandBlockabstraction (nexterm-client-gpu/src/command_blocks.rs): pureextract_command_blocksfolder +find_block_by_id/next_block_id/prev_block_idnavigation +BlockStatus/compute_block_overlay_linesfor the renderer. Includes a defensivesanitize_replay_commandthat rejects ESC / BEL / CSI / embedded newlines so a hostile SSH peer cannot weaponise the replay path.- Named-block persistence (
nexterm-client-gpu/src/named_blocks.rs):NamedBlockStoreat~/.local/state/nexterm/named_blocks.jsonwith atomic write + mode 0600 (palette_history.json pattern). Capped at 10 000 entries with LRU eviction. - ClientState helpers (
nexterm-client-gpu/src/state/blocks.rs):select_next/prev_block,selected_command_block,selected_block_text(clipboard payload),selected_block_replay_command(sanitised PTY input),set/remove_selected_block_name, andBlockNameModalfor the rename UI. - Keybindings:
Ctrl+Shift+ArrowUp/ArrowDownnow updates the block selection alongside the existing prompt jump.Ctrl+Shift+Cis block-aware: when a block is selected it copies the block instead of the whole grid (falls back to grid otherwise).Ctrl+Shift+Rreplays the selected block's command line viaPasteTextafter the sanitiser approves it.Ctrl+Shift+Lopens the block-name input modal (Enter saves, Esc cancels, empty input removes the name).
- Renderer overlay (
nexterm-client-gpu/src/renderer/grid_verts.rs): left N-px border + selection tint coloured by exit code (success = green, failure = red, running = grey). Initial cut renders only while the pane is in scrollback mode; the in-grid path and the status badge glyph land together with on-device verification. - Block-name modal frame
(
nexterm-client-gpu/src/renderer/overlay/dialog.rs): centred panel with accent stripe, input echo, and a help line. Strings come fromnexterm-i18nkeysblock-modal-title/block-modal-help(translated to all 8 locales). - Configuration: new
[blocks]section (enabled/border_width_px/show_exit_code_badge);border_width_pxis clamped to1..=8at draw time. - i18n:
block-modal-titleandblock-modal-helpadded to en / ja / zh-CN / ko / de / fr / es / it. - Docs:
docs/plans/blocks-implementation.mdrecords the phased plan anddocs/shell-integration.mddocuments the bash / zsh / fish prompt snippets required to emit OSC 133.
- In-grid block overlay (
renderer/grid_verts.rs): the left border + selection tint now also draws while the pane shows the live grid (scroll_offset == 0). The viewport top maps topane.scrollback.len()so a running block's border extends to the bottom of the screen as new output streams in. - Status badge (
✓/✗/●): drawn in the right margin at the prompt row, coloured to match the border. Controlled by the newBlocksConfig.show_exit_code_badge(on by default). - Multi-pane layout path: new
build_block_overlay_verts_in_rectmirror so the overlay also renders in split-pane layouts. Drawn outside the per-pane vertex cache so selection updates do not force a full grid rebuild.
Ctrl+Shift+/togglescollapsedon the selected finished block. The scrollback renderer skips rows inside collapsed blocks (the prompt row + the first output row stay visible), compacting the visual gap so the next block sits directly underneath.Ctrl+Shift+Xremoves the user-assigned name from the selected block (no-op when no name is stored).- New pure helper
is_row_collapsed(blocks, abs_row)plustoggle_selected_block_collapseonClientState. Running blocks are refused — their tail is still being written.
- Clicking inside the configured left-border width selects the block
under the cursor (hit zone widens to at least 6 px so the affordance
is reachable even with
border_width_px = 1). - Clicking the rightmost cell on a block's prompt row toggles
collapsed(chevron path; only active whenshow_exit_code_badge = true). - Pure helpers
block_containing_rowandresolve_clicked_scrollback_rowinvert the scrollback render walk so click coordinates map to the absolute row the user sees on that line.
- Opening the palette with
Ctrl+Shift+Pand typing@switches into named-block search. The remainder of the query is fuzzy-matched against the names assigned viaCtrl+Shift+L. - Selecting a result scrolls the focused pane so the matching block's
prompt row sits at the top of the viewport, sets
selected_block, and yields control back to the existingCtrl+Shift+R/Ctrl+Shift+Cchords for replay / copy. - The palette refreshes its named-block list on open, so newly named blocks appear immediately without restarting the palette.
- New "Blocks" entry in the settings-panel sidebar shows the three
[blocks]values and indicates theconfig.tomlkeys that back them. - Fixed a latent NodeId collision:
settings_tab_id_at(idx)returnedSETTINGS_TAB_BASE + idx, which previously sat at18with only 7 slots reserved beforeSETTINGS_CONTENT_ID = 25. The 8th category collided. Moved the base to60with 40 slots of headroom for future categories;decode_node_idand the round-trip tests are updated accordingly.
- Right-clicking inside a known block prepends 5 block actions to the
existing context menu: Copy block (
Ctrl+Shift+C), Replay block (Ctrl+Shift+R), Toggle collapse (Ctrl+Shift+/), Name this block... (Ctrl+Shift+L), Remove block name (Ctrl+Shift+X, hidden when no name is stored). - Five new
ContextMenuActionvariants carry the targetBlockId. - New i18n keys translated across all 8 locales:
context-menu-block- copy/-replay/-toggle-collapse/-set-name/-remove-name.
- The Blocks category in the settings panel is now interactive: row 0
toggles
enabled, row 1 cyclesborder_width_pxthrough1..=8, row 2 togglesshow_exit_code_badge. Each click writes back toconfig.tomlimmediately viatoml_edit, preserving existing comments and unrelated fields. - Direct edits to
config.toml [blocks]still hot-reload, so the settings-panel and manual-edit paths remain equivalent.
command_blocksmodule: 46 unit tests (+14 from v1.9.6 coveringis_row_collapsed,block_containing_row, andresolve_clicked_scrollback_row).state::blocksmodule: 44 unit tests (+12 from v1.9.6 coveringtoggle_selected_block_collapse,select_block_by_id,toggle_block_collapse_by_id,collect_named_block_palette_entries, andjump_to_block).palettemodule: +5 tests for@-prefix routing.- Workspace total: 548 passing.
PATCH release bundling five small client-side UX fixes reported on 2026-06-09. No protocol or snapshot changes (PROTOCOL_VERSION = 8, SNAPSHOT_VERSION = 4).
-
Initial grid drift on Windows + winit 0.30 (
event_handler/lifecycle.rs): the firstwindow.inner_size()returned afterwith_visible(false)+with_inner_size(1280x800)sometimes lags the requested size, and because the eventual size matches the request noWindowEvent::Resizedfires. The terminal stayed pinned at the PTY default of 80×24 and the un-tiled portion rendered as grey. An idempotent post-connect resync now recomputes cols/rows from the liveinner_size()on the first frame after the IPC channel comes up and resizes the state + notifies the server when it differs. Pure helpercompute_grid_cellsextracted with 5 unit tests. -
Settings panel translucent backdrop (
overlay/settings.rs): when the main window usedbackground_opacity < 1.0or Windows 11 Acrylic, the area around the settings panel stayed translucent so the desktop bled through and the panel felt non-modal. A full-screensurface_0scrim is now drawn behind the panel chrome only while the panel is open. The terminal's own transparency is intentionally left alone.
-
Settings panel drag-to-move (
settings_panel.rs,overlay/settings.rs,event_handler/settings_panel_hit.rs,event_handler/mouse.rs): pressing the panel's title bar grabs it; cursor movement updates a cumulativedrag_offset; release ends the drag. Position is clamped via the pureclamp_panel_position(6 unit tests + 3 lifecycle tests) so the panel stays fully visible horizontally and keeps at least the title bar onscreen vertically.close()resets the offset so the next open returns to centered. Renderer and hit-test share the clamp function, so visual position and click regions never drift apart. -
Tab-bar drag-to-move-window (
event_handler/mouse.rs): clicking empty space in the tab bar (no tab, no Settings button, no[×], no[↗]) now starts the OS-driven window move loop viawinit::Window::drag_window(), matching native title-bar behaviour. The pane body remains excluded so text selection is unaffected.drag_window'sResultis swallowed; backends without support simply no-op. -
File-drop paste (
event_handler/window.rs,event_handler/mod.rs):WindowEvent::DroppedFileis now handled. Each dropped path is formatted via the pureformat_dropped_path(6 unit tests) — wrapped in double quotes when it contains whitespace or a literal", with embedded"escaped as\"; Windows backslashes are preserved verbatim for PowerShell. Paths are sent through the existingClientToServer::PasteTextchannel so bracketed paste applies automatically. Drops within 500 ms of the previous one prepend a single space, givingfile1 file2 file3for multi-file drops.
- 20 new unit tests across
compute_grid_cells,clamp_panel_position, settings-panel lifecycle, andformat_dropped_path.
PATCH release fixing the real root cause of the "PowerShell renders blank on Windows" symptom: the VT parser silently dropped every Device Attributes / Device Status Report query, and there was no path for the parser to reply at all. PowerShell + PSReadLine queries the terminal on startup and waits for the reply before drawing the prompt, so on Windows ConPTY pwsh would emit a short escape-sequence preamble and then hang forever. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are unchanged.
-
Reply to Device Attributes (DA) and Device Status Report (DSR) queries: the VT parser handlers for
CSI c/CSI > c/CSI 5 n/CSI 6 nwere missing — the old_ => {}arm incsi_dispatchswallowed them. The parser now queues a reply for each:CSI c→ESC [ ? 6 c(VT102, the conservative xterm-compatible Primary DA reply).CSI > c→ESC [ > 1 ; 276 ; 0 c(Secondary DA: VT220, firmware 276, no ROM).CSI 5 n→ESC [ 0 n(terminal operating normally).CSI 6 n→ESC [ row ; col Rwith 1-based cursor coordinates.
-
PTY write-back path: the
VtParserpreviously had no way to send anything back to the PTY —Pane.writerwas scoped exclusively to client key input. The reader thread now owns anArc<Mutex<Writer>>clone and, after everyparser.advance, drainsScreen::take_pending_responsesand writes the bytes verbatim. PSReadLine receives its DA / DSR replies and proceeds to draw the prompt instead of hanging.
-
Screen::take_pending_responses/push_pending_response: a small queue alongside the existingpending_title/pending_clipboard_writes/ etc. pattern. The performer pushes replies into it; the reader thread drains it. -
Hex preview of the first PTY output: the existing
first PTY output (N bytes)log now also prints the first 32 bytes in hex so a future "still blank" report can be diagnosed without guessing what control sequences the shell sent.
- 5 new unit tests in
nexterm-vtcovering each of the four queries plus atake_pending_responsesdrain test (124 total, up from 119).
PATCH release closing the remaining "blank screen on restored Windows session" race after v1.9.3, plus broad startup diagnostics so a future repro can be triaged from the log alone. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are unchanged.
-
Attach-to-broadcast subscription race:
handle_attachused to sendFullRefreshfirst and only subscribe to the session's broadcast at the very end of the handler. Becausetokio::sync::broadcast::Receiveronly sees messages sent after subscription, anyGridDiffemitted by the PTY reader in the window betweenmake_full_refreshand the subscribe call was dropped — and on Windows that is exactly when PowerShell tends to print its prompt, leaving the screen blank. The handler now subscribes the broadcast and spawns the forwarder before composing theFullRefresh, so prompts arriving during attach are buffered and forwarded to the client. -
Restored-session blank-grid fallback (nudge): when the focused pane's grid is still entirely blank at attach time — typical when the reader has not had time to process the shell's first prompt yet — the server now writes a single
\rto the PTY. The shell echoes the carriage return and re-emits its prompt, and the resultingGridDiffis delivered through the just-spawned forwarder. Costs a harmless empty line at restore time but eliminates the "frozen blank pane" state.
- Startup diagnostics: the server log now includes per-pane spawn details (
shell,args,pid,cwd,cols,rows), one-shot markers forPTY reader thread started/first PTY output/first GridDiff, and per-window restore-progress lines (window 'X': restoring N pane(s)…restore complete, N pane(s) live).handle_attachalso logs theFullRefreshpayload summary (non_blank_rows, cursor position) and whether the nudge fired. The PTYEOFlog was promoted fromdebug!toinfo!. These should make any future "PowerShell did not start" report diagnosable from a single client log file.
PATCH release fixing the long-standing "blank screen on Windows" symptom — restored sessions that always re-attach to a snapshot would show no shell prompt because the PTY reader's grid was never shared with the Pane. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are unchanged.
-
Restored sessions render blank on attach:
Pane::make_full_refreshreturned a fresh emptyGrid::new(cols, rows)instead of the live screen. TheVtParserthat owns the real grid lives inside the PTY reader thread, and the only message the reader emits isGridDiff. When the shell prompt arrives before any client has attached — the common case on Windows where themainsession is always restored — thatGridDiffis dropped (no broadcast receivers) and the late-attaching client gets an emptyFullRefresh; the idle shell then produces no new output and the screen stays blank, which the user perceives as "PowerShell did not launch".The reader now mirrors
parser.screen().full_refresh_grid()into anArc<Mutex<Grid>>on every output burst, andmake_full_refreshclones from it. The shell process was always alive (spawn_commandsucceeded inPane::spawn_impl) — only the display of its existing output was broken.This complements the v1.7.x work tracked in the Windows PowerShell startup investigation: causes A/B/D/E/F were addressed there, but the snapshot-restore path on Windows is dominated by this
make_full_refreshshape, not by connection-race or ConPTY spawn issues.
PATCH release fixing a cross-platform unit-test failure introduced in v1.9.1. The v1.9.1 binaries were functionally correct; only a nexterm-config unit test used a Windows-style path literal that does not split on Unix, so it failed on Linux/macOS CI. No runtime behavior changed.
- Cross-platform watcher test:
is_config_path_matches_toml_and_luaused a backslash Windows path literal that, on Unix, is treated as a single file name (no separator), so the assertion failed on Linux/macOS. The test now uses forward-slash paths that resolve on every platform, and the Windows backslash case moved to a#[cfg(windows)]-gated test.
PATCH release fixing a Windows-specific configuration reload storm and multi-window close handling. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are unchanged.
- Configuration reload storm on Windows: on Windows the config directory and the state directory both resolve to
%APPDATA%\nexterm, so the 30-second snapshot auto-save repeatedly fired the config watcher and caused five to six redundant config reloads every 30 seconds. The watcher now reacts only tonexterm.toml/nexterm.luachanges and ignores every other file in the directory (snapshot, history, and atomic-write temp files). Reloads whose content is identical to the previous one are additionally suppressed. Unix was unaffected because its config and state directories are already separate. - Repeated close requests spamming IPC: clicking a window's close button several times before the confirmation dialog responded sent a fresh
QueryForegroundProcesseach time. The handler now ignores repeated close requests while one is already pending for the same window. - Detached OS windows could not be closed independently: closing a detached (additional) OS window previously targeted the main window's state — it queried the wrong server window and drew the confirmation dialog on the main window, so the detached window looked unresponsive. Detached windows now close on their own through the per-window path (no prompt, no process exit); only closing the main window runs the prompt/exit flow.
- Reformatted
copy_mode.rsandrender_frame.rsto satisfycargo fmt --check(no behavior change). - Removed a redundant
.max(256)in thelru_cap_realistic_fonttest that a newer Clippy (unnecessary_min_or_max) flagged; the 256 floor is already covered bylru_cap_floor_at_256.
MINOR release adding a complete Vim-style copy mode (D2). Users can now navigate, visually select, search, and yank terminal output using standard Vi key bindings without leaving the keyboard. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are unchanged.
- Vi mode in copy mode (D2): copy mode now behaves like a full Vim Normal / Visual / Visual-Line modal editor.
- Navigation:
h/j/k/l(character/line),w/b(word forward/back),e(word end),^(first non-blank of line),G(last row),gg(first row),Ctrl-u/Ctrl-d(half-page up/down). - Visual selection:
venters character-wise Visual mode;Venters line-wise Visual-Line mode. Both toggle off on a second press.yyanks the selection to the clipboard and exits copy mode. - Search:
/opens a forward incremental search;?opens a backward search.n/Nrepeat the last search in the same/opposite direction. The search query is committed onEnter;Escapecancels without navigation. - Rendering: the selected region is drawn as a blue semi-transparent overlay (alpha 0.45) directly on top of pane content, bypassing the C4 vertex cache so it is always up-to-date. Visual-Line highlights full rows; Visual highlights character ranges. The cursor position is marked with a yellow block (alpha 0.60).
- Status bar indicator: the right zone of the status bar shows
COPY,VISUAL, orV-LINEinaccent_primarycolour while copy mode is active.
- Navigation:
- Pre-existing clippy lints from C4:
append_pane_vertsandPaneRenderCache::key_matcheswere flagged fortoo_many_arguments; both now carry#[allow(clippy::too_many_arguments)]. Themap_or(false, …)pattern in the cache-validity check was replaced withis_some_and(…). A redundantlayout.cols as u16cast was removed.
PATCH release completing the DesignTokens tokenization pass started in v1.8.0. All remaining hardcoded [f32; 4] RGBA literals in the GPU renderer overlay modules are now replaced with DesignTokens field references. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are unchanged.
- Key-hint overlay fully tokenized (Phase 12):
build_key_hint_vertsnow accepts a&DesignTokensparameter. The banner background is derived fromsurface_0at 0.92 alpha, the accent stripe fromaccent_muted, the header and key-column text fromtext_primary, and the action-column text fromtext_secondary. - Command palette and picker overlays tokenized (Phase 13): the selected-row highlight in the command palette now uses
surface_2, selected foreground usestext_primary, unselected items usetext_muted, and the empty-list hints in the macro picker and host manager usetext_muted. Intentional purple macro branding and green SSH-host branding are preserved. - Dialog overlays fully tokenized (Phase 14): the password-prompt input text uses
text_primary, the "remember password" checkbox enabled/disabled states usesemantic_success/text_muted, the "prefilled from keychain" label usessemantic_info, the context-menu separator usesborder_subtleat 0.70 alpha, and the key-hint text in context menus usestext_mutedat 0.80 alpha. Black semi-transparent backdrops, dark-text-on-bright-button, and Kill/Cancel semantic colours are preserved as intentional. - Tab bar settings button tokenized (Phase 15): the inactive-state colour of the
⚙button in the tab bar now usestext_secondaryinstead of a hardcoded[0.80, 0.80, 0.80, 1.0].
MINOR release introducing a comprehensive UI/UX overhaul based on a centralised DesignTokens system. All visual chrome — overlays, tabs, pane borders, status bar, banners, and dialogs — now derives colours and radii from a single source of truth, making the renderer theme-aware and eliminating ~500 hardcoded RGBA literals. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are retained; the wire format is unchanged from 1.7.8.
- DesignTokens system (
nexterm-client-gpu/src/design_tokens.rs, Phase 1): a centralised struct of colour and geometry constants (surface,overlay,accent,border,text_*,pane_border_*,tab_*,status_bar_*, corner radii, animation durations). All renderer code now importsDesignTokens::default()rather than scattering literal[f32; 4]arrays throughout shaders and vertex builders. Dark-mode and future light-mode themes can be swapped by returning a differentDesignTokensinstance. - Pill-style tab bar with hover-only close buttons (Phase 1–2): tabs are now drawn as rounded rectangles with a configurable pill radius. The close
×button is invisible at rest and fades in only on tab hover, reducing visual noise when many tabs are open. The active-tab accent line expands from the centre of the pill on focus change. - Pane focus visualisation (Phase 3): the focused pane receives a coloured border drawn with
DesignTokens.pane_border_active; inactive panes are subtly dimmed with a translucent overlay quad (DesignTokens.pane_dim_overlay). Both effects update instantly when focus moves. - Spring-physics animations (Phase 4): the tab accent line and the pane dim overlay now use an
ease_out_cubicspring instead of a hard cut. Duration is driven byDesignTokens.tab_accent_anim_msandDesignTokens.pane_dim_anim_mssoconfig.animations.intensity = "off"continues to produce instant transitions. draw_overlay_panelunified overlay chrome helper (Phase 5): a single function invertex_util.rsthat draws the rounded background rect, drop shadow quad, and border stroke shared by every overlay (command palette, host manager, macro picker, SFTP dialog, settings panel, close-window dialog, context menu). Removing per-overlay duplication cut ~300 lines of vertex-builder code.- Status bar zone redesign (Phase 6): the status bar is divided into left (connection / session info), centre (clock / workspace name), and right (battery / CPU sample, if enabled) zones using DesignTokens spacing. Zone boundaries are drawn with a subtle separator rather than a full-height divider.
- Update banner and incremental-search chrome tokenized (Phases 7–8): the amber update-available banner and the
/incremental-search overlay bar now use DesignTokens colours and radii instead of hardcoded values, so they inherit future theme changes automatically. - Quick Select overlay fully tokenized (Phase 9): the Quick Select label overlays that appear during
Ctrl+B Qcopy mode now use DesignTokens for background, text, and highlight colours. - SFTP file-transfer dialog tokenized (Phase 10): progress bar fill, track, and border colours in the SFTP upload/download dialog replaced with DesignTokens references.
- All remaining hardcoded
[f32; 4]RGBA literals innexterm-client-gpu/src/renderer/overlay/settings.rsreplaced with DesignTokens field references (final cleanup commit after Phases 1–10).
PATCH release that addresses the remaining P2 items from the Windows-launch investigation memo. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are retained; the wire format is unchanged from 1.7.7.
- Offline-mode banner (P2-1): while the GPU client is repeatedly failing to connect to the embedded server, a one-line amber bar at the top of the window shows "Connecting to the embedded server… ({seconds}s)" once the offline streak exceeds 1 s. Previously this state was a silent blank window — particularly visible on Windows where the
\\.\pipe\nexterm-<user>named pipe can take >1 s to come up. The banner auto-clears as soon as the connection succeeds (no key dismissal). New i18n keyoffline-banner-connectingis translated in all 8 locales.
- Restored panes silently disappearing when their saved cwd was deleted (P2-2):
Pane::spawn_with_cwdnow checks that the requested working directory still exists and is a directory before handing it toportable_pty::spawn_command. When the directory is missing (e.g. acargo cleanremoved atarget/subdir, or a scratch directory was deleted while the session was offline), the spawn now falls back to$HOME/%USERPROFILE%instead of surfacingHRESULT -2147024809 (E_INVALIDARG)on Windows ConPTY and letting the pane be dropped by the snapshot self-heal pass. AWARNlog line is emitted so the cwd loss is still visible in diagnostics.
PATCH release that addresses two of the three problems surfaced by the v1.7.5 diagnostic logs in nexterm-client.log.2026-06-05. PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are retained; the wire format is unchanged from 1.7.6. The standalone nexterm-server binary path is unchanged — only the single-binary GPU client uses the new entry point.
- Duplicate
Watching the configuration directory(problem 2): the GPU client and the embeddednexterm-servertask each installed their ownnotify::Watcherover the same TOML directory, keeping two file-system handles open for the entire process lifetime. The client now owns the onlySharedRuntimeConfigand the only watcher, and forwards each reload to the server's dispatch layer viaArcSwap::store. The embedded server skipsruntime_config::spawn_watcherwhen given an external runtime config. - ~1.6 s server starvation between
restored sessionsandipc::serve(problem 3): the embedded server previously ran as atokio::taskon the same runtime that drove winit, so winit's main-thread occupation could block server-side progress for seconds at a time on lower-core machines. The server now runs on a dedicated OS thread (std::thread::Builder::name("nexterm-server")) with its own multi-thread Tokio runtime, fully isolated from winit's scheduling.
nexterm_server::run_server_with_config_and_runtime(cfg, runtime_cfg, shutdown_rx): new public entry point for embedders that own theSharedRuntimeConfigand want explicit shutdown control instead of relying ontokio::task::JoinHandle::abort(). The existingrun_server()(standalone binary) andrun_server_with_config(cfg)(v1.7.6 single-binary entry) are preserved.nexterm_server::{SharedRuntimeConfig, RuntimeConfig, build_shared_runtime_config}: re-exports of the runtime-config types so external embedders can construct and update the shared handle without depending on internal modules.
- Single-binary
nextermclient: the embedded server task became a dedicated OS thread with its own Tokio runtime;server_handle.abort()was replaced by an explicittokio::sync::oneshotshutdown channel. On window close the client now sends()on that channel and joins the server thread so its snapshot save completes before the process exits.
PATCH release that eliminates the duplicate TOML read on startup of the single-binary GPU client. No behavior change beyond fewer file reads and cleaner logs; PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are retained, and the wire format is unchanged from 1.7.5.
- Duplicate
Loaded the TOML configurationlog (nexterm-client.log.2026-06-05): the GPU client and the embeddednexterm-servertask each calledConfigLoader::load()independently, so the same TOML file was parsed twice within microseconds of each other on every startup. The client now loads the config once and hands the parsedConfigto the embedded server via the newrun_server_with_configentry point. The standalonenexterm-serverbinary still usesrun_serverand continues to load the file itself, so the systemd / standalone path is unchanged.
nexterm_server::run_server_with_config(cfg): new public entry point for embedders that have already parsed the config. The existingrun_server()is preserved for the standalone binary and now simply loads the file and delegates to the shared inner routine.
Diagnostic-only PATCH release. No behavior change beyond logging; PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 are retained, and the wire format is unchanged from 1.7.4.
This release adds breadcrumb logging across the server startup sequence and the client reconnect loop so we can pinpoint the silent stall reported in nexterm-client.log.2026-06-03, where the server task vanished for ~38 s between restored sessions and the IPC accept loop without emitting any log line.
- Server startup checkpoints (
nexterm-server/src/lib.rs): six newINFOlog lines covering snapshot load, self-heal check, runtime-config build, WASM plugin load, config-watcher spawn, optional web-terminal launch, and the decisiveentering ipc::serveline right before the named-pipe / Unix-socket accept loop. The last line is what distinguishes "stalled beforeipc::serve" from "stalled inside the accept loop". - Named-pipe create failure context (
nexterm-server/src/ipc/platform.rs, Windows):ServerOptions::createfailures are now logged aterror!with the raw OS error code and the loop iteration number, so collisions on\\.\pipe\nexterm-<USERNAME>are no longer swallowed by?. - Client reconnect diagnostics (
nexterm-client-gpu/src/renderer/event_handler/lifecycle.rs):try_connectnow reports the first failure of an offline streak atINFOwith the underlying error (wasdebug, invisible in production), emits aWARNsummary every ~5 s while still offline (attempt count + elapsed seconds + last error), and logs the total offline duration atINFOonce the connection succeeds. The 200 ms retry cadence and overall behavior are unchanged.
P1-A of the Windows-launch investigation. The next on-device reproduction run will identify exactly which startup step is stalling, which is the prerequisite for designing P1-B (single-instance pipe coordination) against concrete evidence rather than speculation. See memory/project_windows_powershell_startup_investigation.md for the full failure analysis.
PATCH release that re-syncs the Flatpak vendored-sources manifest with
Cargo.lock. No code changes since 1.7.3 (PROTOCOL_VERSION = 8,
SNAPSHOT_VERSION = 4 retained); 1.7.3 shipped with every asset except the
Flatpak bundle, which this release restores.
- Flatpak build (
pkg/flatpak/cargo-sources.jsonout of sync): a previous dependency bump (chacha200.9.1 → 0.10.0,cipher0.4.4 → 0.5.1) updatedCargo.lockwithout regenerating the vendored-sources manifest, leaving stale crate entries that no longer matchedCargo.lock. The Flatpak CI's sync check failed at release time (so 1.7.3 has no Flatpak asset). The manifest is regenerated and back in sync.
Follow-up PATCH on top of 1.7.2. Replaces the blocking startup-connect retry
introduced in 1.7.2 with a non-blocking attempt plus background reconnect,
which addresses the root cause of the offline-mode race, and trims redundant
startup work and log noise observed in real session logs. No breaking changes
(PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 retained).
- Startup connect race, fixed at the root (
nexterm-client-gpu): the 1.7.2 fix retried the IPC connect for ~3 s by blocking the winit main thread (block_in_place+block_on). In the single-binary build the client and the embedded server share one Tokio runtime, so that block starved the server task and delayed the very named pipe the client was waiting for. Real logs showed the client exhausting its 3 s retry budget at T+3.4 s while the server only bound the pipe at T+4.3 s — falling into offline mode even though the server came up healthy ~0.9 s later.on_resumednow makes a single non-blocking connect attempt and, on failure,on_about_to_waitreconnects on a 200 ms cadence until the server is listening. The main thread stays responsive and the server gets the CPU it needs to bind the pipe promptly.
- Server loads its config once at startup (
nexterm-server):run_servercalledConfigLoader::load()twice (once for the shell, again for hooks / web / plugins), doubling startup file IO and emitting duplicate "Loaded the TOML configuration" log lines. It now loads once and reuses the result. - Font system is no longer scanned twice at startup (
nexterm-client-gpu): the font manager was built once at scale 1.0 inNextermApp::newand then fully rebuilt inon_resumedto apply the real DPI scale, triggering a second ~30–50 MB system-font scan (and a duplicate "malformed font" warning for fonts like Windows'mstmc.ttf). A newFontManager::set_scale_factorreuses the existing font system and only recomputes scale-dependent metrics. - Quieter shutdown / font logs: the server's "config watcher channel
closed" message is now
debug!(it fires on every clean shutdown, so awarn!was misleading), and the default client log filter addsfontdb=errorto silence unactionable malformed-font warnings from third-party system fonts.
Follow-up PATCH on top of 1.7.1, fixing a startup race that could leave
the client in offline mode and silencing a second per-frame WARN flood.
No breaking changes (PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4
retained).
- IPC connect race on startup (
nexterm-client-gpu): in the single-binary build the GPU client and the embedded server task race at launch. If the server's snapshot-load + IPC-listen took longer than the client's firstconnectattempt (observed: ~943 ms on a real session), the connect returnedos error 2(Windows: file not found — the named pipe did not exist yet) and the client fell into offline mode permanently, with no further reconnect attempts. The window stayed blank and unresponsive until the user force-closed it. The connect path now retries up to 15 times on a 200 ms cadence (≈3 s total budget). The retried path emits adebug!line per attempt and only escalates towarn!if all attempts fail.
- Default log filter targets
wgpu_hal::vulkan::conv=error(nexterm-client-gpu): newer NVIDIA Vulkan drivers advertiseVK_PRESENT_MODE_FIFO_LATEST_READY_EXT(id1000361000), which current wgpu does not recognize and which it emits as a WARN every single frame (≈30 Hz). The directive added in 1.7.1 only suppressed INFO fromwgpu_hal, so this WARN flood survived and now drowned out the cleaner log. The new targeted=errordirective silences only thewgpu_hal::vulkan::convmodule; otherwgpu_halWARNs continue to surface. SettingNEXTERM_LOGexplicitly continues to override the default exactly as before.
Diagnostic and resilience PATCH release. No breaking changes
(PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4 retained).
- Snapshot self-heal on startup (
nexterm-server): when one or more windows or sessions fail to restore from the persisted snapshot (e.g. ConPTY returnsE_INVALIDARGbecause the saved cwd no longer exists), the snapshot file is now rewritten immediately during startup so the broken entries are not retried on every subsequent launch. Previously a short-lived session that ended before the 30-second auto-save tick fired could leave a broken entry stuck in the snapshot indefinitely. - Detailed context on ConPTY failures (
nexterm-server):openptyandspawn_commanderrors now carrycols/rows/shell/args/cwdin the error chain, andSession::restore_from_snapshotprints the full chain via{:#}. A previously opaquefailed to create psuedo console: HRESULT -2147024809now reads e.g.openpty failed (cols=0, rows=0, shell="powershell.exe"); ConPTY on Windows rejects size 0 with E_INVALIDARG (HRESULT 0x80070057).
- Default logging directives (
nexterm-client-gpu): whenNEXTERM_LOGis unset, the GPU client now appliesinfo,wgpu_core=warn,wgpu_hal=warn,naga=warninstead of plaininfo. This silences the per-frameDevice::maintain: waiting for submission index NINFO thatwgpu_core::device::resourceemits at roughly 60 Hz, which previously bloatednexterm-client.logpast 1 MB for a 4-minute session and drowned out useful diagnostics. SettingNEXTERM_LOGexplicitly continues to override the default exactly as before.
Sprint 5-11-9 (Keybindings interactive editor + screen-reader support) and
Sprint 5-12 (Windows shell-launch visibility fixes) shipped together. No
breaking changes (PROTOCOL_VERSION = 8 and SNAPSHOT_VERSION = 4
retained).
- Sub-phase A — keybinding entry data + display: introduced
KeyBindingEntry { key, action }plus theKeybindingssettings category. The settings panel renders the loaded bindings (and any built-in defaults) as a scrollable list, with→separating the key spelling from the action name.KEYBINDING_ACTIONSenumerates the 27 supported actions (Quit/CommandPalette/CloseOsWindow/ …) — anything outside the list is flagged as invalid. - Sub-phase B — edit logic:
KeyEditMode::Record(capture the next key press) andKeyEditMode::Text(TextInputState)(free-form spelling) drive in-place editing.begin_key_record/begin_key_text_edit/capture_key_record/commit_key_edit/cancel_key_editform the state machine.cycle_keybinding_action_forward/backwardcycles throughKEYBINDING_ACTIONS. - Sub-phase C — Add / Delete buttons:
add_key_bindingappends a fresh entry and immediately enters Record mode.open_key_delete_dialog/cancel_key_delete_dialog/confirm_key_delete_dialog/toggle_key_delete_dialog_focusdrive the delete confirmation dialog (Cancel is focused by default to prevent accidental deletion). - Sub-phase D — settings-panel UI: 5-row Keybindings section in
renderer/overlay/settings.rsshowing the binding list, the selected key field (with Record indicator), the action ComboBox, and Add / Delete buttons. Navigation:↑/↓cycleskey_field_focus(0=List, 1=Key, 2=Action, 3=Add, 4=Delete);←/→cycles the action ComboBox or moves between dialog buttons;Enteractivates the current focus;Esccancels in-flight edits and closes the delete dialog. - Sub-phase E — AccessKit nodes + dispatch + tests: screen-reader
exposure of the entire Keybindings editor.
- NodeId allocation: fixed
50..=56for Key field / Action field / Add / Delete buttons / delete-dialog (AlertDialog+ Confirm + Cancel); dynamic offset900_000_000forSettingsKeyBindingItem(oneListBoxOptionper binding). build_settings_panel_nodesKeybindings branch: surfaces each binding as aListBoxOption, the selected binding's key as aTextInput(description swaps to a "Recording…" hint while in Record mode and exposes the live edit buffer while in Text mode), and the action as aComboBox. Add / Delete are exposed asButtons; Delete becomes a labelled "(disabled)" button when the list is empty so SR navigation stays consistent.dispatch_settings_actiongains 12 arms. Per the design decision "Q1 = (c) both",Action::Clickon the key field starts Record mode andAction::SetValuewrites the spelling directly viaset_keybinding_key_direct. The actionComboBoxacceptsClick/Increment/Decrementcycling plusSetValue(rejected for strings outsideKEYBINDING_ACTIONS).compute_tree_state_hashnow reflects the keybinding list, the selected index,key_field_focus, the delete-dialog state, and theKeyEditMode(including the in-flightTextInputState.buffer/cursor/preedit) so the SR sees every change live.- 25 new unit tests: 7 decode tests (fixed + dynamic offsets), 11
dispatch tests (each Action × node combination, including Q1=(c)
Click→Record + SetValue→direct write regression), 4 build-tree tests
(focus selection, empty-list behaviour, delete-dialog body, focus
follows
key_field_focus), 1 hash detection test, 2 sanity tests (offset isolation, Click+SetValue lands clean).
- NodeId allocation: fixed
- Server error banner UI (Sprint 5-12 Phase 1) —
ServerToClient::Errorused to be log-only. It is now stored inClientState.error_banner: Option<String>and rendered at the top of the screen as a red banner (deep-red background with a bright-red accent). Failures like PTY spawn errors (PowerShell not found, etc.), pane-split failures, and config-load errors are now visible directly on screen, so users no longer needNEXTERM_LOG=debugto diagnose them. Dismissed withEsc(handled beforeupdate_banner). Stacks vertically alongsideupdate_bannerwithout conflict. - i18n key
error-banner-prefix— added the "Error:" prefix string to all 8 languages (en/ja/zh-CN/ko/de/fr/es/it). - Startup warning queue (Sprint 5-12 Phase 4) — added
startup_warnings: Arc<Mutex<Vec<String>>>plusset_startup_warnings/take_startup_warningsmethods toSessionManager. Whenrun_serverfailsConfigLoader::load(), instead of silently using defaults, it queues warning messages and emits them asServerToClient::Error(semicolon-joined) on the first attach. Surfaces through the Phase 1 error-banner machinery. - Lua
shell.argsmerge support (Sprint 5-12 Phase 3) — extendedapply_lua_table_to_configsoshell.argscan be overridden from a Lua table. Example:shell = { program = "pwsh.exe", args = {"-NoLogo", "-NonInteractive"} }. Omittingshell.argspreserves the existing value (from TOML orShellConfig::default()); an explicitly empty table is also preserved, so users cannot accidentally wipe all args.
- Windows version-comparison bug that hid PowerShell 10
(Sprint 5-12 Phase 2) — when
ShellConfig::default()scanned%ProgramFiles%\PowerShell\*on Windows, it comparedPathBufvalues with>, which falls back to lexicographic order. That made"7" > "10", so even if PowerShell 10 was installed, version 7 was chosen. Introduced a newpwsh_version_number()helper that parses the directory name asu32and compares numerically. Preview directories (7-preview, etc.) fall back to 0 on parse failure, ensuring numeric versions always win. - Lua config
shell.argswas being silently dropped (Sprint 5-12 Phase 3) — the previousapply_lua_table_to_configoverrode onlyshell.programand discardedshell.args. Users who wroteshell = { program = "...", args = {...} }in Lua kept the default args (["-NoLogo"]for PowerShell), contrary to expectation. - Config-load failures were being swallowed (Sprint 5-12 Phase 4) —
nexterm_config::ConfigLoader::load().unwrap_or_default()discardedErrcompletely, so even a corruptednexterm.tomlstarted the server on defaults without telling the user. Replaced with amatch: errors are now surfaced in three places —tracing::error!, the startup-warning queue, and the client error banner.
nexterm-client-gpu/src/state/server_message.rs::ServerToClient::Errorhandler changed from "log only" to "log + setstate.error_banner".nexterm-client-gpu/src/renderer/render_frame.rsnow draws the error banner immediately after theupdate_bannerblock. The two banners stack vertically.nexterm-client-gpu/src/renderer/input_handler/mod.rs::on_keynow handlesEscwhileerror_banner.is_some()before theupdate_bannerearly-return, so the most recent (error) banner is closed first when both overlap.
- New
#[cfg(all(test, windows))] mod testsinnexterm-config/src/schema/shell.rswith 4 regression tests for the version comparison bug (v7 < v10 / preview is 0 / no parent is 0 / numeric beats non-numeric). - 3 tests in
nexterm-config/src/loader.rsfor Luashell.argsmerging (override succeeds / preserved on omit / preserved on empty table).
- No functional breaking changes (
PROTOCOL_VERSION8 /SNAPSHOT_VERSION4 retained). SessionManager::newsignature unchanged (the 12 existing call sites are unaffected).- No config-schema changes (the
[shell]section is fully compatible). - No changes to the
ServerToClientIPC type (we only added a receiver-side UI for the existingErrorvariant; the server-side contract is unchanged). - Existing server binaries inter-operate with the new client (older servers
don't populate
startup_warnings, but the client-side error banner still surfaces PTY spawn failures and similar).
# 1. Enable debug logging at startup
$env:NEXTERM_LOG = "debug"
nexterm 2> $env:USERPROFILE\nexterm-debug.log
# 2. Confirm PowerShell 10 is detected in the log
Select-String -Path $env:USERPROFILE\nexterm-debug.log -Pattern "pwsh|powershell"
# 3. Verify a PTY spawn failure shows the red banner at the top of the screenHotfix release for Flatpak distribution. As of v1.6.0, the new dependencies
added in Sprint 5-11-1 (AccessKit PoC) — accesskit / accesskit_winit and
friends — had not been propagated to pkg/flatpak/cargo-sources.json. The
Flatpak workflow failed its Cargo.lock consistency check almost immediately
(17 s). v1.6.1 regenerates cargo-sources.json (+234 lines) and ships the
full asset set including Flatpak.
- Flatpak build failure resolved: re-aligned
pkg/flatpak/cargo-sources.jsonwithCargo.lockand added the AccessKit family of dependencies (accesskit-0.24.0,accesskit_atspi_common-0.18.1,accesskit_consumer-0.36.0,accesskit_ios-0.1.0,accesskit_macos-0.26.1, and many more) to the vendored sources. The CI consistency check (diff against the generator output) passes again and the Flatpak bundle ships.
- Bumped version
1.6.0→1.6.1(workspace.package.version).
- No functional breaking changes (
PROTOCOL_VERSION8 /SNAPSHOT_VERSION4 retained). - macOS / Linux / Windows binary distributions are identical to v1.6.0. Only Flatpak catches up.
- Functionally identical to v1.6.0. Users who already obtained the macOS / Linux / Windows binaries for v1.6.0 do not need to upgrade. Pick v1.6.1 only if you install via Flatpak.
Completes every phase of Sprint 5-11 (5-11-1 through 5-11-8). Delivers the last
remaining HIGH item from audit round 2 — screen-reader support (H1) — fully
implemented with AccessKit 0.24 + accesskit_winit 0.33, alongside terminal grid
diff notifications, cursor TextSelection, Bell / OSC notifications as
Role::Alert, ActionRequest write-response, and SSH host GUI editing, all
landing together.
- Expose the accessibility tree via AccessKit — integrated
accesskit_winit::Adapterwith the primary OS window and each OS window spawned by Phase 4. Publishes the tree to screen readers via Windows UI Automation, macOS NSAccessibility, and Linux AT-SPI. - Dynamic tree generation —
accessibility::build_tree_from_state(&ClientState)rebuilds theTreeUpdatefromClientStateevery frame (reflecting tabs, panes, titles, and cwd). Honorstab_orderand finalises the NodeId scheme:- Fixed NodeId 1–15 (Root / TabBar / PaneArea / 6 overlays / 4 dialog buttons)
- Dynamic offsets: Palette item 100M / Host item 200M / Macro item 300M / Context item 400M
- Tab
1_000_000_000 + pane_id/ Pane10_000_000_000 + pane_id(collision-free; covered by unit tests)
- 6 overlays converted to nodes — CommandPalette (Dialog + SearchInput + ListBox) / ContextMenu (Menu + MenuItem) / CloseWindowDialog (AlertDialog + Kill / Cancel buttons) / HostManager / MacroPicker / SettingsPanel (minimal; field expansion lands in Phase 5-11-6). Priority-based modal control (CloseDialog > ContextMenu > Palette > others).
- Live tree updates (100 ms throttle) —
compute_tree_state_hash(&ClientState)hashes the content andon_about_to_waitcompares against the previous hash. When it changes,Adapter::update_if_activeis called under a 100 ms throttle to push the diff to the screen reader. - ActionRequested handling (read-side operations) — implements the path
where a screen reader's Focus / Click / SetValue request actually makes
Nexterm move.
decode_node_id(NodeId) -> NodeIdKindperforms the reverse lookup;handle_accesskit_actiondispatches:- Tab / Pane Focus or Click →
FocusPaneIPC + update ofstate.focused_pane_id - CloseDialog Kill / Cancel button → reuses the existing
selected_buttonhalf-open contract (0xFE = Kill confirm / 0xFF = Cancel confirm / 0,1 = focus only) - ContextMenu / Palette item Click → reuses existing handlers
(
execute_action/execute_context_menu_action) - PaletteSearch SetValue → updates query string and resets
selected = 0
- Tab / Pane Focus or Click →
- Screen-reader support for the terminal proper — each grid row is
published as an
accesskit::Role::TextRunnode. NodeId scheme extended: PaneRow =20G + pane_id*1000 + row(per-pane partitioning, collision-free). - Row-text API:
pane_row_text(grid, row)strips SGR escapes,trim_end_matches(' ')removes trailing whitespace, and fully blank rows become" "so the screen reader preserves a row boundary. - Only the focused pane runs at
Live::Polite— other panes and the scrollback are kept atLive::Offto avoid over-announcement. - Per-row hash diffing —
compute_grid_row_hasheshashes each row withDefaultHasher;update_accesskit_tree_if_neededis widened to detect "tree-shape change OR grid change" independently.
- Row nodes become
Role::TextRunwithset_character_lengths— the UTF-8 byte-length array lets CJK cells carry correct boundaries. - TextSelection integration — the cursor row of the focused pane is
represented as a caret via
TextPosition { node, character_index = cursor_col }(anchor == focus). Live::Politenarrowed — from every visible row to only the cursor row.- Scrollback exposed —
SCROLLBACK_WINDOW_RADIUS = 100rows aroundscroll_offsetslide through a window (Live::Off). NodeIds are continuous viapane_scrollback_row_node_id: viewport (0–999) / scrollback (1000–9999). - New pure functions:
pane_row_text_with_lengths/scrollback_row_text_with_lengths/cursor_character_index.
ClientState.alerts: VecDeque<AlertEntry>— TTL 5 s, max 16 entries, managed byadd_alert/expire_alerts.Live::Assertiveregion container — eachRole::Alertsits underALERT_REGION_ID = NodeId(26)(separated from the pane-row range viaNODE_ID_ALERT_OFFSET = 50T).- Bell (
\x07) and OSC 9 / OSC 777 notifications — two kinds,AlertKind::BellandAlertKind::Notification, that the screen reader can announce immediately. Added to the SR region first regardless of consent settings (to prevent false suppression).
- Four core write-responses:
- HostItem Click →
connect_ssh_host_new_tab(SSH connect in a new tab) - MacroItem Click →
RunMacroIPC (run macro immediately) - Alert Click →
dismiss_alert(seq)(immediate dismissal, bypassing TTL) - PaneArea ScrollUp / ScrollDown →
scroll_up/down_focused_pane
- HostItem Click →
- SettingsPanel Window category — 4 fields completed:
cursor_style/padding_x/padding_y/present_modewrite back to[window]/[gpu]/ top-level viatoml_edit. NodeId 36–39 assigned toSettingsCursorStyle/PaddingX/PaddingY/PresentMode(SR actions on SpinButton / ComboBox / Slider are all wired up). - Window-category UI expansion — 5-row inline edit (↑/↓ selects a field, ←/→ changes the value, with a highlight rect + a mini-slider).
- PTY input buffer (NodeId 27, fixed) — a single
Role::TextInputnode exposed underPANE_AREA_ID.Action::SetValueroutes throughClientToServer::PasteTextIPC into the PTY. This is a workaround because AccessKit 0.24 does not standardiseRole::TerminalSetValue; line feeds (\n) are transferred as-is. - Profiles category —
SettingsPanel.profilesexposed asRole::ListBoxOption. Click or Focus updatesselected_profile. NodeId offset600_000_000. - Better descriptions on the Ssh / Keybindings categories — replaced the
"not implemented" placeholder with guidance to edit TOML directly
(
[[hosts]]/[[keys]]).
- Step 8-1 (read-only ListBox) — Settings panel → SSH category exposes
[[hosts]]as a ListBox.SshHostEntry.label()produces a single readable row label. - Step 8-2 (Field SR editing) — name / host / port / username / auth_type
exposed as
Role::TextInput/Role::SpinButton/Role::ComboBox. They accept direct edits from the screen reader viaset_value. - Step 8-3 Sub-phase A (Inline GUI editing) —
TextInputStatestruct (cursor + UTF-8 boundary aware + preedit support). Pressing Enter on name / host / username enters edit mode. Key bindings: character input / Backspace / ←→ / Home / End / Delete. - Step 8-3 Sub-phase B (IME preedit routing) — routes CJK IME composition
text correctly into SSH fields.
set_ime_cursor_areaalso tracks the IME window position. - Step 8-3 Sub-phase C (SpinButton / ComboBox visual editing):
- port:
←/→step by 1 (clamped to 1–65535) - auth_type:
←/→cyclepassword/key/agent
- port:
- Step 8-3 Sub-phase D (Add / Delete + delete confirmation dialog):
- NodeId 45 = SettingsSshAddBtn / 46 = SettingsSshDeleteBtn
- NodeId 47 = SettingsSshDeleteDialog (
Role::AlertDialog, modal) - NodeId 48 = SettingsSshDeleteConfirmBtn / 49 = SettingsSshDeleteCancelBtn (Cancel is the default focus to prevent accidental deletion)
- The post-deletion selection clamps to n (tail → n-1 / middle → same index / empty → focus = 0)
ssh_field_focusextended to 0..=7. While the dialog is open, Enter / Esc / ←→ / Tab are handled first (input_handler patch).
- Step 8-3 Sub-phase E — added 25 unit tests (6 for TextInputState
UTF-8 boundaries + 3 for
ssh_field_editlifecycle + 7 for Add/Delete/dialog + 6 for dispatch + 1 fortree_state_hashdetection + Sub-phase C smoke).
- Promoted README.md's
Screen reader supportfrom "experimental" to a full implementation notice.
- No breaking changes.
PROTOCOL_VERSION8 /SNAPSHOT_VERSION4 retained. Existing config files and snapshots keep working. - New dependencies:
accesskit = "0.24"/accesskit_winit = "0.33"(workspace dependencies). - Added a
UserEvent::Accessibility(accesskit_winit::Event)variant. Becauseaccesskit_winit::Eventis notClone, removed#[derive(Clone)]fromUserEventas a whole (a grep confirmed no callers needed cloning). - AccessKit 0.24 does not provide
Action::Default, so every write-side response is implemented throughAction::Clickonly.
- Real-device SR verification — verification with NVDA / VoiceOver / Orca on actual hardware is still pending. The Action paths cannot be validated with local unit tests alone.
- The TUI client is out of scope —
nexterm-client-tuihas no GUI editing or screen-reader support; edit[[hosts]]inconfig.tomldirectly as before.
cargo test --workspace: all 880+ tests pass (339 in thenextermbin plus the rest of the workspace)cargo clippy --workspace --all-targets -- -D warnings: greencargo fmt --check: clean
Completes Sprint 5-10 Phase 4-7. A PATCH release that lands the
Windows has_foreground_process implementation that v1.5.0 had to leave
as a "Known Issue". With this in place, the close_action = "prompt"
confirmation dialog now behaves symmetrically across all three OSes
(Linux / macOS / Windows).
- Windows
has_foreground_processimplementation — enumerates processes viaCreateToolhelp32Snapshot+Process32FirstW/NextWand decides a foreground process is running if any process has the shell PID (Pane.pid) as its parent. As a result, Windows now also fires the confirmation dialog withclose_action = "prompt"whenever ssh, vim, or any long-running job is active.HandleGuardauto-callsCloseHandleto prevent handle leaks- All 4
unsafeblocks carry SAFETY comments - The detection logic is logically equivalent to the macOS implementation
(built on
ps -A), so the false-positive pattern (a shell with background jobs still returnstrue) is the same — and safe.
- No breaking changes.
PROTOCOL_VERSION8 /SNAPSHOT_VERSION4 retained. - New dependency:
windows-sys = "0.59"added to the Windows target ofnexterm-server. Already used at the same version innexterm-client-gpu, so Cargo.lock gets no new package andpkg/flatpak/cargo-sources.jsonis unchanged.
cargo test --workspace: 689 pass + 5 ignoredcargo clippy --workspace --all-targets -- -D warnings: greencargo fmt --check: clean
The stable release of Sprint 5-8 / 5-9 Phase 4 — "tab tearing" (drag-out tab). Contains the v1.5.0-beta.1 content plus the three items deferred to Phase 4-6.
PROTOCOL_VERSION7 → 8: with the addition ofClientToServer::MovePaneToWindow, older clients (up to v1.4.0) and the new server are incompatible at the Hello handshake. Client and server must be upgraded together.SNAPSHOT_VERSION3 → 4: addedServerSnapshot.client_os_windows: Vec<OsWindowSnapshot>. v3 → v4 is auto-migrated through#[serde(default)], so existing users need to do nothing.- See the
v1.4.0 → v1.5.0 section in
docs/MIGRATION.mdfor details.
- Drag-and-drop a tab to another OS window to split it off into a new
window (X11 / macOS / Windows)
- Design holds multiple winit native windows in a single process
(
EventLoopProxy+UserEvent) Window::insert_pane_at/Window::into_single_pane/Session::move_paneimplemented- If the source window becomes empty it is removed automatically
- Design holds multiple winit native windows in a single process
(
- Merge into another OS window — drop the dragged tab onto another window's tab bar to merge them
ClientToServer::MovePaneToWindow { pane_id, target_window_id, insert_at }IPC addedwindow.close_actionsetting added — three values (prompt/detach/kill) control what happens on OS-window close
QueryForegroundProcessIPC +ForegroundProcessStatusresponse (compatible PROTOCOL v8 additions; no discriminant impact thanks to appending at the end of the enum)- State management for the OS-window close dialog — with
close_action = "prompt"(default), the dialog fires whenever a non-shell foreground process is running. - Three Wayland-fallback UX paths — Wayland's security model hides global
coordinates, so drag-out detection cannot work. Alternative paths:
- Context menu: right-click on a pane → "Detach to new window"
- Hotkey:
Ctrl+B D(leader + D) splits the current tab into a new OS window - Command palette:
Ctrl+Shift+P→ "Detach to New Window"
Ctrl+B W(leader + W) closes only the current OS windowOsWindowSnapshot— persists multi-OS-window layout (position, size, and the set of server window IDs that belong to it)- i18n in all 8 languages — 9 tab-tearing strings added to en / ja / zh-CN / ko / de / fr / es / it
- Tab hover
[↗]button — hovering a tab shows an↗icon at the right edge; clicking it detaches the tab into a new OS window. Lets Wayland users perform tab tearing entirely through the GUI (ui_verts drawing + hit-testing + the DetachToNewWindow route) - Renderer drawing + keyboard handling for the confirmation dialog —
build_close_window_dialog_vertspaints a red-accent warning dialog.Enter/Yconfirm the focused button;Esc/Ncancel;←/→/Tabchange focus. - macOS
has_foreground_processimplementation — usesps -A -o pid=,ppid=to find children whose parent is the shell PID. Reliably detects ssh, vim, and other long-running jobs so the confirm dialog fires.
- The
Promptbranch ofon_close_requestedis now real (up through Phase 4-4 it had been degraded to behave like Kill). - The TUI client doesn't support tab tearing; it ignores the new IPC variants as no-ops.
- Windows
has_foreground_processimplementation: currently hard-coded to false (the practical effect is the same as the Kill path). Planned for v1.5.1 PATCH or v1.6.0, using thewindows-syscrate andToolhelp32Snapshot.
cargo test --workspace: 689 tests passcargo clippy --workspace --all-targets -- -D warnings: greencargo fmt --check: clean- 3-OS matrix CI (Linux / macOS / Windows): equivalent to v1.5.0-beta.1
A prerelease of Sprint 5-8 / 5-9 Phase 4 — "tab tearing" (drag-out tab). Shipped to validate the IPC / SNAPSHOT breaking-change impact ahead of the v1.5.0 stable release. Not recommended for production use.
PROTOCOL_VERSION7 → 8: with the addition ofClientToServer::MovePaneToWindow, older clients (up to v1.4.0) and the new server are incompatible at the Hello handshake. Client and server must be upgraded together.SNAPSHOT_VERSION3 → 4: addedServerSnapshot.client_os_windows: Vec<OsWindowSnapshot>. v3 → v4 is auto-migrated through#[serde(default)], so existing users need to do nothing.- See the
v1.4.0 → v1.5.0 section in
docs/MIGRATION.mdfor details.
- Drag-and-drop a tab to another OS window to split it off into a new
window (X11 / macOS / Windows)
- Design holds multiple winit native windows in a single process
(
EventLoopProxy+UserEvent) Window::insert_pane_at/Window::into_single_pane/Session::move_paneimplemented- If the source window becomes empty it is removed automatically
- Design holds multiple winit native windows in a single process
(
- Merge into another OS window — drop the dragged tab onto another window's tab bar to merge them
ClientToServer::MovePaneToWindow { pane_id, target_window_id, insert_at }IPC addedwindow.close_actionsetting added — three values (prompt/detach/kill) control what happens on OS-window close
QueryForegroundProcessIPC +ForegroundProcessStatusresponse (compatible PROTOCOL v8 additions)- OS-window close confirmation dialog — when
close_action = "prompt"(default) and a non-shell foreground process is running, a confirmation dialog appears (state management only at this point; the visual polish arrives in Phase 4-6). - Three Wayland-fallback UX paths — Wayland's security model hides global
coordinates, so drag-out detection cannot work. Alternative paths:
- Context menu: right-click on a pane → "Detach to new window"
- Hotkey:
Ctrl+B D(leader + D) splits the current tab into a new OS window - Command palette:
Ctrl+Shift+P→ "Detach to New Window" - The tab-hover
[↗]button is not shipped in this beta (deferred to Phase 4-6).
Ctrl+B W(leader + W) closes only the current OS windowOsWindowSnapshot— persists multi-OS-window layout (position, size, and the set of server window IDs that belong to it)- i18n in all 8 languages — 9 tab-tearing strings added to en / ja / zh-CN / ko / de / fr / es / it
- The
Promptbranch ofon_close_requestedis now real (up through Phase 4-4 it had been degraded to behave like Kill). - The TUI client doesn't support tab tearing; it ignores the new IPC variants as no-ops.
None (this release is new features only).
- Tab hover
[↗]button: the hover vertex-add inui_verts.rson the renderer side isn't implemented yet. Wayland users should rely on the three alternative paths above. - Renderer drawing of the confirmation dialog: state management only; Phase 4-6 fills in the visual button rendering.
- macOS / Windows
has_foreground_processimplementation: Linux only for now (/proc/{pid}/stattpgid comparison). macOS / Windows return false (the practical effect is the same as the Kill path).
cargo test --workspace: 689 tests pass (+3 over v1.4.0's 686 thanks to snapshot v4)cargo clippy --workspace --all-targets -- -D warnings: greencargo fmt --check: clean- 3-OS matrix CI (Linux / macOS / Windows): equivalent to v1.4.0
release.yml: addedv[0-9]+.[0-9]+.[0-9]+-*(prerelease) to the tag pattern- The
prereleaseflag is now derived automatically based on whether the tag contains- - The Windows MSI build strips the
-beta.1suffix from the WiX Version (WiX v3 constraint)
The minor release following the v1.3.1 hotfix. No user-visible breaking
changes (the nexterm command behaviour, the distribution layout, MSI
shortcut, etc. are all preserved). The internal cleanup that removed the
nexterm-launcher crate (workspace shrinks from 12 to 11 crates) is the
reason this is a MINOR rather than a PATCH. Also bundles a fix for a prefix
keybinding mis-fire bug (real user impact).
- Fixed prefix-keybinding mis-fires (
config_key_matches): the old implementation evaluated only the last token viasplit_whitespace().last(), so configurations likekeys = [{ key = "<leader> d", action = "ClosePane" }]matched a baredpress and fired the action immediately ("<leader> %"similarly fired on bare5). Changedconfig_key_matchesto return false whenever the key string contains spaces, making it strictly for single-key bindings. IntroducedClientState.prefix_pending_until: Option<Instant>to track prefix mode: pressing the leader alone only enters prefix mode (and suppresses PTY transmission) when at least one<leader> Xbinding exists. Splitcheck_config_keybindingsinto prefix and single-key paths, with automatic 2-second timeout. Added 13 unit tests in key_map (4 regression tests for the bug + happy-path single keys + 9 edge cases).
nexterm-launchercrate removed: ever since v0.9.3 madenexterm-client-gpustart the server as an internal tokio task (single-binary design,bin name = "nexterm"), the launcher had no purpose — but it was never deleted. Up through v1.3.1 bothnexterm-launcherandnexterm-client-gpudeclaredbin name = "nexterm", socargo buildwould overwritetarget/release/nextermdepending on compilation order (in practice the client-gpu variant won). This release deletes the launcher crate outright, resolving the bin-name collision at the root. Cleaned up the correspondingif [ -f ]guards in WiX / Flatpak /release.yml(they were for the client-gpu variant). The result matches the layout of mainstream terminal emulators (Alacritty / kitty / Ghostty / WezTerm): a single main binary (nexterm) plus a few auxiliary CLIs (nexterm-ctl/nexterm-client-tui/nexterm-server). No user impact (thenextermcommand behaviour, distribution layout, MSI shortcut, etc. are unchanged). Workspace consolidated from 12 → 11 crates.
- Fixed gaps between public docs and reality: a batch update of stale
text that misled new users.
- The
bincode→postcardmigration (done on 2026-05-12 in Sprint 5-1 / ADR-0006) was not propagated. Updated README.md / README.ja.md / docs/ARCHITECTURE.md / docs/THREAT_MODEL.md / docs/PROTOCOL.md (body text and diagrams). docs/DESIGN.md's ADR-004 is now explicitly noted as superseded by ADR-0006. PROTOCOL_VERSION = 1→7(README and README.ja v1.1.0 sections; annotated "see nexterm-proto/src/lib.rs for the current value").- Rust MSRV
1.78/1.80→1.85(required because the workspace usesedition = "2024"). Updated README.md / README.ja.md / docs/src/install.md. SNAPSHOT_VERSION = 2→3(workspace_name was added in Sprint 5-7 / Phase 2-1). Updated CLAUDE.md / docs/THREAT_MODEL.md / docs/adr/0007-snapshot-v1-deprecation.md.- CONTRIBUTING.md / CONTRIBUTING.ja.md dependency list: bincode → postcard.
- nexterm-client-core/src/lib.rs framing comments switched to postcard.
- README.md test count "240+ tests" → measured 660+ (with an annotation in the Test Strategy table of docs/ARCHITECTURE.md).
- README.ja.md wording around the "daemonless design" updated to "an internal tokio task owns the PTY", matching the single-binary implementation.
- The
- Bumped workspace version
1.3.1→1.4.0(cleanup release, no breaking changes).
Minor version corresponding to Sprint 5-6 (GPU client large-file split) and Sprint 5-7 (UI/UX modernization Phases 1 + 2 + 3). Contains breaking changes to the IPC protocol and snapshot format — consult docs/MIGRATION.md before upgrading.
PROTOCOL_VERSIONbumped4→75: workspace IPC (ListWorkspaces/CreateWorkspace/SwitchWorkspace/RenameWorkspace/DeleteWorkspace+WorkspaceList/WorkspaceSwitched) (Phase 2-1)6: Quake-mode IPC (QuakeToggle+QuakeToggleRequest) (Phase 2-2)7: tab reordering IPC (ReorderPanes) (Phase 2-3)
SNAPSHOT_VERSIONbumped2→3: addedSessionSnapshot.workspace_nameandServerSnapshot.current_workspace. v2 JSON auto-migrates throughserde(default).- A new server rejects an old client at the Hello handshake, and an old server rejects a new client. Always upgrade client and server together.
- Background image support (Phase 3-1):
[window.background_image]displays a wallpaper-style background. Five fit modes:cover/contain/stretch/center/tile, with adjustable opacity. Images larger than 4096×4096 are auto-downscaled with Lanczos3. Reuses the existingimage_pipeline(used for Sixel/Kitty). - UI animations (Phase 3-2): tab switching 200 ms (accent line
extending + fade-in) and pane insertion 250 ms (white-overlay fade-out),
both ease-out.
[animations] enabled = falseorintensity = "off"flips animations to instant (reduced-motion support). Four intensity steps:off/subtle(×0.5) /normal(×1.0) /energetic(×1.5). - Command palette: full coverage + history persistence (Phase 3-3):
added the 6 missing actions (Quit / ClosePane / NewWindow / QuickSelect /
SetBroadcastOn / SetBroadcastOff) for a complete set of 25 actions.
Usage history persisted to
~/.local/state/nexterm/palette_history.json(atomic write + Unix 0600). Ranking: by history when the query is empty; with a query, fuzzy score + history_bonus (use_count × 10 capped at 100, +100 within 24 h, +50 within a week).
- Workspaces (Phase 2-1): introduced a "workspace" concept that groups
sessions. Added
nexterm-ctl workspace list / create / switch / rename / delete [--force]subcommands. The status bar exposes a built-inworkspacewidget that shows the current workspace. Thedefaultworkspace cannot be renamed or deleted. - Quake mode (Phase 2-2): a global hotkey (default:
Ctrl+\``) makes the window slide in from a screen edge (top/bottom/left/right). Uses theglobal-hotkey0.8 crate. Wayland has no global-hotkey API; as a workaround,nexterm-ctl quake toggle/show/hidecan be invoked from a compositorbindsym`. - Tab reordering (Phase 2-3): drag tabs left or right on the tab bar to
reorder. A 6 px threshold confirms the drag; below that it's treated as a
click. While dragging, a ghost tab and an insertion-position indicator
are drawn.
pane_order: Vec<u32>is managed separately from the physical layout.
- Dynamic tab colors + hover highlight (UI-1-1): added
activity_tab_bg/active_accent_color/show_tab_number/inactive_text_brightness/hover_highlighttoTabBarConfig. Tab backgrounds brighten on mouse hover. - Right-side status-bar widgets (UI-1-2): added built-in widgets
cwd/cwd_short/git_branch/workspace. ExtendedWidgetContextto propagate the focused pane's cwd. - Leader key support (UI-1-3):
Config.leader_keyallows configuring the<leader>placeholder, making WezTerm-style prefix keybindings concise to express. - Key-hint overlay (UI-1-4): pressing the leader alone shows
prefix-style bindings as a semi-transparent overlay at the bottom of the
screen for 2 seconds. New module
renderer/overlay/key_hint.rs.
No behaviour changes. Split the four largest GPU client files into submodules to improve maintainability.
event_handler.rs(1,318 lines) → 7 submodules (consent / settings_panel_hit / lifecycle / window / mouse / keyboard)input_handler.rs(1,377 lines) → 6 submodulesrenderer/mod.rs(1,579 lines) → 6 files (wgpu_init / render_frame / event_handler / etc.)state.rs(1,319 lines) → 7 files (pane / search / selection / menus / consent / server_message + state/mod.rs)
Sprint 5-7-related UI strings added to all 8 languages (en / ja / zh-CN / ko / de / fr / es / it). 6 keys × 8 languages = 48 new entries for the command palette, plus workspace / Quake / key-hint related strings.
The release tied to completing Sprints 5-1 through 5-5 of audit round 2 (70 tasks). Contains breaking changes — consult docs/MIGRATION.md before upgrading.
PROTOCOL_VERSIONbumped1→42: removed plaintext SSH password from the IPC (Sprint 5-1 / G1)3: migrated IPC wire format from bincode to postcard (Sprint 5-1 / G3)4: added OSC 7 CWD reporting and theCwdChangedevent (Sprint 5-2 / B2)
- IPC wire format: replaced bincode with postcard (old v1.1.0 clients cannot connect to a v1.2.0 server)
- GPU present mode default:
fifo→mailbox(accept tearing in exchange for a one-frame latency reduction; explicitly setpresent_mode = "fifo"to restore the old behaviour)
- 15 new unit tests in
nexterm-ssh(I1):parse_jump_spec/parse_socks5_credentials/parse_forward_spec/SshConfigconstruction / fast-fail on unreachable port. A full mock SSH server is future work. - 5 new smoke tests in
nexterm-launcher(I2): per-OS extension paths forserver_exe/client_exe/tui_exe,exe_dir, and thewait_for_servertimeout path. tracing::instrumenton key async paths (A6):SshSession::connect/authenticate/open_shell,persist::save_snapshot/load_snapshot, and IPCdispatch_inner.dispatch_innerusesskip_allbecause of sensitive payloads.- Snapshot v1 removal timeline pinned in ADR-0007 (A9): we plan to bump
SNAPSHOT_VERSION_MINfrom1to2in v2.0.0. Thenexterm-pluginv1 removal timeline is referenced through ADR-0003 for consistency. - mdBook skeleton (J1): added new
docs/src/troubleshooting.md/docs/src/adr-index.md.SUMMARY.mdgained a Reference section.README.mdupdated to a v1.2.0 baseline. - rustdoc warnings 9 → 0 (J2): backticked
[[macros]]/vec2<f32>/https://.../rows[y][x]so they are not interpreted as links.cargo doc --no-deps --lib --workspacenow finishes with zero warnings.
- Split
overlay_verts.rs(1,958 lines) into 5 files (A2):renderer/overlay/{picker, dialog, settings, util, mod}.rs. The largest is settings.rs at 795 lines. - Split
nexterm-ctl/main.rs(1,757 lines) (A1): main.rs 343 lines + ipc.rs 96 lines +cmd/{session, record, template, service, ghostty, theme, plugin, wsl, util, mod}.rs. - Split
nexterm-server/web/mod.rs(1,088 lines) (A3): mod.rs 247 lines + router.rs 129 lines + middleware.rs 144 lines +handlers/{page, login, oauth, ws, assets, mod}.rs. - Migrated examples/plugins (4) to Plugin API v2 (F1): added
nexterm_api_version() -> 2, bumped plugin versions to0.2.0, and added a v1→v2 migration guide inexamples/plugins/README.md. - WSL distro auto-detection + Profile import (E1):
nexterm-ctl wsl import-profiles [--dry-run]auto-detects WSL distros like Ubuntu and writes[[profiles]]entries toconfig.toml. - Quick Select expanded: 5 → 11 patterns (D1): added Email / UUID / file:line / Jira / Windows path / IPv6 and others. Priority-ordered with duplicate elimination and 10 unit tests.
- Theme gallery hidden-bug fix +
nexterm-ctl themesubcommand (D4): fixedparse_builtin_schemefalling back to Dark for Catppuccin / Dracula / Nord / OneDark. AddedBuiltinScheme::all() / from_toml_name(). - Added
Ctrl+Shift+Zas an alternative binding for Pane Zen mode (D8): alongside the tmux-styleCtrl+B Z. - ADR directory cleanup (J3): added template, README index, and 5
retroactive ADRs (0002–0006) under
docs/adr/.
- Introduced criterion benchmarks for
nexterm-vt(C5): performance regressions on the VT parser, scrolling, and Sixel decoding are now detectable.cargo bench -p nexterm-vt. - Input-latency measurement script added (C1): scripted VT advance 1 ms / wgpu present queue size measurements.
- wgpu upgrade policy captured in ADR-0001 (C2): organised the 22 → 26 decision with test results and alternatives.
- Default present_mode changed to
mailbox(C3): cuts 1 frame.[gpu] present_mode = "fifo"restores the old behaviour. - Added a coverage job to GitHub Actions (I5):
cargo-llvm-covgeneratestarget/coverage. - Published benchmark numbers in
docs/benchmarks.md(J4): documented reference values and how to re-measure.
- Full OSC 133 (semantic prompt marks) + jump-to-prompt support (B1):
the client records prompt boundaries;
Ctrl+Up/Ctrl+Downjump to the previous / next prompt. Also selectable from the command palette as "Jump to previous prompt / next prompt". Localised in 8 languages. - OSC 7 (CWD reporting) + parent CWD inheritance (B2): added the
CwdChangedIPC event (PROTOCOL_VERSION4). Splitting a new pane now inherits the parent pane's CWD. - Synchronized Output (DCS=2026) test coverage (B5): pinned existing behaviour with VT snapshot tests.
Breaking change: PROTOCOL_VERSION bumped 2 → 3. See
docs/MIGRATION.md for details.
- Removed
bincode = "1"from every crate and replaced it withpostcard = "1" (use-std).- Affected crates:
nexterm-proto/nexterm-server/nexterm-client-core/nexterm-client-gpu/nexterm-client-tui/nexterm-ctl - Affected calls:
bincode::serialize→postcard::to_stdvec,bincode::deserialize→postcard::from_bytes(3 implementation sites + 19 in tests)
- Affected crates:
- Removed the
deny.tomlignore forRUSTSEC-2025-0141(bincode 1.x unmaintained).cargo deny checknow passes theadvisoriessection with zero ignores. - Side benefit: postcard's varint encoding shrinks IPC messages by 10–20% on average.
- Effect: removed the lock-in on bincode 1.x and moved to a maintainable supply chain.
Breaking change: PROTOCOL_VERSION bumped 1 → 2. See
docs/MIGRATION.md for details.
- Removed
password: Option<String>fromClientToServer::ConnectSsh. Replaced with:password_keyring_account: Option<String>— account identifier in the OS keyringephemeral_password: bool— flag to delete the keyring entry after successful authentication
- Client (nexterm-client-gpu):
connect_ssh_host_with_password()first callsnexterm_config::keyring::store_password()to save the password, then sends only the account name over IPC. WhenPasswordModal.remember=false,ephemeral_password=trueis set. - Server (nexterm-server):
handle_connect_ssh()retrieves the password vianexterm_config::keyring::get_password()and passes it to russh wrapped inZeroizing<String>. Whenephemeral_password=true, the entry is deleted after authentication. - Effect: no plaintext password ever crosses the Unix Domain Socket /
Named Pipe; the TODO in
input_handler.rs(HIGH H-6) is cleared.
- Pinned every GitHub Action to a git SHA (an SLSA 2 requirement).
Mutable-tag references like
actions/checkout@v4were replaced with the corresponding git commit SHA plus a# v4.3.1style comment. This defends against supply-chain attacks that retag upstream actions.- Files updated:
ci.yml/release.yml/sbom.yml/fuzz.yml/flatpak.yml/pages.yml(36 sites total) - 9 actions pinned:
actions/checkout,actions/upload-artifact,actions/upload-pages-artifact,actions/deploy-pages,actions/attest-build-provenance,Swatinem/rust-cache,EmbarkStudios/cargo-deny-action,softprops/action-gh-release,dtolnay/rust-toolchain(stable / nightly)
- Files updated:
Rollup release marking the end of Sprints 1–4. Contains breaking changes — consult docs/MIGRATION.md before upgrading.
- Bumped
PLUGIN_API_VERSIONto 2. New host contract:- Input sanitization: ESC, OSC/CSI/DCS/APC sequences, and C0 control
characters (except
\t\r\n) are stripped before they reachnexterm_on_output/nexterm_on_command. Plugins see plain text only. write_panePaneId allow-list: writes are permitted only to the pane IDs allowed for the current call scope. Duringnexterm_on_output(pane_id, ...)only thatpane_idmay be written; duringnexterm_on_commandno pane may be written.
- Input sanitization: ESC, OSC/CSI/DCS/APC sequences, and C0 control
characters (except
MIN_SUPPORTED_API_VERSION = 1preserves backwards compatibility for v1 plugins. They run with the old semantics (no sanitization, no write restriction) and a deprecation warning is logged at load.- Added
PluginInfo.api_version(shown bynexterm-ctl plugin list). - Made
sanitize_for_plugin(input: &[u8]) -> Vec<u8>public (for tests and diagnostics).
- Added
proptest(1.x) as a workspace dependency ([workspace.dependencies]). Referenced from[dev-dependencies]ofnexterm-vt/nexterm-server. - Sixel / Kitty parser property tests
(
nexterm-vt/tests/proptest_image.rs):decode_sixel/decode_kittynever panic on arbitrary byte streams- On success,
rgba.len() == width * height * 4always holds - Huge dimensions (≥ 8193×8192) are always rejected with
None - Panic-resistant when invoked through the VtParser path (including APC)
- BSP / tiling property tests (
nexterm-server/src/window/tests.rs):- Arbitrary sequences of Insert / Remove operations never make
compute()panic - Given enough area, rectangles stay on screen, don't overlap, and IDs are unique
- Snapshot round-trips preserve pane IDs and rectangles
- Tiling invariants hold (pane count match, in-bounds, ID match)
- Arbitrary sequences of Insert / Remove operations never make
Fixed CRITICAL / HIGH issues surfaced by a comprehensive security audit. Contains breaking changes. See docs/MIGRATION.md for details.
- OAuth GitHub Org-validation bypass fixed (CRITICAL): the old
implementation's
get_current_token()always returnedNone, so Org membership was never actually verified.exchange_code()now returns theaccess_tokenand propagates it tois_user_allowed(). - TOTP replay-attack mitigation (CRITICAL): detects and rejects reuse
of an OTP code within the ±1 window using
subtle::ConstantTimeEqconstant-time comparison + aHashSet<(window, code)>. - TOTP IP-based rate limiting (CRITICAL): brute-force defense at 5
attempts / 60 s, implemented in
web::rate_limit. Returns 429 withRetry-After: 60. - TLS fallback off by default (CRITICAL): silent downgrade to HTTP on
TLS-config failure is gone. Explicitly opt in with
[web] allow_http_fallback = true. - OIDC userinfo_endpoint SSRF mitigation (HIGH): enforce HTTPS, reject internal IPs, verify issuer-domain match.
legacy_tokenconstant-time comparison (HIGH):subtle::ConstantTimeEqprevents timing attacks.
- bincode message-size cap (CRITICAL):
MAX_MSG_LEN = 64 MiBprevents local OOM attacks (applied at server / GPU/TUI clients / ctl — 4 places). - Protocol Hello + versioning: connections must begin with
ClientToServer::Hello { proto_version, client_kind, client_version }.PROTOCOL_VERSION = 1. Mismatch disconnects.
- VT buffer caps (CRITICAL): introduced APC 4 MiB / DCS Sixel 16 MiB / Kitty chunked-transfer 64 MiB caps. Prevents DoS by a malicious PTY.
- Image-decode u32 overflow fixed (CRITICAL):
width * height * 4is computed in u64 and limited byMAX_IMAGE_BYTES = 256 MiB. - OSC 8 URI allow-list (CRITICAL): rejects schemes like
javascript:/file:. Allowed:http/https/mailto/ftp/ftps/ssh. Caps: title 256 / notification 1024 / URI 2048 bytes.
- Lua sandbox (CRITICAL): disabled
os/io/package/require/dofile/loadfile/debug. Blocks RCE viaconfig.lua. - WASM sandbox hardening (CRITICAL): wasmi
consume_fuel(true)+FUEL_PER_CALL = 10Mprovisioned before each call.MAX_MEMORY_PAGES = 256(16 MiB) caps memory.nexterm_api_version()verifies the version at load. Mutex poisoning is recoverable.
- snapshot / host_history use atomic write + 0600 (CRITICAL): write to
a temp file → fsync → rename, and force
mode(0o600)on Unix. Prevents corruption on crash and leakage of sensitive data. - Force 0600 on TLS private keys (HIGH): the key file produced when generating a self-signed cert is made 0600 regardless of umask.
- GUI
PasswordModalusesZeroizing<String>(HIGH): the password-input buffer is zeroed on drop.
- Strip query strings from the access log (HIGH): prevents OAuth
?code=/?state=/?token=and friends from leaking into the log.
- TomlConfig functionality regression fixed: the old
TomlConfigintermediate struct was missingwindow/web/hosts/macros/log/cursor_style/ auto_check_update/languageand similar fields, so most of what users wrote inconfig.tomlwas being silently ignored. Switched to deserializingConfigdirectly. - DEFAULT_CONFIG_TOML template fixed: the first-launch template used
keys that didn't match the implementation, e.g.
[color_scheme] builtin = .../[tab_bar] show = .... Aligned the key names with the implementation. - CI repair:
cargo fmt --checkhad been failing on master; cleaned up.
- Tests: added focused tests for every CRITICAL/HIGH fix (about 60 tests across the proto / vt / config / server / plugin crates).
docs/MIGRATION.md: migration document for the breaking changes (Lua sandbox, protocol Hello, TLS fallback default-off).
- v1.0.0 release: the 0.9.x line is stabilised and officially released
as v1.0.0.
- Plugin API v1 frozen (
PLUGIN_API_VERSION = 1) for a stable ABI - WASM plugin runtime (wasmi) +
nexterm-ctl pluginCLI - SSH host history persistence + password-auth modal
- Snapshot schema v2 (with auto migration)
- GPU renderer (wgpu + cosmic-text), 3-pass rendering pipeline
- 8-language i18n (en/ja/zh-CN/ko/de/fr/es/it)
- Auto-update notification (polls the GitHub Releases API)
- Settings panel (7 categories, TOML write-back, hot reload)
- Web terminal (axum WebSocket + xterm.js)
- Serial-port connection support
- Plugin API v1 frozen (
- CI branch fix: changed the trigger branches in
.github/workflows/ci.ymlfrommain/developtomaster. CI now runs automatically on push and PR to the default branch.
- MSI auto-update notification: 5 seconds after startup, polls the
GitHub Releases API in the background and shows a green banner at the
top of the screen if a newer release exists.
- New
update_checkermodule (nexterm-client-gpu/src/update_checker.rs) - Uses
tokio::sync::watchto asynchronously notify the latest version - The banner closes with
Esc;Enteropens the release page in the default browser
- New
auto_check_updateconfig field: addedauto_check_update = true/falsetoconfig.toml. Defaulttrue.- Settings-panel integration: added an
auto_check_updatetoggle to the Startup category (toggle withSpace, save withEnter). - i18n in 8 languages: added the
update-available/update-dismiss/update-open-releaseskeys to all 8 languages.
- Plugin API freeze (
PLUGIN_API_VERSION = 1): Stable WASM ABI is now versioned.nexterm-pluginexportsPLUGIN_API_VERSION: u32 = 1and providesnexterm.api_version() -> i32as a host import so plugins can verify compatibility at runtime. nexterm_metaplugin export: Plugins can now exportnexterm_meta(name_buf, name_max, ver_buf, ver_max) -> i32to publish their name and version. Displayed innexterm-ctl plugin list.unload/reloadmethods onPluginManager: Plugins can be unloaded (by path) or reloaded (unload + load) at runtime without restarting the server.- IPC plugin commands: Four new
ClientToServermessages:ListPlugins,LoadPlugin { path },UnloadPlugin { path },ReloadPlugin { path }. CorrespondingServerToClientresponses:PluginList { paths },PluginOk { path, action }. nexterm-ctl pluginsubcommands:list,load <path>,unload <path>,reload <path>.PluginManagerembedded inSessionManager: Plugin manager is now accessible from the IPC dispatch layer viamanager.plugin_manager.echo-suppresssample plugin (examples/plugins/echo-suppress/): Demonstratesnexterm_meta,api_version()import, and output suppression.docs/plugin-api.md: Full Plugin API reference documenting all host imports, plugin exports, memory layout, and CLI management.
PluginInfonow includesname: Option<String>andversion: Option<String>fields populated fromnexterm_meta.- Existing sample plugins README updated to include
echo-suppress.
- Host history persistence: Connection history is now saved to
~/.local/state/nexterm/host_history.json(Unix) /%APPDATA%\nexterm\host_history.json(Windows). Frequently-connected hosts sort to the top across restarts. - Password authentication modal: Selecting a host with
auth_type = "password"in the SSH Host Manager now opens a password input overlay. Password characters are masked with*. Press Enter to connect, Esc to cancel. record_connectionwired: Entering a host from the Host Manager now records the connection in history and persists it to disk immediately.
HostManager::newnow callsload_history()on startup so previously recorded frequencies are available immediately.PasswordModalstruct added tohost_managermodule withpush_char,pop_char, andtake_passwordmethods.
- Snapshot schema v2: Added
session_titlefield toSessionSnapshotfor future display title support. Old v1 snapshots are automatically migrated on load. - Snapshot migration:
persist::load_snapshotnow migrates v1 snapshots to v2 instead of discarding them. Supported version range: v1–v2. - Version guard:
restore_from_snapshotnow accepts snapshots in the supported range (v1–v2) instead of requiring an exact version match.
test_v1_snapshot_migrates_to_v2: Verifies that a v1 JSON snapshot deserializes correctly withsession_titledefaulting toNone.test_session_title_defaults_to_none: Verifies backward-compat deserialization whensession_titleis absent.
- russh 0.58 → 0.59: Mitigated pre-authentication DoS vulnerability (keyboard-interactive unbounded allocation). Updated
AgentIdentity::public_key()call to match the newauthenticate_publickey_withsignature in russh 0.59. - lru 0.12 → 0.17: Resolved
IterMutstacked-borrows violation in the glyph atlas LRU cache.
- Cursor style: New
cursor_styleconfig option ("block"/"beam"/"underline") to control the cursor shape in the GPU renderer. - Window padding: New
[window] padding_x/padding_yconfig options to add pixel padding around the terminal grid. - Present mode: New
[gpu] present_modeconfig option ("fifo"/"mailbox"/"auto") to control wgpu vsync behaviour. - Default color scheme: Changed default color scheme to
TokyoNight.
- Glyph atlas LRU cache: Replaced the
HashMap-based glyph cache with anLruCacheto automatically evict stale entries after font changes, reducing memory waste. - Atlas size from config:
[gpu] atlas_sizeis now used as the maximum texture size for the glyph atlas. Initial size starts at halfatlas_size(minimum 1024) and grows on demand. - Broadcast channel capacity: Increased IPC broadcast channel capacity from 512 → 2048 to reduce dropped messages under heavy output.
- Pane border visibility: Increased separator width from 1 px → 2 px and adjusted border colour for better contrast with the Tokyo Night theme.
- clippy lint: Resolved
type_complexitylint innexterm-server/src/web/oauth.rsby introducing aOAuthClienttype alias. Resolvedcollapsible_iflint innexterm-server/src/lib.rs.
- Touchpad scrolling: Fixed an issue where Windows touchpad scroll events (PixelDelta) were silently ignored. Added an accumulation buffer that triggers a line scroll once enough delta accumulates to equal one cell height.
- Font ligatures: Fixed an issue where
[font] ligatures = truein the config file was not correctly passed through to FontManager.
- CI quality: Removed
continue-on-error: truefrom the Windows ConPTY integration test so that test failures now cause the build to fail. - WiX build stability: Changed version injection to use
candle.exe -dVersion=X.Y.Zflag instead of modifying the source file (wix/main.wxs) directly.
window_config_default_valuetest: Fixed a mismatch where the test expectedbackground_opacityto be1.0even after the default was changed to0.95.
- PowerShell auto-launch: Fixed an issue where PowerShell did not start automatically on Windows. The config including the
-NoLogoargument is now correctly propagated to all pane creation paths. - Window transparency: Fixed an issue where the window background was not transparent on first launch without a config file. Changed the default opacity to 0.95.
- Freeze on close: Fixed a hang when closing the window with the × button. The IPC connection is now dropped before the server task is terminated.
- Context menu text overflow: Fixed shortcut key labels overflowing outside the menu border. Unified drawing position calculation to use
visual_width().
- Dependency update: Updated
randfrom 0.8.6 to 0.9.4.
- Language selection UI: Added the ability to select the UI language during installation and from the settings panel (8 languages supported).
- Context menu width: Fixed menu overflow for languages with longer translated text.
- Freeze on window close: Fixed a hang that occurred when attempting to close the window.
- PowerShell detection: Improved accuracy of automatic PowerShell shell detection.
-
nexterm-server ipc.rs module split: Split
ipc.rs(1707 lines) into 5 submodules for improved maintainability.ipc/platform.rs— Unix Domain Socket / Windows Named Pipe listener and UID verificationipc/handler.rs— Read/write loop for connected clientsipc/dispatch.rs— Dispatch logic for 40+ IPC commandsipc/key.rs— Keycode → VT escape sequence conversion (8 unit tests)ipc/sftp.rs— SFTP upload/download helpers
-
Integration tests added: Added 2 files under
nexterm-server/tests/.ipc_integration.rs— Round-trip tests for bincode serialization + 4-byte LE framing (14 tests)snapshot_roundtrip.rs— JSON round-trip and persistence tests for session snapshots (6 tests)
-
#![warn(missing_docs)]applied workspace-wide: Applied to 6 crates (nexterm-vt / nexterm-ssh / nexterm-plugin / nexterm-config / nexterm-server / nexterm-i18n) with missing documentation added in bulk.
- Reduced
unwrap()in production code: Converted unsafeunwrap()calls inweb/mod.rs,web/auth.rs,web/oauth.rs,window.rs,nexterm-plugin, andnexterm-sshtoexpect("reason")for improved panic diagnostics. persist::state_dir(): Fixed to prefer theXDG_STATE_HOMEenvironment variable (for test isolation and XDG compliance).
- CLAUDE.md: Added project guide for Claude Code. Documents build commands, architecture overview, and coding conventions.
- docs/KEYBINDINGS.md: Extracted the complete key binding reference into a standalone file.
- Dependency updates: Updated 104 packages to their latest compatible versions, including
vte0.13 → 0.15,cosmic-text0.12 → 0.18, andportable-pty0.8 → 0.9. - README refactor: Reduced README.md by 32% (1019 → 690 lines). Replaced the changelog section with a link to CHANGELOG.md and moved key binding details to docs/KEYBINDINGS.md.
- nexterm-client-gpu module split: Extracted 5 modules from
renderer.rs(5553 lines) to improve maintainability.glyph_atlas.rs— GlyphAtlas, BgVertex, TextVertex, GlyphKeyshaders.rs— WGSL shader constantscolor_util.rs— ANSI 256-color and hex color conversion utilitieskey_map.rs— winit keycode ↔ proto keycode conversionvertex_util.rs— Rectangle, text, URL, and grid → text conversion utilities
- Rustdoc expansion: Added documentation comments to all public APIs in
nexterm-proto(messages, types, enums). Enabled#![warn(missing_docs)]. - unsafe SAFETY comments: Documented safety rationale for
SO_PEERCRED/getpeereidinnexterm-server/ipc.rsandlibc::killinpane.rs. - Clippy warnings resolved: Resolved all Clippy warnings across the workspace. Now compliant with CI's
-D warningsflag.
- PowerShell crash fix: Replaced direct array index accesses in
nexterm-vt'serase_in_line,erase_in_display, andscroll_upwith the safeGrid::clear_row()/Grid::copy_row()methods. Prevents IndexError panics caused by complex VT sequences sent by PSReadLine.
- Settings panel mouse interaction: Sidebar categories, font size/opacity sliders, and theme color dots can now be clicked and dragged with the mouse. Sliders auto-save on drag release. Clicking outside the panel closes it.
- Terminal background transparency: The terminal background is now 95% opaque by default (
background_opacity = 0.95), giving a subtle see-through effect. The settings panel and context menu always remain fully opaque. Adjustable between 0.1 and 1.0 via[window] background_opacityinnexterm.toml. - Memory usage reduction: Changed
cosmic-text'sFontSysteminitialization from a full system scan to loading only OS-specific font directories (macOS:/System/Library/Fonts, Windows:C:\Windows\Fonts). Estimated ~30–40 MB memory reduction.
Web Terminal: OAuth2 / SSO authentication
- OAuth2/OIDC support for GitHub, Google, Azure AD, and any generic OIDC provider.
- Authorization Code Flow with CSRF protection (state parameter, 10-minute TTL).
- Access control via
allowed_emailsandallowed_orgs(GitHub only). - Client secret can be set via
NEXTERM_OAUTH_CLIENT_SECRETenvironment variable (recommended over storing innexterm.toml). - OAuth login button automatically injected into the login page when OAuth is enabled.
Web Terminal: session management improvements
- Configurable session TTL via
[web.auth] session_timeout_secs(default: 86400 s = 24 h). - Concurrent session limit via
[web] max_sessions(0 = unlimited); oldest session is evicted when limit is reached. - Explicit logout endpoint:
POST /auth/logoutrevokes the session cookie.
Web Terminal: HTTPS enforcement
- New
[web] force_https = trueoption; checksX-Forwarded-Protoand issues 301 redirects for HTTP requests (useful behind a TLS-terminating reverse proxy).
Web Terminal: access log
- New
[web.access_log]section; logs every request (including WebSocket upgrades and failed auth attempts). - CSV output to a configurable file path, or to the server log via
tracingwhen no file is set. - Fields:
timestamp,remote_addr,method,path,status,auth_method,user_id.
TUI client: multi-pane support
- Ctrl+B prefix key system for pane management.
- Horizontal/vertical split, focus cycling, pane close, zoom.
- Status bar showing active session and pane count.
- Full help overlay (Ctrl+B ?).
SSH host manager enhancements
- Tag-based filtering and group management.
- Connection history with frequency-based sorting.
- Bulk operations (connect all in group, disconnect all).
WASM plugin examples
- Three ready-to-build sample plugins:
error-detector,command-counter,timestamp-injector. - Full plugin documentation including C and Rust examples.
Documentation
- Quickstart guide improvements, configuration snippet collection, Lua macro recipe collection.
- Full web terminal authentication reference including enterprise GitHub SSO example.
[web.auth]now containssession_timeout_secsfield (previously hardcoded to 24 h).[web]has new fields:max_sessions,force_https,access_log.nexterm-config:OAuthConfigandAccessLogConfigare now publicly exported.
GPU client: TUI-parity tab bar and settings panel
- Tab bar now displays the OSC 0/2 window title (e.g. current working directory) in each tab label, matching the TUI client behaviour.
- "⚙ Settings" button rendered on the right side of the tab bar; clicking it toggles the settings panel without a keyboard shortcut.
- Mouse click hit-testing on tab bar: clicking a tab switches the active pane; clicking the settings button opens/closes the panel.
- Settings panel font family field is now fully editable: press F (on Font tab) to enter edit mode, type the family name, Backspace to delete, Enter to confirm, Escape to cancel. Characters are intercepted before forwarding to the server.
PaneStatecarries atitle: Stringfield updated byServerToClient::TitleChangedmessages.ClientStatecarriestab_hit_rectsandsettings_tab_rectpopulated each frame bybuild_tab_bar_verts.
render()andbuild_tab_bar_verts()now take&mut ClientStateto allow per-frame hit-rect writes.
- All documentation converted to English as the primary language.
- Japanese translations added for user-facing docs:
shaders.ja.md,performance.ja.md,graphics.ja.md,plugins.ja.md. docs/ARCHITECTURE.mdanddocs/CONFIGURATION.mdfully translated to English.
GPU client: rendering quality pass
- Added status bar height (
cell_h) to thevisible_rowscalculation in scrollback view, fixing overlap between the last row and the status bar. ScaleFactorChanged(DPI change) event now recalculatescols/rowsand sends a Resize notification to the server, resolving layout shift when moving to a high-DPI display.- Applied
tab_bar_hoffset to the right-click context menu y-coordinate, fixing menu position when the tab bar is enabled. - Added
cleared_this_frameflag toGlyphAtlas; resetting the flag at the start of each frame prevents glyph corruption from stale UV coordinates after an atlas overflow mid-frame. - Pre-declared
family_ownedfor all code paths infont.rsto clarify lifetime structure.
GPU client (Windows): fix CJK full-width character spacing
- Added
wide: boolparameter torasterize_char(); full-width characters (Unicode width ≥ 2) now render into a 2-cell buffer (display_cols = 2.0). - Added
widefield toGlyphKeyso full-width and half-width glyphs are cached separately in the atlas. - Japanese, Chinese, Korean, and other CJK characters are now evenly spaced and correctly rendered.
GPU client (Windows): fix tab bar / terminal content overlap
- Fixed tab bar (at y=0) and row-1 terminal content being drawn at the same y-coordinate.
- Added
y_offset: f32parameter tobuild_grid_verts/build_scrollback_verts. - Multi-pane
_in_rectfunctions updated to useoff_y = row_offset * cell_h + tab_bar_h. - Pane borders and number badges now account for the tab bar height.
GPU client (Windows): fix black band on the right side
- The
rowscalculation was using the full window height, causing overlap with the tab bar and status bar. - Fixed with
rows = (height - tab_bar_h - status_bar_h) / cell_hfor accurate usable row count. - Corrected in both the initial window setup and resize event handler.
- Mouse click → cell coordinate conversion now subtracts
tab_bar_hfor accurate row targeting.
GPU client (Windows): fix font character spacing
Attrs::new()defaulted toFamily::SansSerif, causing fallback to a proportional font (Segoe UI, etc.) on Windows.measure_char_widthandrasterize_charnow explicitly setFamily::MonospaceorFamily::Name(family).- Config font name
"monospace"maps toFamily::Monospace(fontdb selects the system monospace font); specific names (Consolas,JetBrains Mono, etc.) useFamily::Namedirectly. - Cell width measurement switched from
Buffer::draw()ink pixels tolayout_runs()advance width, which includes right bearing for accurate character spacing. - Eliminates the "Wi ndows PowerShe l l" extra-space rendering bug.
Shader hot-reload, gallery, and migration tools
- Added
WgpuState::reload_shader_pipelines(): hot-reloads WGSL shaders on file change (no restart needed). examples/shaders/: bundled sample WGSL shaders — CRT, Matrix, Glow (background) / Grayscale, Amber (text).nexterm-ctl import-ghostty: imports a Ghostty config file and converts it to nexterm config.nexterm-ctl service install/uninstall/status: manages autostart services via systemd (Linux) / launchd (macOS).
Custom WGSL shader support
- Added
[gpu]section tonexterm-config(custom_bg_shader/custom_text_shader/fps_limit/atlas_size). - GPU client loads WGSL files from the specified paths at startup (falls back to built-in shaders on failure).
- Enables custom effects such as CRT scanlines and glow.
Documentation site expansion
docs/src/features/graphics.md: Sixel / Kitty graphics protocol guide.docs/src/features/plugins.md: WASM plugin development guide (with Rust sample code).docs/src/advanced/shaders.md: custom WGSL shader reference and examples.docs/src/advanced/performance.md: performance tuning guide.
GPU buffer reuse for rendering optimization
- Added reusable vertex/index buffers to
WgpuState. - Replaced per-frame
create_buffer_init(GPU allocation) withqueue.write_bufferoverwrites. - Buffers are only reallocated (2× size) when capacity is exceeded; no reallocation in normal operation.
- GPU allocation count for an 80×24 terminal drops from ~4 per frame → 0 per frame.
FPS cap
gpu.fps_limit(default 60 FPS) controls the frame rate.- Set to 0 for uncapped (vsync only).
ASCII glyph pre-warming
- ASCII printable characters (0x20–0x7E) are pre-loaded into the glyph atlas at startup in both Regular and Bold.
- Eliminates first-keystroke rasterization latency.
Launcher startup time optimization
- Changed
wait_for_serverpolling to exponential backoff (10 ms, 10 ms, 10 ms, 20 ms, 50 ms, 100 ms). - Average server-ready detection time reduced from 100 ms → ~30 ms when the server starts quickly.
Fix ad-hoc codesign failure on macOS Intel builds
- Signing individual binaries before signing the whole app bundle caused a subcomponent error.
- Changed to a single
codesign --force --deep --sign - dist/Nexterm.appfor the full bundle.
Floating panes
- Added
OpenFloatingPane/CloseFloatingPane/MoveFloatingPane/ResizeFloatingPaneIPC commands. - Added
FloatRectcache andfloating_pane_rectsfield to the GPU client.
WASM plugin system
- New
nexterm-plugincrate (wasmi 0.38-based sandboxed WASM runtime). - Built-in plugin API:
nexterm_on_output,nexterm_on_command; host imports:nexterm.log,nexterm.write_pane. - Added
plugin_dir/plugins_disabledfields to config.
Status bar widget enhancements
- Built-in widgets:
"time","date","hostname","session","pane_id". - Added
right_widgets(right-aligned) andseparatorfields toStatusBarConfig. WidgetContextnow passes session name and pane ID to widgets.
Linux packaging
linux/AppRun: AppImage entry-point script.pkg/flatpak/: Flatpak manifest + AppStream metadata.- Added AppImage build and upload step to GitHub Actions.
.github/workflows/flatpak.yml: dedicated Flatpak build workflow.
Test coverage improvements
- Total test count: 145 → 178 (+33 tests).
- New tests in: nexterm-proto, nexterm-client-tui, nexterm-vt, nexterm-config, nexterm-plugin.
Four new built-in color schemes (Catppuccin / Dracula / Nord / One Dark)
- Added
Catppuccin,Dracula,Nord, andOneDarktoBuiltinSchemeinnexterm-config. - Defined full fg/bg/ANSI[16] color palettes for all 9 schemes; reflected in the GPU renderer's terminal drawing.
- Settings panel (
[Colors]tab) expanded to show all 9 scheme dots.
Shell completion script generation
- Added
nexterm-ctl completions <shell>command. Outputs completion scripts for bash / zsh / fish / powershell / elvish to stdout.
Man page generation
- Added
nexterm-ctl mancommand. Outputs a troff-format man page to stdout (nexterm-ctl man > nexterm-ctl.1to save).
Bracketed paste mode (DEC ?2004)
- VT parser now interprets
CSI ?2004h/CSI ?2004lto track bracketed paste mode. - When the mode is active, pasted text is wrapped with
ESC[200~…ESC[201~before sending to the PTY. Prevents accidental command execution in zsh, fish, vim, and other shells/editors.
Auto-load ~/.ssh/config
- Host Manager (
Ctrl+Shift+H) now parses~/.ssh/configat startup and merges entries with[[hosts]]. Host *wildcards are excluded. Duplicate entries (same host + port already innexterm.toml) are suppressed.
Vim-compatible copy mode keys
w/b: word-wise forward / backward movement.$: jump to end of line.Y: yank the entire current line and exit copy mode./: incremental search mode (Enter to confirm, n for next match, Esc to cancel).
OSC 8 hyperlink support
- Added
Grid.hyperlinks: Vec<HyperlinkSpan>tonexterm-proto. - VT parser interprets
ESC ] 8 ; ; <url> BEL…ESC ] 8 ; ; BELand records spans in the grid. - GPU client's URL click (
Ctrl+Click) now detects OSC 8 links first.
Tab/pane activity notification
- When output arrives in an unfocused pane, its tab shows an orange background and a
●indicator.
Mouse reporting (SGR ?1006 / X11 ?1000)
- VT parser interprets
CSI ?1000h/CSI ?1006hto track mouse modes. - GPU client mouse clicks and drags are sent to the PTY as SGR escape sequences.
- Added
ClientToServer::MouseReportmessage tonexterm-proto.
Scrollback search UI completed
- Added
Scrollback::search_prev().Shift+EnterorShift+Nmoves to the previous match. - Improved search bar UI: cursor
|, accent line, key hint display.
OSC 133 semantic zones
- VT parser interprets
ESC ] 133 ; A/B/C/D BELto track prompt / command / output boundaries. - Exit code of a completed command (D mark) is shown in the status bar (non-zero only).
- Added
ServerToClient::SemanticMarkmessage tonexterm-proto.
Profiles (named configuration sets)
- Added
Profilestruct andConfig.profiles/Config.active_profiletonexterm-config. Profilecan override font, colors, shell, scrollback, and tab bar from the base config.Config::effective()returns the config with the active profile applied.Config::activate_profile(name)/clear_active_profile()control profile switching.
nexterm-client-gpu: Settings panel scheme selector now supports all 9 schemes.
nexterm-vt: added bracketed paste mode enable/disable tests; OSC 8 hyperlink and OSC 133 semantic zone tests (18 tests total).nexterm-server: added BSP 4-split layout, session management API, and SSH config parser tests.nexterm-config: added profile application and TOML parse tests (17 tests total).
Windows — GPU client font rendering fixed
- Replaced the
cell_w = font_size * 0.6fixed-ratio heuristic with actual advance width measurement by rasterizing the reference character'0'at runtime vialayout_runs(). Eliminates extra spaces between characters ("Wi ndows Power She l l"). - Added
scale_factor: f32toFontManager::new(); passeswindow.scale_factor()from winit so the physical font size is correctly computed for high-DPI displays (125 %, 150 % scaling). - Fixed a negative-coordinate wrap bug (
x as u32) in therasterize_charclosure; addedif x < 0 || y < 0 { return; }guard. WindowEvent::ScaleFactorChangedis now handled: font and glyph atlas are automatically regenerated on DPI change.
Windows 11 — Acrylic frosted-glass background
- Calls
DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE, DWMWCP_ACRYLIC)to apply a frosted-glass effect to the window background, similar to Windows Terminal. - wgpu Surface composite alpha mode set to
PreMultipliedfor correct transparent blending. - No effect on Windows 10 or non-Windows platforms; code is
#[cfg(windows)]-guarded.
Windows — console window no longer appears on launch
Added #[windows_subsystem = "windows"] (release builds only) to nexterm.exe,
nexterm-server, and nexterm-client-gpu. Launching nexterm.exe from the MSI installer
or Explorer no longer opens a stray black console window.
- Logs are written to
%LOCALAPPDATA%\nexterm\nexterm-server.log/nexterm-client.logwith daily rotation (tracing-appender). - Errors are reported via
MessageBoxWdialogs.
macOS — binaries are ad-hoc signed + Intel Mac support
- All macOS release binaries are now signed with
codesign --sign -(ad-hoc).xattr -dr com.apple.quarantine <file>is all that's needed to bypass Gatekeeper. - Built
x86_64-apple-darwintarget on themacos-13(Intel) runner;nexterm-vX.Y.Z-macos-x86_64.tar.gzis now included in release assets.
This patch release fixes compilation and test failures that prevented the Windows binary from being produced in the v0.5.0 release workflow.
| # | Crate / file | Root cause | Fix |
|---|---|---|---|
| 1 | nexterm-launcher/Cargo.toml |
windows-sys 0.59 split CreateFileW security descriptor handling into a separate Win32_Security feature; the feature was missing from the dependency declaration |
Added "Win32_Security" to the windows-sys features list |
| 2 | nexterm-launcher/src/main.rs |
GENERIC_READ was imported from Win32::Storage::FileSystem; in windows-sys 0.59 it was moved to Win32::Foundation |
Moved GENERIC_READ (and INVALID_HANDLE_VALUE) to the Win32::Foundation use statement |
| 3 | nexterm-server/src/pane.rs |
portable_pty imports were guarded with #[cfg(unix)], preventing MasterPty, NativePtySystem, PtySize, and CommandBuilder from being compiled on Windows even though portable_pty supports ConPTY on Windows |
Removed the #[cfg(unix)] attribute from the portable_pty use statement |
| 4 | nexterm-server/src/ipc.rs |
Path-validation unit tests used Unix-style absolute paths (/home/user/…, /etc/passwd, /tmp/…) which are not recognised as absolute by std::path::Path::is_absolute() on Windows, causing the "reject forbidden absolute paths" test to pass silently for the wrong reason |
Added #[cfg(unix)] / #[cfg(windows)] guards; Windows tests use %TEMP%\nexterm\… and D:\secret\… / C:\Windows\System32\… style paths |
All 93 unit tests now pass on x86_64-pc-windows-msvc.
SSH & Connectivity
- SSH multi-tab connections — SSH Host Manager (
Ctrl+Shift+H) opens each host in a new tab - X11 forwarding —
x11_forward = true/x11_trusted = truein[[hosts]](equivalent tossh -X/ssh -Y)
UX
- In-app Settings GUI —
Ctrl+,opens a Font / Colors / Window panel; changes write back tonexterm.tomlinstantly - Settings action added to command palette (now 17 actions)
Web Terminal
- Embedded web terminal —
[web] enabled = true; xterm.js served atws://localhost:7681 - Token-based auth (
token = "..."in config), disabled by default
Package Distribution
- Homebrew tap formula (
pkg/homebrew/nexterm.rb) - Scoop bucket manifest (
pkg/scoop/nexterm.json) - winget manifest (
pkg/winget/mizu-jun.Nexterm.yaml) - GitHub Pages documentation site auto-deployed via CI
SSH & Connectivity
- SSH Host Manager — fuzzy-searchable host list (
Ctrl+Shift+H); connects with one keystroke - SFTP Upload / Download dialogs (
Ctrl+Shift+U/Ctrl+Shift+D) with live progress bar - Remote port forwarding (
-R) over SSH sessions - Serial port connections (
ConnectSerialvia command palette)
UX & Pane Management
- Command palette (Ctrl+Shift+P) extended with 16 actions including SFTP and host manager
- Lua Macro Picker — fuzzy-searchable macro list (
Ctrl+Shift+M); one-key execution - Quick Select mode (
Ctrl+Shift+Space) — highlight URLs, paths, IPs, and hashes - Pane zoom toggle (
Ctrl+B Z) — focus a single pane full-screen - Swap pane with next/previous sibling (
Ctrl+B {/Ctrl+B }) - Break pane to new window (
Ctrl+B !)
Automation
- Lua event hooks:
on_session_start,on_attach,on_pane_open - Lua Macro engine: define
[[macros]]in TOML, execute via picker
Logging
- Log filename templates (
{session},{date},{time}placeholders) - Binary PTY log mode
Windows
- MSI installer built with WiX Toolset v3 (CI-automated)
- Windows Service install/uninstall scripts
- Automatic code signing via
signtool.exewhen CI secrets are configured nexterm-launcher— singlenexterm.exeauto-starts server + opens GPU client
SSH & Security
- Known-hosts host key verification
- SSH agent authentication via
SSH_AUTH_SOCK - Local port forwarding through SSH tunnels
- ProxyJump multi-hop connection support
- SOCKS5 proxy support
Terminal & Display
- Full alternate screen buffer support (SMCUP/RMCUP)
- OSC 0/1/2 window title support
- OSC 9 desktop notifications
- CJK wide character rendering fixes
GPU Client
- IME input support (Japanese, Chinese, Korean)
- Keybinding customization
- Right-click context menu (Copy/Paste/Split/ClosePane)
- Pane number overlay in display-panes mode
- Mouse selection with automatic clipboard copy
- GPU-accelerated renderer using wgpu + cosmic-text
- Command palette (
Ctrl+Shift+P) with initial 8 actions - Split pane: horizontal (
Ctrl+B %) and vertical (Ctrl+B ") - Scrollback buffer with configurable history size
- Basic session save / restore (JSON snapshots)
- Initial release
- TUI client (
nexterm-client-tui) using ratatui + crossterm - IPC protocol between server and client (
nexterm-proto) - VT parser (
nexterm-vt) with ANSI/xterm sequence support - SSH client (
nexterm-ssh) viarussh - TOML configuration (
nexterm-config) - i18n support for 8 languages (
nexterm-i18n) nexterm-ctlCLI for session management