Releases: benyblack/ntilde
Release list
v0.11.0
0.11.0 makes Ntilde feel native on macOS, where Cmd now does what Cmd does everywhere else,
and closes several holes where program output or clipboard content could do more than it
should: a crafted paste can no longer end the paste block early, a file: link is never
executed, and a Sixel image can no longer freeze a pane. It also fixes character widths for
emoji and CJK, sends Ctrl/Shift/Alt with arrow and function keys, stops SSH jump chains from
sending a password to the wrong host, and moves to Avalonia 12.1.
Installing and upgrading
Already running Ntilde (or NovaTerminal) from the Windows installer, the macOS .pkg or
the AppImage? Nothing to do. The update is found in the background and applied on your next
restart.
Installing fresh:
- Windows —
ntilde-Setup-win-x64-v0.11.0.exe. Per-user, no admin prompt, self-updating.
ntilde-win-x64-v0.11.0.zipis the portable build, with no updater. - macOS (Apple Silicon) —
ntilde-Setup-osx-arm64-v0.11.0.pkg, signed and notarized.
ntilde-osx-arm64-v0.11.0.zipholds the sameNtilde.appfor a drag install. - Linux —
ntilde-linux-x64-v0.11.0.AppImage(or-arm64), self-updating;
ntilde_0.11.0-1_amd64.deb(or_arm64);ntilde-linux-x64-v0.11.0.tar.gz(or-arm64),
portable. glibc 2.35 or newer. - Arch —
PKGBUILD-v0.11.0andSRCINFO-v0.11.0are attached;makepkg -sibuilds
ntilde-bin.
macOS: Cmd is the shortcut key
- Cmd+C / Cmd+V copy and paste. Cmd+C keeps the selection highlighted, as in Terminal.app
and iTerm2. - App shortcuts moved from Ctrl to Cmd: Cmd+T new tab, Cmd+W close tab, Cmd+, Settings,
Cmd+F find, Cmd+Shift+P command palette, and so on. - Ctrl is the shell's again. Ctrl+C is always the interrupt, even with a selection, and
Ctrl+R, Ctrl+W, Ctrl+F and Ctrl+V reach readline instead of triggering app commands. - A few keep Ctrl because macOS owns the Cmd form: Ctrl+Tab / Ctrl+Shift+Tab to switch
tabs (Cmd+Tab is the app switcher) and Ctrl+Space for Command Assist (Cmd+Space is
Spotlight). - Custom shortcuts can use
Cmd, and the Settings recorder captures it. Bindings you already
saved are used exactly as written. Windows and Linux shortcuts are unchanged.
Security
- Bracketed paste can no longer be broken out of. A clipboard snippet could assemble a
paste-end marker that survived the old filter, so text after it arrived as typed input and
could run commands. Every ESC and 8-bit CSI is now removed from pasted content, which leaves
no terminator in any spelling. file:links are never executed. A link's visible text can differ from its target, so a
Ctrl+click on what looked like a web link could run a local program or, on Windows, connect to
a remote SMB share and offer your credentials. Local file links now only reveal the file in
Finder, Explorer or your file manager; remote ones are refused, and are no longer underlined.
Links that name this machine's own hostname (asls --hyperlink,rg,fdandezawrite
them) are treated as local.- SSH jump chains send each password only to its own host. With a password-auth bastion, the
target's saved password could be sent to the bastion, the bastion's password replayed to the
target, and SFTP could send one password to every hop. Each prompt now names the host asking,
and passwords are kept per host. - Sixel images are bounded. A 20-byte sequence with a huge repeat count could freeze a pane
and exhaust memory. Images are now capped at 2000 pixels per side, the same limit as kitty and
iTerm2 images. - On Linux,
xdg-openis started from its absolutePATHlocation, so a copy in the current
directory cannot stand in for it.
Fixes
- Character widths come from Unicode data. Emoji such as ✅ ❌ ⚡ ☕ ⭐ are now two cells, as glibc, Node and Rust measure them, and
narrow symbols (legacy-computing sextants, alchemical symbols, arrows, chess) are no longer
drawn wide, so TUIs stop misdrawing lines that contain them. Emoji with VS16 widen only when
Unicode says they should, and keycaps such as 1️⃣ now take two cells. - Modifiers reach arrow, editing and function keys. Ctrl+Left/Right jumps words in bash,
zsh and PowerShell, Shift+arrows select in editors, and Ctrl+Delete deletes a word, using
xterm's encoding. - Theme import: importing a native Ntilde theme
.jsonworks again (it silently did
nothing), and a theme whose name contains/,\or:imports, saves and deletes cleanly. - Quake mode (Windows): hiding and showing the window no longer leaks a hotkey hook, which
could crash the app after a garbage collection. - Captures and exports (agent snapshots, PNG export) no longer leave stale cells in the live
view.
Under the hood
- Avalonia 12.0.4 → 12.1.3.
NTILDE_RENDER_METRICSand the render HUD now show which Skia backend drew each frame
(for exampleGPU/OpenGL, orSoftwarewhen there is no GPU context). The metrics file is flushed on exit and never
ends in a half-written line.
Full Changelog: v0.10.0...v0.11.0
v0.10.0
NovaTerminal is now Ntilde. 0.10.0 is the first release under the new name: same
terminal, new binaries, new package names, ntilde on the command line. It also adds an
Interface scale that zooms the whole window independently of the terminal font, fixes the
lag that crept into a long-lived SSH tab, implements REP so ncurses borders and rules are
drawn at full length, and lets the SSH connection dialog scroll instead of clipping its form.
Installing and upgrading
Already running NovaTerminal from the Windows installer or the macOS .pkg? Nothing to
do. Your install keeps checking the same update feed, and the next update it finds is this
one. It applies in place on your next restart: the install folder stays where it is, the
shortcuts are renamed to Ntilde, and the app comes back as Ntilde with your settings already
migrated. (The spec for the rename assumed the old updater would not follow across the
name change. It does, and this was verified against a real 0.9.0 install before tagging.)
Running the AppImage? Same story. The updater rewrites the AppImage in place, so the file
keeps whatever name it has on disk; rename it when you like.
Installing fresh:
- Windows —
ntilde-Setup-win-x64-v0.10.0.exe. Per-user, no admin prompt, self-updating.
ntilde-win-x64-v0.10.0.zipis the portable build, with no updater. - macOS (Apple Silicon) —
ntilde-Setup-osx-arm64-v0.10.0.pkg, signed and notarized.
ntilde-osx-arm64-v0.10.0.zipholds the sameNtilde.appfor a drag install. - Linux —
ntilde-linux-x64-v0.10.0.AppImage(or-arm64), self-updating;
ntilde_0.10.0-1_amd64.deb(or_arm64), which declaresReplaces/Conflictson
novaterminalsoapt install ./the-file.debupgrades the old package cleanly;
ntilde-linux-x64-v0.10.0.tar.gz(or-arm64), portable. glibc 2.35 or newer. - Arch —
PKGBUILD-v0.10.0andSRCINFO-v0.10.0are attached;makepkg -sibuilds
ntilde-bin, which provides, conflicts with and replacesnovaterminal-bin.
Still on a portable zip or tarball? Download the new one and delete the old copy. Portable
builds have no updater and cannot bring themselves forward.
What the rename means for you
Everything user-visible is renamed; the things that would cost you data are kept compatible.
Settings carry over. On first launch Ntilde copies settings, themes, connection profiles,
workspaces, snippets, backups and history from the NovaTerminal data folder into its own
(%LOCALAPPDATA%\ntilde on Windows, ~/.local/share/ntilde elsewhere). Newer file wins,
logs is skipped, the old folder is left untouched, and a marker file stops it running
twice. The marker is written only when every copy succeeded, so a transient failure retries
on the next launch instead of leaving one file permanently behind.
Saved passwords do not. Keychain, Credential Manager and Secret Service entries live under
the new name. Re-enter SSH passwords once; the profiles themselves are already there.
Old files still open. Backups now export as .ntildebackup, workspaces as
.ntildews.json, recordings with an ntilderec header, and the readers still accept
.novabackup, .novaws.json and novarec. Existing snapshots stay listed in Backup &
Restore.
The command is ntilde (was nova). Environment overrides are NTILDE_* (was
NOVATERM_*), for example NTILDE_APPDATA_ROOT. MCP tools are prefixed ntilde..
Remote shell integration writes ~/.ntilde-shell-integration.sh (or .ps1). Re-run the
installer from Settings on each host, and remove the old ~/.nova-shell-integration.* loader
line from your rc file or $PROFILE by hand.
A new icon. The ñ mark replaces the old one everywhere it is consumed: the Windows ICO,
the macOS .icns, the Linux hicolor sizes and AppImage icon, the site favicon and social
card. All derive from one master PNG, so the packaging lanes did not change.
Interface scale
Font size only ever reached the terminal. On a dense display the tabs, sidebars, dialogs
and Settings itself stayed small however large the terminal font got, and roughly 140
hardcoded font sizes across the UI meant "scale the fonts" was never going to work. 0.10.0
takes the route Warp and VS Code took: one zoom for the whole window, separate from the
terminal font.
Settings → Appearance → Window → Interface scale, 80% to 200%, with live preview. Ctrl+=
and Ctrl+- keep zooming the terminal font only.
Terminal text stays sharp. The terminal rasterizes its glyph atlas at the monitor's
render scale, and under a plain layout transform those bitmaps were then enlarged, so at 150%
terminal text was visibly soft while the tab title beside it stayed crisp. The terminal now
folds the interface scale into its render scale and rebuilds the atlas, the same path a DPI
change takes, and it reads the scale from its own window, because a window can render at a
different scale than the app (the Settings window holds the scale it opened with so the
slider does not run away from the pointer; a window the screen cannot fit renders smaller).
Fixed-size dialogs fit. Settings, the SSH connection dialog, the Connection Manager,
About, Replay, transfers, prompts and the askpass helper are sized for the scale in effect,
reduced to what the screen can hold rather than clipped, refit when the scale changes from
their current size so your own resizes survive, and left alone while maximized. A dialog
that lands on a smaller secondary monitor is re-checked against that monitor once it opens.
The askpass helper follows. OpenSSH launches it as a separate process with its own tiny
application, so it never saw the app's theme. It now installs the same window theme and reads
the saved scale from settings.json, falling back to 100% on any problem so authentication
is never blocked by a settings file.
Kitty in-band resize reports true pixels. The mode 2048 report multiplied the grid by
the cell size in DIPs, which is only pixels at 100% on a 96-dpi monitor. It now uses the
effective render scale, monitor times interface scale, so a terminal browser is told the
pixels the terminal actually occupies.
Known gap: popups (context menus, flyouts, tooltips) live in their own top-levels and stay
at 100% for now.
SSH: the tab that got laggy after a day
Field report from 0.7.0: an SSH tab left open for about 24 hours on an Apple Silicon Mac
became laggy, with a suspected memory leak. The VT core was soak-tested first, 127k lines
through the real parser at 220×50 with alt-screen cycles, marks, emoji and links: memory
saturates at 32 MB and stays flat. The leak was not in the buffer. Two defects, only on the
SSH path, compounded instead.
REP (CSI Ps b) was unimplemented. We advertise TERM=xterm-256color, whose terminfo
declares rep, so ncurses on the far end uses it to draw every run of repeated characters:
box borders, rules, padding, meter bars. Measured: - followed by CSI 39 b drew one cell
instead of forty. Every such run also fell through to the "unhandled CSI" diagnostic. REP
is implemented per ECMA-48: it repeats only the immediately preceding graphic character, the
repeatable character is tracked in its own state (the cell under the cursor cannot say what
was last written), a control or cursor move clears it, a surrogate half or combining mark
yields no repeat rather than half a grapheme, the count is clamped to one line, and the
charset-mapped glyph is what repeats, so DEC special graphics works.
Every unhandled sequence wrote to disk, synchronously, from the parse thread. The app
logger was an open-append-flush-close per call under a process-global lock, with no size cap
and no level filter. One 50-row ncurses redraw became about 150 synchronous disk writes,
serialized against the UI and render threads, and debug.log grew for the life of the
process at 1 to 10 GB a day.
| parse cost per frame | debug.log growth |
|
|---|---|---|
| 0.9.0, logging sink wired | 1.24 ms | 1–10 GB/day |
| 0.9.0, sink disabled | 0.23 ms | — |
| 0.10.0 | 0.82 ms | bounded at 16 MB |
The 0.82 ms is not like-for-like with 0.23: it now draws the 6000 cells per frame it used
to discard. Three layers replace the old sink: a minimum level (NTILDE_LOG_LEVEL, default
Info) so per-sequence diagnostics are filtered by an enum compare before any work happens;
rate limiting by message shape in front of every parser call site, with errors never muted
and a periodic report of what was swallowed; and a rotating writer with a bounded queue, one
background thread, 16 MB rotation and a drain on exit. A log storm now costs dropped lines,
counted and reported, not memory or latency.
The per-chunk UI refresh is coalesced. The pane posted a dispatcher job for every chunk
read from the session, so the queue grew with the remote's send rate rather than the UI's
drain rate, each closure doing the same work as the one ahead of it. Everything that job does
is a level, not an edge, so one pass after the last chunk is all that was ever needed. A
single-flight flag guarantees it, cleared at the start of the pass so a chunk arriving
mid-pass queues the next one instead of going unshown.
The SSH connection dialog scrolls
The Basic tab of New/Edit Connection has eleven form rows plus a Notes box inside a bare
grid, so at the default height the bottom rows (Favorite, Notes) were cut off with no
scrollbar; the only way to reach them was to resize the window by hand. Every tab now scrolls,
the footer with warnings and Cancel/Save/Connect stays pinned, and the default size grew to
760×680 with a 640×480 minimum so the common case fits without scrolling.
Also in this release
- The Homebrew tap lane is in the repo, secret-gated in the release workflow. The tap itself
is not live yet; the README will say when it is. - Debian, Arch and Velopack packaging carry the new identity, and the Velopack pack ID
(NtildeApp) de...
v0.9.0
What's Changed
- site: bring the Install section and feature list up to date for 0.8.0 by @benyblack in #396
- fix(assist): apply the OSC 133 lifecycle gate on the thread the bytes arrive on by @benyblack in #448
- fix(security): resolve the SonarCloud code-scanning alerts by @benyblack in #421
- ci(release): stop untrusted code writing the release caches by @benyblack in #450
- fix(app): enable X11 drawn decorations so Linux gets caption buttons by @benyblack in #451
- fix(app): measure the caption-button reserve instead of hardcoding 140px by @benyblack in #452
- feat(vt): kitty graphics end-to-end — native APC on Windows, t=f transport, o=z, raw RGBA, in-band resize, CSI 14/16 t by @benyblack in #449
- feat(arch): gated novaterminal-bin AUR package (#385) by @benyblack in #455
- fix(input): smooth wheel gesture reaches its target and survives live output by @benyblack in #456
- fix(release): create the tag on the commit being built by @benyblack in #457
- fix(vt): make the kitty snapshot-gate test ordering-determined, not clock-determined by @benyblack in #458
- fix(tests): make the tab-overflow precondition hold by construction, not by 3px by @benyblack in #462
- fix(tests): keep AgentHost socket paths under the macOS sun_path limit by @benyblack in #461
- ci: run the unit test suite on linux-arm64, nightly and on dispatch by @benyblack in #459
Full Changelog: v0.8.0...v0.9.0
v0.8.0 — Markdown agent output, an About window, icon glyphs out of the box
NovaTerminal 0.8.0 gives agent output a reader: a side panel that renders what an agent
printed as markdown, with fenced code, tables and diffs, instead of as a wall of terminal
text. It also adds an About window with a working update check, bundles the icon glyphs a
fresh install was missing, and fixes a command palette that rendered blank in released
builds — along with the release gate that would have caught it.
Installing and upgrading
On Windows? If you installed with NovaTerminal-Setup-win-x64-*.exe, 0.8.0 arrives on
its own — check for the update toast and it applies on your next restart. You can also ask
now, from About NovaTerminal… in the title-bar flyout.
On macOS? Same, via NovaTerminal-Setup-osx-arm64-v0.8.0.pkg. Signed and notarized, as
of 0.7.0.
On Linux? Three ways, unchanged from 0.7.0 — pick one:
NovaTerminal-linux-x64-v0.8.0.AppImage(or-arm64) —chmod +xit and run.
Self-updating, like the Windows and macOS builds.novaterminal_0.8.0-1_amd64.deb(or_arm64) —sudo apt install ./the-file.deb.
Desktop entry, icons,novaon yourPATH, a man page. No APT repository yet, so it
does not update itself — download and reinstall each release.NovaTerminal-linux-x64-v0.8.0.tar.gz(or-arm64) — portable, no integration, no
updater.
Which Linux distros? Ubuntu 22.04+, Debian 12+, Fedora 36+, and current rolling
releases. The floor is glibc 2.35.
Still on a portable zip or tarball? Run an installer once by hand. Portable builds have
no updater and cannot bring themselves forward.
Agent output, rendered as markdown
Coding agents print markdown into a terminal that cannot render it. 0.8.0 adds a side panel
that does, opened per pane, over the output you already have.
It shows the latest response, not the whole pane. A long agent conversation is rounds of
prompt → response on one grid, so trimming the raw tail cannot describe "the response" —
prompts are interleaved throughout and the tail is a fragment of several rounds at once.
Prompt-shaped lines act as segment boundaries, and the panel renders the last segment with
content. A mis-split degrades to a later fragment of the current answer, never to an earlier
round.
Prompt lines are trimmed off both ends, so opening the panel after a command finished
does not show you the prompt that launched it and the one that followed. PowerShell PS …,
cmd drive paths and POSIX user@host shapes are all recognised, with or without the
echoed command riding the line. Prompt-shaped lines inside a response are left alone.
Fenced blocks get handlers. A diff fence colours lines by leading marker, testing the
three-character file headers before the one-character ones so +++ b/file is a header and
not an addition. A markdown fence recurses back through the renderer's own block walk, so
every block type the panel supports works inside a fence — capped at one level, so a fence
inside a rendered fence stays source. Copy always yields the raw source, whatever the block
rendered as.
The md toggle appears only when there is something to toggle. While the panel is
closed, a lazy cadence looks for distinct structural signals in the recent output — fences,
ATX headings, GFM table delimiters, task lists — and the toggle shows or hides as the verdict
changes. Two strong signals, or one plus a couple of list lines, is the bar; a log separator
or a stray # never triggers it.
An About window, and an update check that answers in place
There was no way to see which version you were running, and the manual update check existed
only as a palette command that answered with a toast.
About NovaTerminal… in the title-bar flyout now shows the icon, name and version, with
the update check inline: it reports the answer in the window, and offers Restart now when
an update is staged. Non-installer builds get a link to the releases page instead, because
they cannot update themselves.
Both surfaces run through one coordinator and share the in-flight guard and announce-once
state, so a check from About cannot race the palette's or the one at startup, and the wording
is centralized so the two cannot drift.
Icon glyphs out of the box
A fresh install had no icon glyphs. The one bundled font covered ASCII, box drawing and
powerline — so starship and oh-my-posh arrows worked — but not the dev, file-type and brand
icons those prompts also draw.
Three fonts now ship:
| font | size | role |
|---|---|---|
| JetBrains Mono NL | 204 KB | new default terminal face |
| Cascadia Mono PL | 360 KB | kept |
| Symbols Nerd Font Mono | 2,549 KB | icon glyphs, fallback only |
That follows WezTerm rather than Ghostty: a single shared symbols font is paid for once and
serves every face — including a system font you pick yourself — where per-style patched faces
cost about the same while serving icons only to whoever selected that one font.
The command palette rendered blank in released builds
Reported from a fresh Windows install: the command palette opened with blank rows. Not an
empty list, not an error — rows with nothing in them.
The palette's item template was the only place in the app that opted out of compiled
bindings, so its two bindings resolved their paths by reflection at runtime. Releases publish
with NativeAOT, and ILC had already trimmed both property getters away, because those two
XAML strings were their only readers in the whole program. The bindings then produced nothing
at all: no exception, no log line, just blank rows. It could not reproduce in any dev build,
which are JIT with full metadata.
Releases now have to start before they ship
That palette bug is the kind that only exists in the artifact users download, so 0.8.0 also
closes the gap that let it through.
Trim and AOT warnings now fail the publish. IL2026 and IL3050 are errors during the
release publish, so the class of break behind the palette bug cannot reach an asset.
The Windows bundle is started before it is archived. Every user-facing step — the release
asset, the installer, the update feed — is now downstream of a smoke launch that runs the
published bundle from a pristine profile and asserts it wrote no startup error, stayed alive,
got managed Main through startup, brought up a themed terminal view, and spawned a shell
with a live child process. The natives it loads are the prebuilt ones the release ships, not
freshly compiled copies, so this is the only place the shipped combination is ever executed.
A bundle that cannot start now fails the tag instead of being published.
Linux has had an equivalent gate since 0.7.0. macOS does not yet — launching a .app on a
macOS runner is a different problem, and it is deliberately left for a follow-up rather than
guessed at.
Terminal correctness
Splitting a pane no longer blanks it. The pane being split loses about half its height,
and the shrink path used to take every row it needed off the top of the viewport — so a
transcript in the top half was evicted into scrollback while the empty bottom half was kept,
and the pane came back with a lone prompt. Empty rows below the input line are spent first
now, which in the ordinary case means nothing reaches scrollback at all.
Sixel probes no longer scroll the screen. A control sequence is named by its final byte
together with its leader and intermediate bytes, and the CSI dispatch keyed on the final byte
alone for most cases. DA1 advertises sixel, so sixel-capable clients probe XTSMGRAPHICS
as a matter of course — and every probe ran the bare meaning of S, scrolling the screen.
Text after an inline image starts at column 0. All three image protocols reserve the
picture's cells and stopped without a final newline, leaving the cursor at the end of the
image's last row; whatever the program printed next was indented, or overran the last column
and wrapped mid-word. iTerm2 compounded it by eating the newline the shell sent to escape.
Agent screenshots keep glyphs the primary font lacks. capture_screen rendered notdef
boxes where the live pane showed the glyph, because per-codepoint font fallback was consulted
on only one of the two draw paths.
Typing while scrolled up returns you to the input line. Keystrokes, typed text, paste and
drag-drop all snap the viewport back to the live line first, including in panes receiving
broadcast input. Output keeps the existing stay-put rule, and non-writing sends — focus
reports, mouse reports, device replies, copy — do not scroll.
Also in this release
capture_screengains alivemode that carries the pane's background image and window
opacity, and a caller-namedscalefrom 1 to 3 so text stays readable in a capture.- SSH path resolution honours
NOVATERM_APPDATA_ROOT. - A pane refuses a degenerate size instead of clamping it to 1×1.
- Resizes are recorded where they are dispatched, and a resize cancelled before it ran is
re-sent. - A large reliability pass on the headless test lane, so a run that is cut short or aborted
fails the gate instead of reporting green.
v0.8.0-rc.1
What's Changed
- fix(tests): contain the Avalonia platform boot in its own test process by @benyblack in #395
- feat(agent-output): render command output as markdown in a side panel by @benyblack in #378
- feat(agent-output): act on a fenced block's language — nested markdown and diff colouring by @benyblack in #397
- docs: correct stale facts in README and the main docs, and document what shipped by @benyblack in #400
- fix(tests): run the AgentOutput suites on the headless session thread by @benyblack in #399
- feat(agent-host): capture_screen gains live mode and scale, and drops its own gate by @benyblack in #401
- feat(fonts): bundle JetBrains Mono NL as the default, with a symbols-only Nerd Font fallback by @benyblack in #402
- Return viewport to the input line on user input by @benyblack in #377
- fix(agent-host): capture_screen lost every glyph the primary font lacked by @benyblack in #403
- fix(tests): put the last snapshot-path booter in the PlatformBoot lane, and make the guard see transitive boots by @benyblack in #409
- ci: fail the headless lane when a run is cut short, instead of summarising it by @benyblack in #410
- fix(vt): return the cursor to column 0 after an inline image by @benyblack in #407
- fix(ssh): honour NOVATERM_APPDATA_ROOT when resolving SSH paths by @benyblack in #408
- fix(agent-output): contain the debounce tick, which was aborting CI collections by @benyblack in #411
- fix(vt): stop the post-image newline swallow leaking past other output by @benyblack in #413
- test(command-assist): wait on the pass's commit point, not a property it sets early by @benyblack in #415
- ci(app-tests): a runner-level abort now fails the gate, hang or no hang by @benyblack in #414
- feat(app): About window with embedded manual update check by @benyblack in #412
- fix(command-assist): stop a stray pass from claiming Avalonia's UI-thread identity (#81) by @benyblack in #416
- test(app): dispose the windows the tests build, and the shells behind them by @benyblack in #417
- fix(command-assist): make disposal terminal, not just a cancellation by @benyblack in #418
- docs: retract the disproven #81 explanations still recorded in the tree by @benyblack in #420
- fix(app): make process-wide cached brushes immutable by @benyblack in #422
- fix(app): exit zoom before disposing a tab strip that is being replaced by @benyblack in #419
- fix(app): stop TerminalPane reading Avalonia's UI-thread static (#423) by @benyblack in #426
- build: fail a test run that aborted, whatever its summary says (#425) by @benyblack in #427
- test(command-assist): wait on the assist surface, not on a fixed delay (#424) by @benyblack in #428
- build: refuse to run on a toolchain that cannot compile (#347) by @benyblack in #429
- ci(macos): make the dSYM leak assertion unable to pass vacuously (#390) by @benyblack in #430
- fix(app): refuse a degenerate pane size instead of clamping it to 1x1 by @benyblack in #431
- fix(vt): identify a CSI by its leader and intermediates, not its final byte alone by @benyblack in #433
- test(app): make the window-teardown tests' precondition deterministic by @benyblack in #435
- test(app): stop the suite writing a session into the real app-data profile by @benyblack in #436
- fix(session): keep an unhydrated tab's panes when capturing during startup restore by @benyblack in #437
- test(app): assert the preview recompute instead of racing a live shell's output by @benyblack in #439
- fix(vt): spend blank padding before evicting the transcript on a height shrink by @benyblack in #440
- fix(app): record a resize where it is dispatched, not where it is computed by @benyblack in #441
- fix(app): re-send a resize dispatch that was cancelled before it ran by @benyblack in #442
- fix(app): compile the command palette's item bindings so release builds render them by @benyblack in #443
- ci(aot): fail the publish on trim/AOT warnings, and start the Windows bundle once by @benyblack in #444
- ci(release): block the Windows release on a smoke launch of the bundle by @benyblack in #445
- test(command-assist): give the grid-truth harness the pane's dispatcher by @benyblack in #446
Full Changelog: v0.7.0...v0.8.0-rc.1
v0.7.0 — Signed macOS builds, Linux packages, inline images, live themes
NovaTerminal 0.7.0 is the first release Apple has notarized, the first with real Linux
packages instead of a broken zip, and the first where inline images actually render. It
also rebuilds the tab strip for people running several coding agents at once, and makes a
theme change apply the moment you pick it.
Installing and upgrading
On Windows? If you installed with NovaTerminal-Setup-win-x64-*.exe, 0.7.0 arrives
on its own — check for the update toast and it applies on your next restart.
On macOS? Same, via NovaTerminal-Setup-osx-arm64-v0.7.0.pkg — and Gatekeeper will
not warn you any more. See below.
On Linux? Newly supported, three ways — pick one:
NovaTerminal-linux-x64-v0.7.0.AppImage(or-arm64) —chmod +xit and run.
Self-updating, like the Windows and macOS builds.novaterminal_0.7.0-1_amd64.deb(or_arm64) —sudo apt install ./the-file.deb.
Gives you a desktop entry, icons,novaon yourPATH, and a man page. Updates come
from your package manager, so it does not update itself yet.NovaTerminal-linux-x64-v0.7.0.tar.gz(or-arm64) — portable, no integration,
no updater.
Which Linux distros? Ubuntu 22.04+, Debian 12+, Fedora 36+, and current rolling
releases. The floor is glibc 2.35. Debian 11 and RHEL 8/9 will not run this build.
Still on a portable zip or tarball? Run an installer once by hand. Portable builds
have no updater and cannot bring themselves forward. They remain available for anyone who
prefers them.
macOS builds are signed and notarized
This is the first NovaTerminal release Apple has seen. Until now every macOS build was
unsigned, so first launch meant a Gatekeeper dialog and a trip through System Settings to
allow an app "from an unidentified developer".
- The
.appis deep-signed with a Developer ID Application certificate — the bundle
and every dylib inside it, including the Rust native libraries. - The
.pkginstaller is signed with a Developer ID Installer certificate. - Both were submitted to Apple, notarized, and stapled. The ticket travels with the
file, so verification works even offline. The build then runs Apple's ownspctl
assessment on each, and fails the release if either is rejected.
That covers both macOS downloads: the .pkg installer and the portable
NovaTerminal-osx-arm64-v0.7.0.zip, which is the same signed .app in a zip.
Nothing is required of you. Already-installed unsigned copies keep working and update
to 0.7.0 normally; the update just happens to be signed from here on.
Windows installers are still unsigned, so SmartScreen will warn on first run. That
leg is tracked in #91.
Linux gets real packages
Linux used to ship as a single portable zip that was broken two ways at once.
- The binary arrived without its executable bit. The zip was written by
Compress-Archive, which cannot store Unix mode bits at all. - It excluded the most-deployed LTS. The build pinned a glibc floor that Ubuntu 22.04
and Debian 12 could not meet, so the two distros most people are on could not run it.
Both are fixed by replacing it with an auto-updating AppImage, a system-integrated .deb,
and a portable tarball — for x64 and arm64 alike.
The .deb's dependency list was the subtle part: of the nine X11 and fontconfig libraries
the app needs, eight are invisible to ldd, because Avalonia loads them at runtime and
only libfontconfig1 is linked directly. Deriving dependencies the obvious way would have
shipped a package that installs cleanly and then cannot open a window. Nothing reaches this
release page until a smoke gate has installed the .deb and launched the real bundle in
two pristine ubuntu:22.04 containers.
Still to come: a signed APT repository (#383), nova -e and x-terminal-emulator
integration (#384), and Flatpak/AUR/RPM/Snap (#385).
Inline images actually render
Sixel, iTerm2 (OSC 1337), Kitty and tunneled OSC 1339 payloads were documented as
supported and silently decoded to nothing. Everything around the decoder existed —
placement, scrolling, eviction, drawing — but the decoder itself had only ever been wired
up in tests, so in a real terminal every image protocol was a no-op.
It is now wired in every live pane and in replay windows, so a replay renders images the
same way the session did.
Evicted images also release their memory now. Pruning an image used to drop its last
reference and leave the native bitmap to a finalizer; handles are disposed at the render
frame boundary instead, after a grace period long enough that no in-flight frame or agent
screenshot can still be drawing one.
Tabs, for when you are running five agents
The vertical tab strip was serviceable for a few tabs and painful for a dozen. This is the
pass that fixes that.
Drag a tab to reorder it, in either orientation. An insert gap shows where it will
land, the strip auto-scrolls when you drag to its edge, and Escape cancels. A short click
still just selects — dragging starts only after about 5 DIP of movement. Reordering reuses
the existing tabs, so your panes, sessions and recent-tab order are untouched, and the new
order persists.
Status markers moved out of the title. The bell, activity, agent-typed and
agent-reading markers used to eat into a title that was already truncated; they are now
trailing chips, and the title gets its full width back. Tooltips and the tab list still
spell them out.
One dot tells you the state, with a clear precedence: needs attention, then
agent-typing, then running, then agent-reading. The colors match the agent segment inside
the pane.
A thinking agent no longer looks idle. The old read on "is this tab busy" was a
two-second output heuristic, so an agent that had gone quiet to think decayed to idle
between bursts. Running state now comes from the agent-session registry directly.
Overflow and keyboard moves. A "+N more" pill sits at the bottom of the vertical
sidebar and opens the tab list. Ctrl+Shift+PageUp / Ctrl+Shift+PageDown move the
current tab without the mouse, and both are in the command palette as "Tab: Move
Previous/Next".
The selected vertical tab also gets a 3px left accent bar. Horizontal tabs keep their
underline.
Themes apply when you pick them
Switching themes used to need a restart, and even then parts of the screen kept the old
palette. Several separate causes, all fixed:
- Cached rows were serving pre-switch colors forever. Scrollback snapshots were pinned
as immutable, so the cell and row caches never noticed a theme change — and a frame in
flight during the switch could re-seed stale colors after the caches were cleared.
Snapshots now carry a theme epoch that invalidates both. - Applying a theme was quietly making colors explicit. The buffer's live style state
resolved "default foreground" into a literal color and dropped the flag that said it
was the default. Every cell written afterwards stored a fixed color that no later theme
switch could move, and the state itself stayed pinned to the old palette. This was the
root of the whole "one switch behind" family of bugs. - Panes nested inside scroll and border wrappers were skipped entirely when settings
were applied, so they got neither the new theme nor new fonts. - Title-bar foregrounds were being discarded by the rebuild that ran right after the
theme was applied — and the tab-list button was repainted white on every layout pass,
which is why that one icon vanished on light themes.
Light themes work on the Settings page. The title-bar item cards, shortcut binding
cards and Command Assist snippet rows had dark backgrounds hardcoded. They now follow the
theme, along with the pill, icon and sidebar hover tints.
The default theme's blue is easier to look at. The UI accent is now derived from the
theme's blue with its hue preserved but saturation and lightness pulled into a comfortable
band. The terminal palette itself is untouched.
Output an application painted with an explicit truecolor still does not move when you
switch themes. That is correct terminal behavior — the app asked for that exact color —
and it is deliberate, not an oversight.
Eight more built-in themes
The built-in set grows from 5 to 13: Nord, Tokyo Night, Catppuccin Mocha,
Gruvbox Dark, Cobalt2, and light counterparts to themes already shipping —
Solarized Light, One Half Light, GitHub Light.
Dracula is fixed. Its foreground shipped as #FF5F1F, a bright orange, instead of
Dracula's #F8F8F2, so all normal text rendered orange.
New and corrected themes are seeded on next launch. As has always been the case with
built-ins, the packaged file wins — so a hand-edited copy of a built-in theme gets
overwritten. Save your own under a new name.
Also
- Connection Manager is a real window (
Ctrl+Shift+K), centered on the main window and
resizable, instead of an overlay drawn inside it with a hand-made title bar. - Unix sessions start your actual login shell. The default was a hardcoded
zsh → bash → sh existence probe, so a bash user got zsh whenever zsh happened to be
installed, and fish or nushell users could never be the default at all. NovaTerminal now
asks the system what your login shell is, and fresh installs lead with a "Default Shell"
profile. Existing profiles are untouched. dotnet buildprogress stays in one place instead of scrolling a new block of output
per refresh. The .NET 10 terminal logger drives this withCSI E/CSI F, which the
parser did not handle.
For contributors
v0.7.0-rc.1
What's Changed
- feat(app): wire inline image decoding for sixel, iTerm2, and Kitty by @benyblack in #369
- test(vt): decoder-wired e2e coverage for OSC 1337 and Kitty image paths by @benyblack in #372
- fix(vt): dispose pruned image bitmaps at the render frame boundary by @benyblack in #374
- test: serialize xunit collections (fixes #371 suite hang; exposes 8 ordering-sensitive UI tests) by @benyblack in #373
- fix(vt): keep dotnet terminal logger updates in place by @benyblack in #375
- Add an executable VT capability contract by @benyblack in #376
- feat(app): open connection manager as a real window by @benyblack in #379
- fix(pty): default unix sessions to the user's login shell by @benyblack in #380
- Expand built-in themes and fix Dracula foreground by @benyblack in #381
- ci: wire macOS codesigning + notarization behind MAC_* secrets by @benyblack in #382
Full Changelog: v0.6.1...v0.7.0-rc.1
v0.6.1
A bug-fix release for Windows, from a real first-run report. If you installed with
Setup.exe or the .pkg, this arrives on its own — check for the update toast and it
applies on your next restart. No manual cleanup is needed, including if 0.6.0 already
left a PowerShell tab misbehaving.
PowerShell tabs work again
Three problems that compounded into one bad first run.
A stale launch line was being replayed forever. A pane saved the command line it was
launched with, not the one you configured — so the shell-integration bootstrap ended up
written into your session file. On the next start NovaTerminal saw that bootstrap in
"your" arguments, concluded you had supplied your own script, and stepped aside — leaving
Command Assist off and the old bootstrap running anyway. Every launch re-saved it, so it
never recovered on its own. Panes now store what you configured; the launch details stay
where they belong.
Sessions already saved are repaired on load, so upgrading is enough. Only arguments
NovaTerminal itself injected are removed, identified by resolving the full path; your own
-File or -EncodedCommand is left exactly as you wrote it, spacing included.
The PowerShell startup banner no longer trips the execution policy. It was run from a
temporary .ps1, which Windows blocks under its default Restricted policy — so tabs
opened with a red UnauthorizedAccess error. Those statements are now sent straight to
the shell, so no script file is loaded and nothing is written to %TEMP% at all.
Also
- Shell-integration tests no longer write into the config directory of whoever is running
them.
Full changelog: v0.6.0...v0.6.1
v0.6.0
NovaTerminal 0.6.0 brings your configuration under your own control, and gives macOS
the same one-click installer and background updates Windows got in 0.5.0.
Installing and upgrading
macOS now has an installer. Download NovaTerminal-Setup-osx-arm64-v0.6.0.pkg. It
installs NovaTerminal as a proper .app, and from here on it updates itself in the
background like the Windows build does.
On Windows? If you installed 0.5.0 with NovaTerminal-Setup-win-x64-*.exe, you get
0.6.0 automatically — check for the update toast, and it applies on your next restart.
Still on a portable zip, on any platform? Run the installer once by hand. Zip builds
have no updater and cannot bring themselves forward. The zips remain available and
unchanged for anyone who prefers them.
Installers are not code-signed yet, so Windows SmartScreen and macOS Gatekeeper will
warn on first run. Signing is tracked in #91.
Configuration backup and restore
Your settings, themes, connections, workspaces, policy and snippets can now be exported
to a single portable .novabackup file and imported on another machine — or back onto
the same one after something goes wrong.
- Pick what travels. Six categories — Settings, Themes, Connections, Workspaces,
Policy and Snippets — each independently selectable on export and import. - Merge or replace. Import either layers the bundle over what you have or replaces
it outright, so moving to a new machine and topping up an existing setup are both
first-class. - Automatic snapshots. NovaTerminal now snapshots your configuration in the
background as it changes, deduplicated by content hash so unchanged config costs
nothing, with retention limits so they don't accumulate. A snapshot is also taken
immediately before any import or restore, so the operation is always reversible. - Restore asks first. Rolling back to a snapshot overwrites live configuration, so
it confirms before doing anything. - Imports are staged, then committed. The bundle is unpacked beside your live
configuration and swapped in as one step, so a failure part-way through leaves your
existing setup intact rather than half-replaced.
Available three ways: Settings → Backup & Restore, the command palette, and the CLI
(backup export, backup import, backup list, backup restore) for scripted setups.
Agents can read backups too, through read-only export and list MCP tools.
Connection passwords are not included in a bundle. They live in your OS credential
store — Windows Credential Manager, macOS Keychain, Linux Secret Service — not in
NovaTerminal's config folder, and a portable file is the wrong place for them. Imported
SSH profiles arrive complete apart from their passwords; re-enter those on first
connect. NovaTerminal tells you this in the import result rather than letting you find
out at the wrong moment.
Windows first-run fixes
Two problems that only ever showed up on machines that aren't a developer's, both
reported from a real install:
- The "PowerShell" profile no longer points at a shell you may not have. It targeted
pwsh.exe(PowerShell 7, a separate download) unconditionally, so on stock Windows it
was a dead entry that failed to launch. Default profiles are now filtered to shells
that are actually installed. The same applied to Zsh on Linux. - Command Assist works under the default execution policy. Its shell-integration
bootstrap was loaded with-File, which Windows blocks under the defaultRestricted
policy — so every PowerShell tab opened with a red security error and shell integration
silently did nothing. It no longer touches disk to start, so no policy applies, and
your own execution policy is left exactly as you set it.
Tabs
- The vertical sidebar's preview line now tracks the last meaningfully-changed row, so
a pane running a full-screen TUI shows actual content instead of static chrome. - Preview recomputation is gated behind a dirty flag and throttled, so a chatty pane no
longer costs you frames.
Fixes
- Themes repopulate and the selected profile re-points when settings change on disk
- Command-palette usage data saves atomically, so a crash mid-write can't corrupt it
- The Command Assist snippet list refreshes after an import or restore
For contributors
docs/CONFIG_STORAGE_CONTRACT.md now covers the backups\ directory and why recovery
data has to live outside the install root. If you touch anything that reads or writes
user configuration, read it first.
Full changelog: v0.5.0...v0.6.0
v0.5.0
NovaTerminal 0.5.0 is a big one — 145 commits since 0.4.0. Three things stand out:
Windows finally has a real installer that keeps itself up to date, tabs can live in a
vertical sidebar, and the native SSH backend has grown up enough to be the default for
new connections.
Installing and upgrading
Windows now has an installer. Download NovaTerminal-Setup-win-x64-v0.5.0.exe. It
installs per-user — no admin prompt, no UAC — and adds Start Menu and Desktop shortcuts
plus an Add/Remove Programs entry.
Coming from 0.4.0? Run the installer once by hand. 0.4.0 shipped as a zip and has no
updater, so it cannot bring itself forward. From 0.5.0 on, NovaTerminal checks for updates
in the background, downloads them quietly, and applies them the next time you restart —
no modal, no interrupted session. Turn it off under Settings → Automatic update checks
if you'd rather update yourself.
Prefer portable? The zips are unchanged — NovaTerminal-win-x64-v0.5.0.zip, plus
Linux x64 and macOS arm64. Portable builds have no updater.
The installer is not code-signed yet, so Windows SmartScreen will warn the first time you
run it. Signing is tracked in #91.
Vertical tab sidebar
Press Ctrl+Shift+L (or run Tabs: Toggle Vertical Tab Sidebar from the command palette)
to move the tab strip down the side of the window. Rows are richer than the horizontal
strip: each shows a status dot and a preview of the pane's most recent output, so you can
see which tab is working, which wants attention, and which is idle without switching to it.
The sidebar is resizable by dragging its edge, and the width persists.
Native SSH
The native SSH backend — no ssh binary required — is now the default for newly created
profiles. It gained the features that were keeping it experimental:
- Multi-hop jump chains, so bastion hops work end to end
- Remote port forwards, alongside the existing local and dynamic ones
- ssh-agent authentication
- A Settings → SSH tab, including the toggle to opt existing profiles in or out
- A warning when a profile carries
ProxyCommand/mux options or extra SSH args the native
backend won't honour, instead of silently ignoring them
Existing profiles keep using whichever backend they were already using. It remains
opt-in for them, and you can switch any profile back.
Command Assist
Command Assist was rebuilt around shell integration marks rather than guesswork:
- Reads the command line from the terminal grid between the
OSC 133;Bmark and the
cursor, so it no longer maintains a shadow copy that could drift out of sync - A one-line installer for shell integration on remote hosts, and mark detection over SSH
- Fix mode is real — failing command output is captured and used to propose a correction
- A command catalogue derived from tldr, plus snippet management
- A passive suggestion bubble with rebindable shortcuts, and history search where
accepting a row replaces what you typed - Works in sessions with no marks at all, by capturing straight-through-typed commands
There's also an opt-in seam for an external content provider, with structural redaction
applied before anything leaves the process.
Connections
You can now delete a connection, and deleting it purges the saved password rather than
orphaning it in the vault. A connection's saved password can also be viewed and cleared
on its own, without deleting the profile.
Terminal correctness
- Kitty keyboard protocol (disambiguate tier) — modified and special keys are now
distinguishable by apps that ask for it - OSC 52 clipboard writes, settings-gated; reads stay denied by design
- DECRQM mode queries answered with DECRPM, and OSC 10/11 colour queries answered
- OSC 8 hyperlinks carry their identity, not just the URI, so split segments of one link
behave as one link - Hover motion reported under
?1003any-event mouse tracking - Graphemes and hyperlinks survive every resize path, scrollback paging, and
ICH/DCH/insert-mode cell shifts - A completed ZWJ emoji no longer swallows the character after it
CSI s/u/rguarded against leader-prefixed variants
Rendering
- One glyph per grapheme cluster rather than per rune
- Glyph ink bounds packed in the atlas instead of the advance box, fixing clipped glyphs
- Flags, keycaps and standalone emoji routed to the colour atlas
- Box-drawing primitives on by default; rounded corners are arcs, not chamfers
- Sixel HLS colours decoded correctly
- The cursor blink timer stops while a pane is unfocused
Shells and panes
- The ConPTY host is sideloaded, so a
conhostcrash can no longer take the pane with it - A dead pane says so, and child exit is actually detected
- A killed shell no longer reports exit code 0 on Unix
- Win32 spawn handles have owners instead of leaking
- Local shell commands resolve by the platform they were written for
ShellExitPolicydefaults to Graceful
Security and hardening
- Session passwords are no longer retained as unclearable managed strings, and Rust-owned
credential copies are wiped - SFTP rejects unsafe remote entry names and no longer truncates downloads; UNC-style
remote paths normalise correctly in the files sidebar - SSH rejects invalid UTF-8 in connect arguments instead of mangling it
- Password-capture holes in Command Assist closed
- Two FFI abuse-test gaps closed
Also
- Customisable title bar icons
- Agent access shows live per-pane indicators, and a pane can be rendered to PNG via
novaterminal.capture_screen - The command palette lists every installed theme, not two hardcoded ones
- Performance: SSH event payloads no longer copied twice, the native SSH event queue is
bounded with producer backpressure, and the VT write path no longer allocates a string
per grapheme - A project landing site
For contributors
New: docs/CONFIG_STORAGE_CONTRACT.md
— where user state lives, what an update deletes versus what an uninstall deletes, and why
secrets are not in the config folder. Read it before touching anything that reads or writes
user configuration.
Full changelog: v0.4.0...v0.5.0