<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Termuna changelog</title>
    <link>https://termuna.com/changelog/</link>
    <description>Every user-visible change in each Termuna release, newest first, with the full entry.</description>
    <language>en</language>
    <atom:link href="https://termuna.com/changelog/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>0.2.4</title>
      <link>https://termuna.com/changelog/#0.2.4</link>
      <guid isPermaLink="false">tag:termuna.com,2026:changelog/0.2.4</guid>
      <pubDate>Tue, 25 Aug 2026 00:00:00 GMT</pubDate>
      <description>&lt;h3&gt;Added: a .deb package, and an apt repository that carries its updates&lt;/h3&gt;
&lt;p&gt;Linux gets a real package: &lt;code&gt;termuna-amd64.deb&lt;/code&gt; on every release (built by cargo-deb from the same binary the tarball ships, with the desktop entry and icon in their proper places), and an apt repository at &lt;code&gt;termuna.com/apt&lt;/code&gt; - GPG-signed, &lt;code&gt;signed-by&lt;/code&gt;-pinned, served as static files by the site's own nginx. Add it once and Termuna updates with the rest of the system through &lt;code&gt;apt upgrade&lt;/code&gt;. A package install lives in &lt;code&gt;/usr/bin&lt;/code&gt;, which the in-app updater already treats as not its own (ADR 0012), so the two update channels never fight. The repository is signed on the maintainer's machine (&lt;code&gt;scripts/publish-apt.sh&lt;/code&gt;), never in CI - the same custody rule as the update manifest key.&lt;/p&gt;
&lt;h3&gt;Added: a modal with edits asks before it discards them&lt;/h3&gt;
&lt;p&gt;Esc, Cancel, or a click outside a modal used to close it no matter what it held: half a filled-in SSH host, a typed passphrase, a picked set of teammates - gone on one stray key. A dismiss that would lose typed input now stops at a small question - keep editing, or discard - and esc on the question itself keeps editing, so the safe answer is the easy one. Pristine forms and confirm dialogs close exactly as before: the question only exists when there is something to lose.&lt;/p&gt;
&lt;h3&gt;Fixed: the Windows installer no longer kills your sessions&lt;/h3&gt;
&lt;p&gt;Upgrading on Windows ran &lt;code&gt;taskkill /f /im termuna.exe&lt;/code&gt; before copying the new build - ending the window, the daemon, and every running session mid-install, the exact thing the product promises never to do. NTFS allows renaming a running executable, so the installer now moves the old build aside (&lt;code&gt;termuna.exe.old&lt;/code&gt;) and lays the new one down without touching a single process: the window picks the new build up on its next launch, the daemon on its next restart, where it hands its shells over instead of losing them. The app sweeps the leftover &lt;code&gt;.old&lt;/code&gt; on a later start once nothing runs from it. Verified live on the Windows test machine: a silent upgrade over a running instance left both processes untouched, and the relaunched window attached to the surviving daemon. Uninstall still ends the processes, because there ending them is the point.&lt;/p&gt;
&lt;h3&gt;Fixed: a URL in parentheses is a link again&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;(https://...)&lt;/code&gt; - the way agents and docs print links - hovered as nothing: the opening paren is a legal URL character, the detector swallowed it, and the &amp;quot;starts with http&amp;quot; check then threw the whole match away. The link is now anchored on the scheme, so the surrounding prose punctuation stays prose, and a trailing &lt;code&gt;)&lt;/code&gt; is only trimmed when the URL's own parentheses do not balance (a Wikipedia-style path keeps its parens). Hover underlines exactly what ctrl+click opens, including under a TUI that owns the mouse.&lt;/p&gt;
&lt;h3&gt;Changed: the daemon's health moved to the continuity panel&lt;/h3&gt;
&lt;p&gt;It was squeezed under the account name in the sessions drawer's foot, three lines deep in the narrowest corner of the window, while the continuity panel - the column that already holds the machine facts - had the room. The daemon block now sits there, above the encryption line: a calm dot and uptime when healthy, and when the daemon is an older build, the row itself is the restart button. The drawer's foot goes back to being about the account: name and email, nothing else.&lt;/p&gt;</description>
    </item>
    <item>
      <title>0.2.3</title>
      <link>https://termuna.com/changelog/#0.2.3</link>
      <guid isPermaLink="false">tag:termuna.com,2026:changelog/0.2.3</guid>
      <pubDate>Fri, 21 Aug 2026 00:00:00 GMT</pubDate>
      <description>&lt;h3&gt;Changed: on Linux, the update installs itself - in a session you watch (ADR 0012)&lt;/h3&gt;
&lt;p&gt;&amp;quot;Install&amp;quot; used to end at a file manager opening on a folder with a tarball in it, which is where the first person to try it reasonably asked what a terminal was doing showing them a folder. When the running binary is ours to replace (its directory is writable and lives under &lt;code&gt;$HOME&lt;/code&gt;), Install now opens a fresh session named for the version and executes the install there: the exact &lt;code&gt;tar&lt;/code&gt; command scrolls by in a pane, the closing line says what remains, and the session's scrollback is the install log. Nothing happens off-screen. A binary that is not ours (a distro package, an admin install) keeps the hands-off behaviour, because there &amp;quot;don't touch&amp;quot; is still the only honest answer. The artifact is SHA-256-verified against the signed manifest before any of this, as before.&lt;/p&gt;</description>
    </item>
    <item>
      <title>0.2.2</title>
      <link>https://termuna.com/changelog/#0.2.2</link>
      <guid isPermaLink="false">tag:termuna.com,2026:changelog/0.2.2</guid>
      <pubDate>Fri, 21 Aug 2026 00:00:00 GMT</pubDate>
      <description>&lt;h3&gt;Fixed: a daemon restart reconnects quietly instead of alarming you&lt;/h3&gt;
&lt;p&gt;Pressing &amp;quot;Restart daemon&amp;quot; (or restarting it by hand) dropped every viewer for under a second, and the window treated that like any dead daemon: the connecting screen, in red, &amp;quot;connection to mux daemon lost&amp;quot;. A lost daemon connection now walks back in silently first - the successor holds the same sessions, so the window lands back where it was, typed-but-unsent input still on screen - and the red screen is reserved for when coming back actually fails twice. The drawer foot's daemon line also refreshes on reattach instead of claiming &amp;quot;offline&amp;quot; until the next slow poll. And the handoff quiesce no longer depends on the signal mask the daemon happened to inherit: a spawner with SIGUSR1 blocked degraded every restart to a ten-second stall; the reader thread now unblocks it for itself, and the sandboxed swap went from 10s to 8ms.&lt;/p&gt;
&lt;h3&gt;Fixed: a daemon handoff no longer loses output a shell is streaming&lt;/h3&gt;
&lt;p&gt;From the moment a successor daemon rebuilt the sessions until the predecessor exited, both were reading the same PTY masters: whatever the predecessor won died with its log, and whatever the successor won reused sequence numbers the relay had already seen and silently dropped. A repro streaming numbered lines through a handoff lost 400-900 lines per swap; the &amp;quot;Restart daemon&amp;quot; button (ADR 0009) put this path in users' hands. The handoff is now serialized (ADR 0008 amendment): the predecessor stops consuming and provably drains its pipeline before the snapshot, the successor starts reading only once the predecessor's exit closes the handoff socket, and a closing delta covers the seam. Cost: a streaming shell briefly blocks on write during the swap, which it cannot observe. Stressed on an otherwise saturated machine (16 busy cores), 30 of 30 handoffs mid-flood now replay every one of 50,000 numbered lines; the wire change is compatible in both directions with older builds. The Windows named-pipe handoff keeps the old semantics until it gets the same treatment.&lt;/p&gt;
&lt;h3&gt;Fixed: a tab's auto-title no longer misses the &lt;code&gt;cd&lt;/code&gt; that goes quiet&lt;/h3&gt;
&lt;p&gt;The cwd-based title refreshed on output behind a 2s throttle, and the first chunk after a &lt;code&gt;cd&lt;/code&gt; is the terminal's echo of the command - sent before the shell has run it. That chunk spent the refresh on the old directory, the real output landed inside the closed window, and a shell that then went quiet kept its stale title until it next said something. Now the first throttled chunk of a burst schedules one recheck for when the window reopens, so the title lands within ~2s of the change no matter how quiet the shell goes. This is also what &lt;code&gt;tabs_auto_title_from_cwd_and_rename_pins&lt;/code&gt; had been failing on since 19.8: not runner load, a refresh race, which is why raising its deadline never helped.&lt;/p&gt;
&lt;h3&gt;Added: a live daemon can say which protocol it speaks&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;DaemonStatus&lt;/code&gt; carries the TSP &lt;code&gt;PROTOCOL_VERSION&lt;/code&gt; beside the crate version, &lt;code&gt;termuna-daemon status&lt;/code&gt; prints it for the running process (with a nudge when the binary on disk speaks a newer one), and the desktop judges &amp;quot;restart the daemon&amp;quot; on both numbers. The crate version alone was the wrong signal in both directions: 0.2.1 spanned TSP v2 and v3, so during this week's breaking rollout a daemon two protocols behind reported itself current, and a same-protocol rebuild reported itself stale. A daemon too old to report the field reads as protocol 0, which correctly counts as stale.&lt;/p&gt;
&lt;h3&gt;Fixed: &lt;code&gt;Pty::disarm&lt;/code&gt; no longer lets the writer hang up the shell&lt;/h3&gt;
&lt;p&gt;portable-pty's writer politely writes &lt;code&gt;&amp;quot;\n&amp;quot;&lt;/code&gt; + VEOF into the terminal when dropped, and in canonical mode VEOF is end-of-input: dropping a disarmed pty told the very shell being handed over to exit. The daemon never hit it (it steps aside with &lt;code&gt;process::exit&lt;/code&gt;, so no Drop runs), but the adoption test did, intermittently, and the failures were misread as CI slowness. Disarm now neutralises the writer, and the tests assert on printf-assembled output the terminal echo cannot fake, which is what they believed they were asserting all along.&lt;/p&gt;
&lt;h3&gt;Changed: clients ask where to connect (ADR 0011)&lt;/h3&gt;
&lt;p&gt;The relay address was a constant in every client: &lt;code&gt;wss://termuna.com/v1/session&lt;/code&gt; compiled into the desktop and the headless daemon, derived from &lt;code&gt;location.host&lt;/code&gt; on the web, derived from the API base on the phone. That works as long as there is one relay, forever. The day there is a second one - a node closer to Asia, a self-hosted deployment, staging - every installed copy would have to be replaced before it could be told.&lt;/p&gt;
&lt;p&gt;Sign-in now asks &lt;code&gt;GET /v1/config&lt;/code&gt; and writes the answer into &lt;code&gt;[cloud] url&lt;/code&gt; / &lt;code&gt;share_base&lt;/code&gt;, so the compiled-in address is only a bootstrap: it is how a fresh install reaches the API the first time, and what the API answers is what is used from then on. `termuna-daemon login` does the same. Anything unrecognised keeps the address we already had, because a cloud that cannot answer this is still a cloud worth signing into.&lt;/p&gt;
&lt;p&gt;Session listings also carry an optional &lt;code&gt;relay_url&lt;/code&gt;, and clients prefer it: a session lives on whichever relay its host bridged to. It is null for every session today; clients read it now so that the day it is not is a config change rather than a protocol change.&lt;/p&gt;
&lt;p&gt;Share links deliberately stay on the apex. &lt;code&gt;termuna.com/s/&amp;lt;id&amp;gt;&lt;/code&gt; gets pasted into other people's chat histories and outlives every deployment decision we will ever make, so the region belongs in the routing layer and is resolved when the link is opened.&lt;/p&gt;
&lt;p&gt;Along the way the three sign-in paths (password, TOTP, browser) stopped carrying three copies of the same tail; they share &lt;code&gt;cloud_client::land_sign_in&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Changed: the vault records the KDF that made it (ADR 0010)&lt;/h3&gt;
&lt;p&gt;The Argon2id parameters that stretch a vault passphrase used to exist only as constants in three separate client source trees, with the server storing a salt and nothing else. That works exactly until they need changing, and they will: they are OWASP's *minimum* (19 MiB, t=2), and Argon2 costs are supposed to rise with hardware. Raising them without knowing what a given account key was sealed under is impossible, and the only repair would be asking every user to re-enter the passphrase that guards custody of their machines.&lt;/p&gt;
&lt;p&gt;An account now publishes &lt;code&gt;kdf&lt;/code&gt; and &lt;code&gt;kdf_params&lt;/code&gt; alongside &lt;code&gt;kdf_salt&lt;/code&gt;, and clients derive with what the server returns rather than with their own constants. A recovery kit records its own, since one can be minted long after the account key was. Blobs from before this say nothing, and the historical values are the right answer for them, not a guess.&lt;/p&gt;
&lt;p&gt;Rust also stops inheriting its parameters from &lt;code&gt;Argon2::default()&lt;/code&gt;: they are pinned explicitly, with a test asserting they still equal the crate default. Otherwise a dependency that changed its defaults in a minor release would change how every passphrase derives, and the symptom of a routine &lt;code&gt;cargo update&lt;/code&gt; would be every existing vault reporting a wrong passphrase.&lt;/p&gt;
&lt;p&gt;An algorithm a client cannot derive is now refused by name instead of attempted anyway: &amp;quot;this vault uses X, update Termuna&amp;quot; rather than a wrong key presenting as a wrong passphrase.&lt;/p&gt;
&lt;h3&gt;Changed: sealed blobs name their own format (TSP v3)&lt;/h3&gt;
&lt;p&gt;Every sealed blob now begins with a byte naming the construction that produced it: &lt;code&gt;version(1) || nonce(24) || ciphertext+tag&lt;/code&gt; for content, and &lt;code&gt;version(1) || epk(32) || nonce(24) || box_ct&lt;/code&gt; for a key wrapped to an account. The byte is checked before the key, and an unrecognised value is its own error rather than an authentication failure, so a blob from a newer Termuna reports &amp;quot;this needs a newer client&amp;quot; instead of something indistinguishable from a wrong passphrase.&lt;/p&gt;
&lt;p&gt;One byte per frame buys the ability to ever change how bytes are sealed. Without it that change is a flag day over data we no longer control: frames sit durably in the relay's database, a sealed account secret sits in the accounts database, and share links in other people's chat histories point at both. &amp;quot;Try the new format, fall back to the old&amp;quot; is not a substitute, because an AEAD failure looks exactly like a wrong key, so the fallback would silently retry every real tampering.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PROTOCOL_VERSION&lt;/code&gt; is 3. The envelope did not change at all, which is precisely why the version had to: a v2 peer would route v3 frames flawlessly and then fail to decrypt every one of them, with no way to say why. **The relay, the desktop, the web client and the phone must be rebuilt and deployed together.**&lt;/p&gt;
&lt;p&gt;The web and Dart clients are now pinned to fixtures this repo emits (&lt;code&gt;cargo run -p termuna-sync --example format-fixtures&lt;/code&gt; and &lt;code&gt;--example tsp-vectors&lt;/code&gt;) rather than to their own past, or to a scratch cargo project the docs asked a maintainer to write from scratch.&lt;/p&gt;
&lt;h3&gt;Added: the app can tell you it is out of date (ADR 0009)&lt;/h3&gt;
&lt;p&gt;Termuna had no way to reach an installed build. With &lt;code&gt;PROTOCOL_VERSION&lt;/code&gt; refusing peers across major versions, that meant the day the relay was upgraded every desktop that had not been manually reinstalled would stop connecting, with no way to say why.&lt;/p&gt;
&lt;p&gt;Once a day the app now fetches a signed manifest from the public releases repo and, if a newer release exists, says so as one line in the status bar. Pressing it downloads that release's artifact for this platform and checks it against the SHA-256 the manifest promises; pressing it again hands the file to the OS (the per-user installer runs on Windows, the file manager opens on the verified archive on macOS and Linux). The app never downloads without being asked and never replaces its own binary.&lt;/p&gt;
&lt;p&gt;The manifest is Ed25519-signed and verified in-app against a key compiled into the build, with the private half on the maintainer's machine and never in CI: a stolen release token can publish files, but not files any installed app will accept. The signature covers the manifest as text and is checked before anything is parsed, and the trusted keys are a list the envelope selects from, so rotating a key is a rollout rather than a flag day.&lt;/p&gt;
&lt;p&gt;The check is a plain GET of a public file that carries nothing about the user; the version comparison happens locally. It is on by default with a switch in Settings → About, and it is deliberately not on the launch path: the first check waits until twenty seconds after the window is up, so cold start never waits on the network.&lt;/p&gt;
&lt;p&gt;Settings → About also grew the other half of an install: when the daemon is running an older build than the app (after an install it always is, and on Unix it is still holding the deleted inode of the old binary), it offers to restart it. The ADR 0008 handoff means the successor adopts the running shells rather than ending them.&lt;/p&gt;
&lt;h3&gt;Added: the sign-in screen speaks two-factor authentication&lt;/h3&gt;
&lt;p&gt;An account with TOTP on (Termuna Cloud, Security page) used to be unable to sign in from the desktop at all: the password came back without a session and the screen had nothing to say. The password half now pauses on a code form - six digits from the authenticator app, or a recovery code - and finishes the sign-in exactly as before. The browser sign-in (Continue with Google/GitHub) needs nothing: the web form carries its own 2FA. &lt;code&gt;termuna-daemon login&lt;/code&gt; explains that a 2FA account joins a server with a borrowed token instead of failing with &amp;quot;no session&amp;quot;.&lt;/p&gt;
&lt;h3&gt;Added: sign in with Google or GitHub&lt;/h3&gt;
&lt;p&gt;The sign-in screen now offers &amp;quot;Continue with Google&amp;quot; and &amp;quot;Continue with GitHub&amp;quot; whenever the cloud has those providers configured; without them the screen is unchanged. The app starts an app sign-in flow, opens the system browser to finish it, and polls the cloud until the browser half is done; the device token is minted at claim time and delivered exactly once, after which everything proceeds as a password sign-in would (config written, daemon flipped to cloud mode, vault passphrase asked). A cancelled or failed browser round-trip reports its reason on the sign-in screen, and a Cancel button stops the wait. This closes a real hole: an account created with Google on the website has no password, so the desktop could not sign into it at all.&lt;/p&gt;
&lt;h3&gt;Added: the daemon upgrades without killing sessions (ADR 0008)&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;termuna-daemon&lt;/code&gt; server now takes over a running daemon's sessions instead of refusing to start. On start it looks for a predecessor; if there is one it adopts its shells over a Unix socket (&lt;code&gt;SCM_RIGHTS&lt;/code&gt; carries each pty master), the predecessor acks and steps aside, and the running shells keep running under the new binary. No predecessor means a cold start, so the same command does the right thing either way. The relay connection reconnects (a viewer sees a brief &amp;quot;reconnecting&amp;quot;, no loss); a crash or reboot still resurrects as before, and Windows is unchanged. Rolling out new daemon code on a machine someone is using no longer ends their work.&lt;/p&gt;
&lt;p&gt;Under systemd the same holds for &lt;code&gt;systemctl --user restart&lt;/code&gt;, where the old process is gone before the new one starts: the unit is now &lt;code&gt;Type=notify&lt;/code&gt; with a file-descriptor store, so the daemon hands its pty masters to systemd on stop and reads them back from &lt;code&gt;LISTEN_FDS&lt;/code&gt; on the next start (&lt;code&gt;KillMode=process&lt;/code&gt; keeps the shells in the cgroup meanwhile). Verified end to end both ways: two hand-started daemons, and a real &lt;code&gt;systemctl --user restart&lt;/code&gt;, each with the shell still running under the new instance (same pid).&lt;/p&gt;
&lt;p&gt;Windows does it too, by its own mechanism: the daemon drives ConPTY natively (portable-pty hides the handles it needs), and a successor &lt;code&gt;DuplicateHandle&lt;/code&gt;s each pane's conin/conout pipe out of the predecessor over a sibling handoff pipe, adopts the still-running shell, and acks before the predecessor exits. One thing is lost there: the pseudoconsole's resize handle cannot be transferred, so an adopted terminal keeps its last size until the session is rebuilt.&lt;/p&gt;
&lt;p&gt;Live-verified on all three platforms with a real process surviving a daemon swap under the same pid: Linux (bash, plus a desktop full of &lt;code&gt;claude&lt;/code&gt;/&lt;code&gt;htop&lt;/code&gt;/&lt;code&gt;node&lt;/code&gt;), macOS 26.2 (zsh), and Windows 10 (powershell). The desktop-spawned daemon now adopts on start, so it is covered too.&lt;/p&gt;
&lt;h3&gt;Added: a launch-time permission mode that survives a resurrect&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;CreateAgentSession&lt;/code&gt;'s &lt;code&gt;AgentLaunch&lt;/code&gt; now carries an optional &lt;code&gt;permission_mode&lt;/code&gt; (one of &lt;code&gt;AgentCommand::REMOTE_PERMISSION_MODES&lt;/code&gt;, &lt;code&gt;bypassPermissions&lt;/code&gt; included). A remote client that starts an agent in bypass had, until now, only the runtime &lt;code&gt;SetPermissionMode&lt;/code&gt;, which lasts the run and is gone after a resurrect. Setting the mode at launch lets the host write it into the session's meta, so the choice holds when the conversation is brought back. The host validates it (an unknown mode is dropped) and ignores it for a shell-typed agent. The field is additive: an older peer omits it and the host falls back to the CLI default. Starting an agent on another machine from the desktop now carries its bypass toggle the same way.&lt;/p&gt;
&lt;h3&gt;Fixed: a connection whose id another account holds still syncs&lt;/h3&gt;
&lt;p&gt;Item ids are globally unique on the relay, so two independent accounts that ended up with the same connection id (a vault copied between them) collided: the server refused to let one account write an id the other already owned (a 403, correct, so no account can hijack another's item), and the desktop swallowed it. The connection sat local forever while the &amp;quot;synced&amp;quot; note stayed green. The push now re-keys its own copy on that 403 (a fresh id, its own independent item) and retries, dropping the old local id without a tombstone (the old id belongs to the other account, so deleting it is not ours to do). A push that fails for any other reason on the personal vault now surfaces instead of being shrugged off.&lt;/p&gt;
&lt;h3&gt;Fixed: a saved connection reaches the cloud, or says why not&lt;/h3&gt;
&lt;p&gt;Saving or deleting an SSH connection while signed in but with the vault locked used to write to this disk and silently skip the sync: the host stayed local, the &amp;quot;synced&amp;quot; note kept its green tick, and nothing said the change never reached the cloud or your other devices. Now, when a vault action needs the passphrase, the app asks for it instead of falling back in silence, and a failed sync shows as a problem, not a quiet success. The connections subtitle no longer claims hosts are &amp;quot;sealed on this device&amp;quot;: signed in, they are encrypted end to end and synced to every device you sign in on. There is one passphrase behind all of it (the account key that seals connections, sessions and device custody alike), so unlocking once covers everything through the server.&lt;/p&gt;
&lt;h3&gt;Added: start an agent on another machine&lt;/h3&gt;
&lt;p&gt;&amp;quot;Start agent&amp;quot; now asks which machine should run it, exactly like &amp;quot;new session&amp;quot; does — but only when another machine is reachable; with just this device it skips straight to the directory chooser as before. Pick a remote machine and the directory field targets it (its own recent directories aren't shown, they're this machine's), the far daemon creates the agent and answers with its share link, and the conversation mirrors here. The launch-time bypass toggle stays on the local path (the wire launch carries no mode); a remote agent reaches bypass from its chat once it is up.&lt;/p&gt;
&lt;p&gt;A provider whose CLI isn't installed on *this* machine is still selectable when another machine is reachable (it may have it): the segment no longer greys out just because the local daemon lacks the binary. The machine picker then marks &amp;quot;this device&amp;quot; as &amp;quot;not installed here&amp;quot; and unpressable, steering you to a machine that has it. This is what makes the feature work from a Windows box that has no agent CLIs locally but is paired with a Linux machine that does.&lt;/p&gt;
&lt;h3&gt;Added: launch a managed agent in bypass-permissions mode&lt;/h3&gt;
&lt;p&gt;A managed Claude session can now run without the per-tool permission asks, when the operator asks for it (ADR 0007). The start-agent card has a &amp;quot;bypass permissions&amp;quot; toggle, the chat mode menu (Shift+Tab) gains a fourth entry beside manual / edit / plan, and the mode is persisted: a resurrected unattended agent comes back unattended. &lt;code&gt;bypassPermissions&lt;/code&gt; is no longer refused on the wire — custody (the account passphrase that gates reaching a session at all) is the boundary, not a mode the protocol pretends it can hide from its own operator; so a phone reaches it too, at runtime. Off by default, always a deliberate choice.&lt;/p&gt;
&lt;h3&gt;Fixed: an unfocused window shows the hollow cursor&lt;/h3&gt;
&lt;p&gt;The cursor kept its solid, blinking self while the window sat in the background: the universal terminal signal for &amp;quot;typing lands here&amp;quot; shown somewhere typing could not land. Window focus now joins the pane-focus test: every pane of an unfocused window draws the hollow outline cursor, the blink timer stops while nobody is watching (and restarts on the visible half when focus returns).&lt;/p&gt;
&lt;h3&gt;Fixed: the paste-confirm card answers to the keyboard&lt;/h3&gt;
&lt;p&gt;The multiline-paste question could only be answered with the mouse: Enter did nothing (worse: keys pressed while the card was open leaked into the shell underneath, and Esc dismissed the card's animation without cancelling the paste, leaving it stuck). Enter now pastes, Esc cancels, every other key stops at the card, and the card says so. Terminal hands live on the keyboard; a modal that ignores it is a bug, not a style.&lt;/p&gt;
&lt;h3&gt;Added: shells know they run in Termuna&lt;/h3&gt;
&lt;p&gt;Spawned shells now carry &lt;code&gt;TERM_PROGRAM=termuna&lt;/code&gt; (and &lt;code&gt;TERM_PROGRAM_VERSION&lt;/code&gt;), the convention terminal-aware tools read. Claude Code's &amp;quot;auto&amp;quot; notification channel, for instance, is a whitelist of terminals it recognizes by exactly this variable (Apple Terminal, iTerm2, kitty, ghostty: everyone else gets silence): identifying ourselves honestly is the prerequisite for ever being on such lists. Until then, claude's &lt;code&gt;/config&lt;/code&gt; → Notifications → &lt;code&gt;terminal_bell&lt;/code&gt; is the setting that makes it ring here.&lt;/p&gt;
&lt;h3&gt;Added: links look like links under the mouse&lt;/h3&gt;
&lt;p&gt;Hovering a URL in the grid (a detected http(s) one, or an OSC 8 hyperlink an application drew) now underlines its span and turns the cursor into a pointer, the affordance every other surface taught your hand to expect. Ctrl+click opens it, as before. The underline stops where the URL does: trailing punctuation stays plain.&lt;/p&gt;
&lt;h3&gt;Added: focus reporting (?1004), so agent CLIs know when you left&lt;/h3&gt;
&lt;p&gt;Applications that subscribe to focus reporting now hear &lt;code&gt;ESC[I&lt;/code&gt;/&lt;code&gt;ESC[O&lt;/code&gt; when their pane gains or loses the user's attention (window focus, tab switch and pane focus all count). Claude Code gates its &amp;quot;I'm done&amp;quot; bell on exactly this: in a terminal that never answers, it believes it is watched forever and never rings. With its notification channel set to &lt;code&gt;terminal_bell&lt;/code&gt; (claude's &lt;code&gt;/config&lt;/code&gt; → Notifications), a finished answer in an out-of-sight pane now lands as a desktop notification. An OSC 9/777 notification caught by the daemon (the Attention frame) reaches the desktop the same way.&lt;/p&gt;
&lt;h3&gt;Added: a finished command notifies the desktop&lt;/h3&gt;
&lt;p&gt;A command that ran at least 15 seconds in a pane you cannot see — a background tab, or any tab while the window is unfocused — now raises a desktop notification when it finishes (&amp;quot;finished after 2m 13s&amp;quot;), detected from the daemon's own busy-tab tracking: nothing is injected into the shell. Bells from out-of-sight panes (TUIs and agent CLIs ring one when they need you) notify under the same switch, which also respects the window's real focus now, not just the active tab. &lt;code&gt;notify_when_done&lt;/code&gt; defaults to on; what you are looking at never notifies, and there is no sound. &lt;code&gt;notify_on_activity&lt;/code&gt; (first output of a quiet background tab) stays opt-in.&lt;/p&gt;
&lt;h3&gt;Fixed: status emoji render in color&lt;/h3&gt;
&lt;p&gt;✔️ ⚠️ 💡 📊 🐛 and friends — the emoji every CLI status line leans on — rendered as small monochrome glyphs: the monospace-first fallback (right for braille and CJK) let a symbols face win over the color emoji font. Emoji-class codepoints (the pictograph planes, UTS #51 emoji-default singles, and anything carrying VS16) now route explicitly to the platform's color emoji face (Noto/Segoe/Apple), the same way PUA icons route to the bundled Nerd symbols. Bare text-presentation dingbats (✔ without VS16) stay in the text face, as in other terminals. Known engine-level limits, unchanged and shared with stock Alacritty: skin-tone modifiers, ZWJ sequences and flags render as their parts.&lt;/p&gt;
&lt;h3&gt;Fixed: dragging selects text in a Claude session&lt;/h3&gt;
&lt;p&gt;Claude Code turns on mouse reporting with its first prompt, and it ends up in *any-motion* tracking (1003), because the mouse protocols are mutually exclusive and that is the last one it sets. The drag-takeover path (a left-drag in the normal screen becomes a local selection) was keyed on the *button-event* flag (1002), which 1003 had just replaced: so in a Claude session a drag reported the press and the release to the agent and selected nothing at all. The takeover now keys on mouse reporting itself, whichever protocol is active, including click-only (1000), where the drag previously vanished with no selection offered. Verified against a live Claude session: a drag mid-stream selects, the highlight rides the text into scrollback, and Shift still hands the drag to the app. (Note xterm.js-based terminals require Shift for this; a plain drag selecting is on purpose.)&lt;/p&gt;
&lt;h3&gt;Fixed: a resurrected session's shells no longer wake at 80×24&lt;/h3&gt;
&lt;p&gt;A resurrected pane keeps its id, but its PTY spawned at the default grid, and the window's own bookkeeping believed the size was already sent, so nothing ever corrected it: prompts wrapped at 80 columns in a full-width window until a resize crossed a cell boundary. The daemon now spawns resurrected panes at the size the persisted model remembers (sizes now persist with the debounced flush, once per burst), the GUI forgets its size bookkeeping on every attach, and an integration test resizes, restarts and asks &lt;code&gt;stty&lt;/code&gt; on both sides of the grave.&lt;/p&gt;
&lt;h3&gt;Fixed: resizing reflows in the same frame&lt;/h3&gt;
&lt;p&gt;The grid waited for the daemon's layout round-trip before adopting a new size, so every resize had a window where output for the new PTY grid reflowed into an old mirror: the wrap artifacts you saw while dragging the window edge. The mirror now resizes optimistically to the window's own grid (the daemon remains authoritative: a smaller co-viewer still letterboxes via smallest-wins), background tabs adopt the authoritative size from layout snapshots, and the reattach repaint nudge re-reads the model so it can no longer clobber a resize that landed between its two halves.&lt;/p&gt;
&lt;h3&gt;Fixed: pane geometry is computed the way it is drawn&lt;/h3&gt;
&lt;p&gt;Split sizing ignored the 6px divider grab strip, the broadcast banner's height was never budgeted, and a maximized pane kept the small grid of its split slot: each one a column or row the shell believed in and the window could not draw. One geometry now (and the banner has a fixed height so the view and the math cannot drift apart). Dragging a divider also stops writing the session to disk on every mouse-move: ratio updates are throttled to ~12/s with the final one guaranteed on release.&lt;/p&gt;
&lt;h3&gt;Fixed: selection is anchored to the text, not the glass&lt;/h3&gt;
&lt;p&gt;Selection lived in screen coordinates and was cleared on every output frame, which made copying from a streaming TUI (a Claude session, a build log) practically impossible: the highlight vanished or slid onto different text. Selection now lives in the emulator, anchored to the buffer the way VTE and Alacritty do it: it scrolls with the content, survives streaming output, copies exactly what was swept (wide chars and soft-wraps handled by the engine), and triple-click grabs a whole wrapped line. Double-click word selection keeps the configurable &lt;code&gt;word_separators&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Fixed: grid glyphs: icons, braille, CJK, accents&lt;/h3&gt;
&lt;p&gt;Four rendering gaps that together read as &amp;quot;the font looks worse than the system terminal&amp;quot;:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Nerd icons&lt;/strong&gt;: Symbols Nerd Font Mono (MIT, ~2.5MB) is bundled and every private-use codepoint routes to it explicitly, so prompt glyphs, powerline segments and CLI-agent spinners render identically on every machine instead of gambling on installed fonts.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Fallback prefers monospace&lt;/strong&gt;: cosmic-text's &lt;code&gt;monospace_fallback&lt;/code&gt; feature is on, so glyphs the grid font lacks (braille, legacy blocks, CJK) come from a mono face instead of a proportional one that wobbles out of its cell.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Whole pixels&lt;/strong&gt;: grid text was vertically centered, which parked the baseline on a half pixel whenever the cell height was odd (the default 12.5px × 1.65 = 21px did exactly that): a uniform vertical blur, now gone: runs are top-aligned on integer cell tops.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Wide glyphs and combining marks&lt;/strong&gt;: a double-width glyph's underline, strikeout and background now span both columns, and zero-width characters (combining accents, ZWJ sequences) reach the renderer instead of being dropped.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: renaming a session takes the caret, and keeps the icon&lt;/h3&gt;
&lt;p&gt;The row turns into a field for one purpose and then made you click it before you could type. It takes focus now, from the drawer's own rows and from another machine's alike.&lt;/p&gt;
&lt;p&gt;A remote row also lost its glyph the moment you started typing and got it back when you stopped: its editor was the field alone, while a local row's keeps the icon beside it. Both are the same shape now.&lt;/p&gt;
&lt;h3&gt;Fixed: closing the last tab of another machine's session closes it here too&lt;/h3&gt;
&lt;p&gt;The tab went, the far machine ended the session, and its dead tab sat on screen waiting for something to notice.&lt;/p&gt;
&lt;p&gt;A mirror was never told it was over. When the far host says &lt;code&gt;Bye&lt;/code&gt; the watching loop simply stopped watching: the mirror stayed in this daemon's registry holding the last tree it saw, so the window went on showing a session that had already ended somewhere else, and only a later poll shook it loose. A mirror now ends the way a local session ends, which is what tells the window and takes the row out of the registry, so closing a remote tab behaves like closing a local one.&lt;/p&gt;
&lt;h3&gt;Changed: a signed-in machine stays reachable with its window closed&lt;/h3&gt;
&lt;p&gt;The idle exit applied to the desktop's daemon too, and that quietly undid what the device channel is for: fifteen minutes after closing Termuna, a computer that was still switched on stopped being listed, stopped accepting a new session, and took its dormant sessions out of reach with it. Before machines were addressable this cost nothing; now it is the difference between a promise and a footnote.&lt;/p&gt;
&lt;p&gt;Signed in means resident, on every platform. Signed out keeps the timeout: with no account there is nobody to be reachable for, and an offline user should not be left carrying a process for nothing. Checked continuously rather than at startup, so signing in mid-run keeps the daemon alive and signing out hands it back to the clock.&lt;/p&gt;
&lt;h3&gt;Fixed: a headless daemon stops quitting every fifteen minutes&lt;/h3&gt;
&lt;p&gt;The daemon exits after fifteen idle minutes: the desktop spawns it and will spawn another when a window needs one, so a process left behind with nothing live is just a process left behind. On a server that rule inverts. Nothing is going to start it again, and &amp;quot;nothing live&amp;quot; is its resting state, which is the entire premise of being addressable at all.&lt;/p&gt;
&lt;p&gt;So it quit, systemd restarted it, and it read as &lt;code&gt;active&lt;/code&gt; to anyone who looked: eight restarts in two hours, each dropping the device channel and anything running on it. &lt;code&gt;termuna-daemon&lt;/code&gt; is resident now and says so at startup. The desktop's own daemon keeps the timeout.&lt;/p&gt;</description>
    </item>
    <item>
      <title>0.2.1</title>
      <link>https://termuna.com/changelog/#0.2.1</link>
      <guid isPermaLink="false">tag:termuna.com,2026:changelog/0.2.1</guid>
      <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
      <description>&lt;p&gt;Nothing in the app changed. 0.2.0 was built and published by hand, which skipped the release pipeline and, with it, the unversioned asset names every download button on termuna.com links to: those links 404'd until they were uploaded after the fact.&lt;/p&gt;
&lt;p&gt;This version exists to go out the way releases are supposed to, through the tag. The binaries are 0.2.0's, with the version string moved on.&lt;/p&gt;</description>
    </item>
    <item>
      <title>0.2.0</title>
      <link>https://termuna.com/changelog/#0.2.0</link>
      <guid isPermaLink="false">tag:termuna.com,2026:changelog/0.2.0</guid>
      <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
      <description>&lt;h3&gt;Added: a machine is a machine, with or without a session on it&lt;/h3&gt;
&lt;p&gt;The desktop is untouched by all this: it still spawns and owns its own daemon from its own binary, and there is nothing separate to install. The operator side of the server case is docs/SERVER.md.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;termuna-daemon&lt;/code&gt; is the daemon on its own: shells, the session tree, the mux socket and the relay, with no window bolted beside it. 10MB against the desktop's 32, and no GPU stack, fonts or display connection for a box in a rack to not use. &lt;code&gt;termuna-daemon login&lt;/code&gt; joins it to an account by typing what the sign-in screen asks; &lt;code&gt;installer/linux/install-daemon.sh&lt;/code&gt; fetches it, joins it and starts it as a user service that survives logout. Per-user, no root, nothing outside your home directory.&lt;/p&gt;
&lt;p&gt;A server can join by borrowing a token instead of being told a password: `TERMUNA_TOKEN=&amp;lt;any of the account's tokens&amp;gt; termuna-daemon login` mints this machine its own, with its own name and id. An account password typed into a box in a rack is a password that now lives in that box's shell history and in whatever provisioned it; the borrowed token is used for one call and never written down there.&lt;/p&gt;
&lt;p&gt;That is the easy half. The hard half was that a daemon had no address. It reached the relay once per live session it hosted, so a machine was only reachable *through* something it was already holding open, which is fine for a laptop and circular for a server: it could be signed in, listed among the account's devices, and impossible to ask for its first session, because asking required a session.&lt;/p&gt;
&lt;p&gt;So a daemon holds one connection of its own now (docs/PROTOCOL.md, the device channel), and the sessions drawer offers a machine whether or not anything is running on it. Two things fall out. Online is finally honest, asked of the machine rather than inferred from its sessions, which used to call a busy server gone and a closed laptop present. And a machine that has not published a key yet is still reachable the old way, so nothing has to be upgraded in step.&lt;/p&gt;
&lt;p&gt;Commanding a machine still costs the vault passphrase. The daemon holds a key of its own and publishes it wrapped to the account, exactly as a session's content key is: the relay stores a blob it cannot read, and a stolen device token buys what it bought before.&lt;/p&gt;
&lt;h3&gt;Added: the account's other sessions are yours to manage&lt;/h3&gt;
&lt;p&gt;Rename, kill and wake now reach the machine a session actually runs on. Before, every session was read-only from every screen but the one hosting it: the drawer's right-click menu did not even open on another machine's row, because it looked the session up in the local daemon's list and gave up when it was not there.&lt;/p&gt;
&lt;p&gt;The verbs are the same ones a local row offers and they are carried, not imitated: renaming a mirror here would have lasted until the next poll, and killing one would have ended the mirror while the shells went on running over there, which is not what the word means.&lt;/p&gt;
&lt;p&gt;Clicking a dormant remote row wakes it now instead of mirroring nothing. A dormant session has no connection to the relay, which is exactly why it needs waking, so the request travels through a live session on the same machine and the woken session comes back with a fresh key.&lt;/p&gt;
&lt;p&gt;A sealed row still offers no menu. Naming something is the one thing that cannot be done without being able to read its name.&lt;/p&gt;
&lt;h3&gt;Added: start a session on another one of your machines&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;+&lt;/code&gt; in the sessions drawer asks where, when there is more than one answer: this machine, or any other one the account can reach. Pick one and the session is created over there, mirrored here, and walked into, the same as clicking a row of that machine's would be.&lt;/p&gt;
&lt;p&gt;The phone could already do this and the desktop could not, which was backwards. Everything for it was on the wire already (&lt;code&gt;DaemonQuery&lt;/code&gt;, and a reply that carries the new session's share link sealed to the asking session's key). What was missing was the desktop's way to reach it: the GUI speaks no protocol but TSP to its own daemon, and the relay is the daemon's door, so the daemon now does the asking on the window's behalf (&lt;code&gt;MuxRequest::RemoteQuery&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;A machine is only offered when it has something live. A daemon has no address of its own: it reaches the relay once per session it hosts, so speaking to one means speaking through something it already holds open, and one holding nothing cannot be asked to start anything. Nor is a session offered as that conduit unless this device holds its key, since the question is sealed to it.&lt;/p&gt;
&lt;h3&gt;Fixed: macOS stops asking for the keychain on every launch&lt;/h3&gt;
&lt;p&gt;Two causes, one symptom.&lt;/p&gt;
&lt;p&gt;The app read the passphrase out of the keyring at boot and then, having unlocked, wrote the same bytes straight back, because &amp;quot;remember on this device&amp;quot; was true. Every touch of a macOS keychain item is a prompt, so auto-unlock cost two of them to accomplish nothing. It writes only when the value would change now.&lt;/p&gt;
&lt;p&gt;The rest was the signature. The bundle was ad-hoc signed and &lt;code&gt;deploy.sh&lt;/code&gt; re-signed it again on arrival, and an ad-hoc signature has no identity beyond its own hash. macOS binds a keychain item's permission to the signing identity that asked for it, so every build was a stranger and &amp;quot;Always Allow&amp;quot; never meant always. The build signs with a stable self-signed certificate held outside the repo now, and the install stops re-signing. The designated requirement is the bundle id and the certificate, both of which survive a rebuild.&lt;/p&gt;
&lt;p&gt;A stable identity was necessary and not sufficient. macOS gives an app free access to keychain items it created itself and guards everything else behind the item's access list, and the passphrase already on disk had been created by an earlier ad-hoc build. So the app went on being a stranger to its own item. Worse, &amp;quot;remember on this device&amp;quot; updated that item in place, inheriting its access list and re-arming the problem: the write replaces the item now, so what ends up on disk is always something this build owns.&lt;/p&gt;
&lt;p&gt;An item stranded by an older build has to go once, by hand, in Keychain Access. After that nothing asks.&lt;/p&gt;
&lt;p&gt;Every keychain touch is logged at info level, with its outcome, because each one is a dialog in the user's face. The outcome is the point: a refusal returns as fast as a grant, so a fast call is not evidence that nobody was asked, and reading it that way misdiagnosed this once.&lt;/p&gt;
&lt;h3&gt;Changed: on macOS the window is a macOS window&lt;/h3&gt;
&lt;p&gt;Termuna drew its own minimize/maximize/close top-right on every platform. On a Mac that is where no window button has ever been: the app was announcing itself as a port.&lt;/p&gt;
&lt;p&gt;So macOS keeps its real titlebar now, made transparent with its title hidden and the content running underneath (&lt;code&gt;fullsize_content_view&lt;/code&gt;). The system's own traffic lights float over our chrome bar, top-left, with their real colours, their real hover glyphs, and a green button that zooms the way a Mac user expects. We draw no window buttons there and leave 78px of room for the system's.&lt;/p&gt;
&lt;p&gt;The wordmark is gone from the bar on macOS with them. The top-left there belongs to the system's buttons, and the app's name is already in the menu bar where a Mac user looks for it; a wordmark beside the traffic lights says a second time what the screen already says once. The column it occupied stays, empty, because it is exactly as wide as the drawer beneath it and that is what keeps the panel toggles beginning where the grid does.&lt;/p&gt;
&lt;p&gt;Keeping the decorations hands back more than the buttons: the rounding, the shadow and the resize edges come with them, so &lt;code&gt;window_radius&lt;/code&gt; and our hand-rolled resize strips both stand down on macOS. They exist for the platforms that will not do it for us.&lt;/p&gt;
&lt;p&gt;Drawing lookalike lights ourselves would have meant redrawing another platform's own controls, which never survives contact with the real thing beside it.&lt;/p&gt;
&lt;h3&gt;Fixed: the unlock modal takes the caret with it&lt;/h3&gt;
&lt;p&gt;Clicking &amp;quot;Unlock&amp;quot; opened a modal that asks for exactly one thing and then made you click the field before you could type it. It takes focus on open now, from either way in (the drawer's locked row, the account screen). Enter already submitted and still does.&lt;/p&gt;
&lt;p&gt;The passphrase field carries two ids, one per place the form can appear: the account screen keeps its own copy and the modal can open over it, so a single id would name two widgets at once and focus would have to pick between them.&lt;/p&gt;
&lt;h3&gt;Fixed: the continuity panel reads differently on each machine&lt;/h3&gt;
&lt;p&gt;It read the same on all three, which is the one thing it exists not to do. A machine mirroring somebody else's session is itself a viewer at the relay, so the roster it fetches has itself in it, and the panel drew it verbatim under a fixed &amp;quot;this device&amp;quot; row: the computer you were sitting at appeared twice, once as &amp;quot;this device&amp;quot; and once under its own name. Identity is keyed on the device label, not the token id, for the reason the sessions drawer already keys on it: a token is minted fresh on every sign-in, so the id cannot say &amp;quot;this computer&amp;quot;.&lt;/p&gt;
&lt;p&gt;The machine actually running the session is named now too. A mirror's panel had listed every screen holding the session except the one it runs on, and said &amp;quot;Not shared&amp;quot; about a session it was watching arrive over the relay: it has no share link of its own, only the bridging host mints one, but the cloud list carries it, rebuilt from the content key the vault unwrapped. So the link and its QR are there on a mirror too.&lt;/p&gt;
&lt;h3&gt;Fixed: a mirrored conversation is one you can take part in&lt;/h3&gt;
&lt;p&gt;Opening another machine's agent session gave an empty chat, and anything typed into it vanished: the composer sat on &amp;quot;working…&amp;quot; while the far machine never heard a word.&lt;/p&gt;
&lt;p&gt;The mirror carried a terminal and only a terminal. &lt;code&gt;Layout&lt;/code&gt; and &lt;code&gt;Output&lt;/code&gt; came down, &lt;code&gt;Input&lt;/code&gt;, &lt;code&gt;Resize&lt;/code&gt; and &lt;code&gt;Command&lt;/code&gt; went up, and the whole agent lane, &lt;code&gt;AgentFrame&lt;/code&gt; down and &lt;code&gt;AgentCommand&lt;/code&gt; up, was dropped on the floor with a comment saying a terminal mirror had no use for it. For a managed agent session those frames are not a decoration on the session, they *are* the session: it has no PTY, and the mirror has no agent process of its own.&lt;/p&gt;
&lt;p&gt;Both directions now cross. Turns and asks arrive and are sequenced into the mirror's log like output, so a window that reattaches gets the conversation back. Token deltas and the attention nudge are passed through unsequenced, exactly as the far host sent them: the far host already decided those describe a moment and should not be replayed. Upward, saying something, answering a permission ask, interrupting and changing the permission mode all go up the link instead of looking for a local agent that does not exist, and an attached image goes with them, because writing it here would put the file on the wrong machine. The host validates what it is asked, which is where that belongs: &lt;code&gt;bypassPermissions&lt;/code&gt; stays refused.&lt;/p&gt;
&lt;h3&gt;Fixed: a conversation on another machine opens as one&lt;/h3&gt;
&lt;p&gt;Clicking a remote agent session still landed on a terminal, and for a managed agent that is a blank grid: such a session has no PTY at all. The window had already worked out it was a conversation, which is why the panel beside the empty grid read &lt;code&gt;claude · managed&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Two things asked the question too early. The drawer's cache cannot answer for a session this machine does not host, and the fallback (ask the layout when it lands) was answered by the wrong layout: the daemon stands a nameless placeholder tree up the moment it starts mirroring, so the attach has something to show, and replaces it when the far host's real tree crosses the relay a moment later. The placeholder said &amp;quot;not an agent&amp;quot;, and nobody asked again.&lt;/p&gt;
&lt;p&gt;So the placeholder no longer answers, and the question rarely reaches it now anyway: the cloud list already knows, which is what the row's glyph was drawn from.&lt;/p&gt;
&lt;h3&gt;Fixed: a conversation on another machine looks like one&lt;/h3&gt;
&lt;p&gt;A remote agent session wore the shell glyph in the sessions drawer, so the list called a Claude conversation a terminal until you opened it.&lt;/p&gt;
&lt;p&gt;The drawer had no way to know better. A local session carries its whole tree, agent and all; a remote one arrived as a name, a device and a liveness flag, because the relay stores the tree sealed and cannot read it to tell anyone what is inside. It hands the sealed tree over for live sessions now (termuna-website, 2026-08-10) and the desktop opens it with the content key it already unwraps for the title, so the row says what the local ones have always said. Dormant sessions still carry no tree: that would be most of a megabyte of ciphertext to decide a few glyphs.&lt;/p&gt;
&lt;p&gt;Reading it takes unwrapping a frame first, not just unsealing a blob: what the relay retained is the host's whole encoded &lt;code&gt;Layout&lt;/code&gt; frame, since its job is to replay those bytes to viewers verbatim.&lt;/p&gt;
&lt;h3&gt;Added: a machine with no connection says so&lt;/h3&gt;
&lt;p&gt;A device heading in the drawer whose sessions are all idle now carries a quiet &lt;code&gt;offline&lt;/code&gt;. Nothing on that machine opens and nothing new starts there, and a heading that looks like every other one promises otherwise.&lt;/p&gt;
&lt;p&gt;Only honest because the relay stopped believing a closed laptop (termuna-website, 2026-08-10). A suspended machine does not hang up, it goes quiet, so a MacBook with its lid shut counted as a live host for hours.&lt;/p&gt;
&lt;h3&gt;Added: Termuna runs on macOS, built from Linux&lt;/h3&gt;
&lt;p&gt;There is now a macOS build, and it is produced the same way the Windows one is: cross-compiled on the Linux box, shipped to the Mac already assembled. &lt;code&gt;scripts/build-macos-app.sh&lt;/code&gt; links an arm64 binary with clang and lld: no osxcross, no zig, nothing on the host that the Windows cross build did not already install, and wraps it in &lt;code&gt;Termuna.app&lt;/code&gt; with an icon drawn from the same moon-cursor geometry as every other surface. &lt;code&gt;scripts/macos-qa/deploy.sh&lt;/code&gt; installs it per-user into &lt;code&gt;~/Applications&lt;/code&gt; and launches it in the logged-in session.&lt;/p&gt;
&lt;p&gt;The one thing that cannot be cross-compiled away is Apple's SDK, which may not be redistributed; &lt;code&gt;docs/MACOS-QA.md&lt;/code&gt; says how to copy one off a Mac and covers the rest of the loop, including the two permissions macOS demands before an SSH session may screenshot or type.&lt;/p&gt;
&lt;p&gt;First run on an M4: Metal selected on its own, the daemon spawned and a shell attached in 97 ms, first frame at 100 ms, inside the 300 ms cold-start budget.&lt;/p&gt;
&lt;h3&gt;Added: another machine's session opens here, not in a browser&lt;/h3&gt;
&lt;p&gt;Clicking a session on one of the account's other machines used to open a browser tab. That was expedient, not right: the desktop is a terminal.&lt;/p&gt;
&lt;p&gt;It now opens in the window. &lt;code&gt;termuna-mux::remote&lt;/code&gt; is the mirror image of the cloud bridge: there this daemon is a session's *host* and pushes it to the relay, here it is a *viewer* and pulls one down. Same wire, same content key, opposite direction. The mirror is registered as an ordinary &lt;code&gt;SessionHost&lt;/code&gt;: same registry, same broadcast channel, same &lt;code&gt;attach_viewer&lt;/code&gt;. So the window opens another machine's session by attaching to its own daemon, exactly as it does for a shell downstairs, and not one line of the UI knows the bytes crossed the internet, which is the layering rule, and it keeps the network and the keys in the process that already holds both.&lt;/p&gt;
&lt;p&gt;Only two places know the difference: input goes up the link instead of into a shell, and a structural change (a split, a resize) is a *request* rather than an edit: the far host makes it and the layout comes back. Acting locally would fork the tree between two machines, only one of which runs the session. Mirrors are never listed and never persisted: a view of somebody else's session must not come back after a restart as a dormant session of ours.&lt;/p&gt;
&lt;p&gt;A guest's resize does reach the host, and the host takes its own size back when the guest leaves: the relay side of that already existed (smallest size across live viewers, guest sizes expiring 15s after the guest stops asserting them). The gap was ours: the window only sends a resize when its own window changes, so a mirror would have been forgotten mid-watch and the session would have snapped back under us. The mirror now repeats its size every 5s.&lt;/p&gt;
&lt;h3&gt;Changed: a locked machine is one row, and that row is the way in&lt;/h3&gt;
&lt;p&gt;A machine whose sessions cannot be read yet used to print one &amp;quot;sealed&amp;quot; row per session and offer a faint line of text at the foot of the drawer to do something about it. Both halves were wrong. N identical anonymous placeholders are noise, and noise with no remedy attached is the worst kind; and the exit was quiet in exactly the situation where a person is most stuck, sitting far from the thing it fixes.&lt;/p&gt;
&lt;p&gt;Each such machine is now a single bordered row under its own heading: a lock, how many sessions are there, and &lt;code&gt;unlock&lt;/code&gt;. It says the true thing (that there are two of them and that they are shut) and it is itself the button. From &lt;code&gt;docs/design/termuna-ui-v6.html&lt;/code&gt;, where the three states are walkable with &lt;code&gt;?s=solo|locked|open&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;All or nothing, never half: a session's title is wrapped to the account key the moment its host hears the account has one, and refreshed on every reconnect, so either the account has no key and nothing is readable, or it has one and this device is locked, or it is unlocked and everything is. Checked against production before relying on it: zero sessions with a device and no wrapped key.&lt;/p&gt;
&lt;h3&gt;Added: the passphrase can be entered where the lock is felt&lt;/h3&gt;
&lt;p&gt;The vault passphrase was asked for on the ssh screen and nowhere else, so a signed-in person looking at sessions named &amp;quot;sealed&amp;quot; had no way to learn what the word meant or what to do about it: the key was asked for in the one place they had no reason to look. One form now, three doorways: the ssh screen, the drawer (an &amp;quot;unlock to read names&amp;quot; row, which appears only while something is sealed), and the account screen, where a locked passphrase is a row that says what it costs. Unlocking closes the card and re-reads the names immediately rather than leaving &amp;quot;sealed&amp;quot; up until the next minute-tick.&lt;/p&gt;
&lt;h3&gt;Added: the sessions drawer knows about your other machines&lt;/h3&gt;
&lt;p&gt;Sign in on a second machine and the drawer stops being a list of this computer and becomes a list of the account. Live and idle stay the top division, because that is the one that decides what you can do: a live session takes a keystroke now, an idle one has to be woken first, and the machines are the division inside each, this device always first. Never the other way round: group by machine at the top and &amp;quot;what is running&amp;quot; ends up scattered across every heading.&lt;/p&gt;
&lt;p&gt;Only machines running a daemon appear. The relay stamps a session's device from the token that attached as its *host*, and only a daemon ever does that: a phone attaches as a viewer, so a phone cannot become a heading here. It stays in the account's device list, where it means something.&lt;/p&gt;
&lt;p&gt;With one machine signed in nothing changes: nesting a single group under a single heading buys nothing and costs a line.&lt;/p&gt;
&lt;p&gt;Which machine you are on is worked out, not looked up: `[cloud] device_id` only exists if the install signed in after that field was added, and trusting it made an older install list its own computer as a stranger, under its hostname, beside the sessions it had already printed. The daemon is the authority on what it hosts: every row the relay reports that the daemon also has is ours, and the device stamped on those rows is this machine's whatever the config believes.&lt;/p&gt;
&lt;p&gt;The pieces for this had been in the protocol since M9 and unused: &lt;code&gt;AccountInfo&lt;/code&gt; hands the host the account's vault public key, and the host answers with &lt;code&gt;SessionMeta&lt;/code&gt;: the session's content key wrapped to the account, and the title sealed with that key. The relay stores both and can read neither. So the desktop lists the account's sessions with the device token alone, and opens their names with the vault passphrase: locked, a machine shows how many sessions it has and calls them &amp;quot;sealed&amp;quot;; unlocked, they have names. Nothing is faked in between.&lt;/p&gt;
&lt;p&gt;Clicking one opens it in the browser through a share link rebuilt from the unwrapped key: a session this machine was never handed a link to. The desktop is a host, not a viewer, so that is the honest way in.&lt;/p&gt;
&lt;h3&gt;Fixed: signing out lets go of the account's sessions, and says so first&lt;/h3&gt;
&lt;p&gt;A session bridged under one account stayed bound to it forever. Sign in on that machine as somebody else and the daemon reconnects the same session id with the new token; the relay's upsert carried a &lt;code&gt;WHERE sessions.user_id = ?&lt;/code&gt; guard, so the row silently refused to move while the socket stayed open and the host streamed happily into a session its own dashboard would never list. The app showed it live, synced, with a share link. The cloud had never heard of it. Nothing anywhere said why.&lt;/p&gt;
&lt;p&gt;Sign-out now asks first, in a card that names what it costs: this machine stops syncing, N sessions leave the cloud and their share links stop working, and (the half worth saying) they keep running here. Confirming releases each of them from the account (`POST /v1/sessions/{id}/release&lt;code&gt;, which unlike &lt;/code&gt;DELETE` does not refuse a running session, because that is exactly the case) before the credentials that prove they are ours are dropped. A relay that cannot be reached does not block it: leaving an account has to work offline.&lt;/p&gt;
&lt;p&gt;On the relay, a host attaching to another account's session is now refused outright with a reason instead of accepted and orphaned, and the &lt;code&gt;SessionMeta&lt;/code&gt; write is guarded by owner: it had none, so the new account's host was overwriting the old account's wrapped key with one wrapped to the wrong vault, leaving a row its owner could no longer open.&lt;/p&gt;
&lt;h3&gt;Fixed: signing out signs the daemon out too, on Windows as well&lt;/h3&gt;
&lt;p&gt;The window signed out and the daemon carried on. The call that tells it to drop its cloud settings was wrapped in &lt;code&gt;#[cfg(unix)]&lt;/code&gt;: a leftover from before the daemon spoke over a named pipe (&lt;code&gt;MuxConn::connect&lt;/code&gt; has had a Windows implementation since 2026-07-23, and &lt;code&gt;set_cloud&lt;/code&gt; was never gated). So on Windows a sign-out cleared the window and left the daemon bridging every live session to the relay under the old device token, still minting share links, until somebody restarted it. It is no longer conditional on the platform.&lt;/p&gt;
&lt;p&gt;Sign-out also took only half of the account with it. &lt;code&gt;clear_cloud_auth&lt;/code&gt; removed &lt;code&gt;token&lt;/code&gt; and &lt;code&gt;email&lt;/code&gt; and set &lt;code&gt;enabled = false&lt;/code&gt;, leaving &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;avatar_url&lt;/code&gt; and &lt;code&gt;device_id&lt;/code&gt; in &lt;code&gt;config.toml&lt;/code&gt; and their copies in memory. The next account to sign in on that machine was greeted by the previous one's name and picture until a profile fetch happened to correct them, and the stale &lt;code&gt;device_id&lt;/code&gt; named a device row the install no longer owned. Every key &lt;code&gt;[cloud]&lt;/code&gt; holds about a person now goes; the relay and share-base URLs a self-hoster set stay, because those are not the account's to take away.&lt;/p&gt;
&lt;h3&gt;Added: point a phone at the share link&lt;/h3&gt;
&lt;p&gt;The continuity panel can now show the share link as a QR code, under &amp;quot;Scan with phone&amp;quot;. A share URL is a capability: &lt;code&gt;…/s/&amp;lt;session&amp;gt;#k=&amp;lt;key&amp;gt;&lt;/code&gt; which nobody is going to type into a phone, and copying it there needs a channel that then holds the key. A camera needs neither.&lt;/p&gt;
&lt;p&gt;The code is generated on this machine (&lt;code&gt;qrcodegen&lt;/code&gt;, no dependencies of its own, no network): running a capability URL through somebody's QR service would hand them the session, which is the same rejection the relay gets. It is hidden until asked for, and hides again with a second click, because the line the panel prints beside it deliberately shows the place and not the key, and a QR shows everything to anyone who can see the screen. It follows the link it stands for: a session that re-bridges is issued a new key, and the code is regenerated rather than left pointing a phone at something it can no longer decrypt.&lt;/p&gt;
&lt;p&gt;It is drawn as a light card even on a near-black panel: a reader thresholds on luminance and the inverted-code case is still where cheap scanners give up. Verified by decoding it back out of a screenshot of the running app: the payload matches the URL the daemon minted, key fragment included, and still reads at half the screen's resolution.&lt;/p&gt;
&lt;h3&gt;Added: the window has corners&lt;/h3&gt;
&lt;p&gt;Termuna's window is rounded now: 9px, square again while maximized, because a rounded corner flush against the screen edge is not a corner but a notch showing the desktop through a screen that should be full.&lt;/p&gt;
&lt;p&gt;The window has drawn its own chrome since v6, so the rounding is drawn too rather than asked for: the surface is transparent, and the two widgets that own the window's corners: the chrome bar at the top, the status bar at the bottom: carry the radius. Under them the view sits on an opaque ground rounded to the same radius, because a transparent surface means a widget that paints no background of its own no longer falls back on the application's colour but falls all the way through to the desktop; the split dividers, which were deliberately drawn at &lt;code&gt;alpha 0.001&lt;/code&gt;, did exactly that, and now paint their own seam. The only see-through pixels in the window are the four corners. Without a compositor those pixels are undefined and the window simply looks the way it looked before.&lt;/p&gt;
&lt;h3&gt;Fixed: a narrow window keeps its titlebar&lt;/h3&gt;
&lt;p&gt;Below 720px the chrome bar used to disappear: v6 defines that width as the grid and the status bar and nothing else. That is right for what the bar draws and wrong for what it *is*: with OS decorations off it is the window's only title, drag handle and set of window controls, so a narrow window could not be moved, minimized, maximized or closed with the mouse, and lost its two upper corners with the bar. The bar is now in every width class, stripped at quake to the mark, the session name and the three controls: the panel pins go, because there is nothing to pin at that width and they were dead buttons, and the share link goes for room. The tab strip is still the first thing a narrow window gives back.&lt;/p&gt;
&lt;h3&gt;Fixed: one right-click, one menu; a tab looks clickable&lt;/h3&gt;
&lt;p&gt;Right-clicking a tab opened two menus stacked on each other: the tab's own, and the pane context menu behind it. Canvas events reach every pane whether or not the cursor is on that pane, and the terminal's right-press handler took the cursor's window-absolute position without first asking whether the click had landed inside its own bounds, so it answered right-clicks meant for the tab strip, and for the drawers and panels too. It now opens only for a click that is actually in the pane, which also means a right-click in a split can no longer be attributed to its neighbour.&lt;/p&gt;
&lt;p&gt;Tabs also show the pointer cursor on hover now, instead of the plain arrow. The close × already did; the tab itself did not, and a click target that does not say so reads as decoration.&lt;/p&gt;
&lt;h3&gt;Changed: asking a tab a question no longer switches to it&lt;/h3&gt;
&lt;p&gt;Right-clicking a tab used to activate it first, which meant you could not so much as read the menu of another tab without leaving the session you were working in. It did that because every row the menu offers (move left/right, close to the right, close others) acted on whatever tab was active. Those rows now name the tab the menu belongs to, so the menu can be opened, read and used on any tab while the one you are working in stays on screen. The keyboard shortcuts beside them still belong to the active tab, so they are shown only when that is the same tab; a hint that would do something else is worse than no hint. Moving a tab from the menu also closes it, instead of leaving it hovering over the slot the tab has just left.&lt;/p&gt;
&lt;h3&gt;Fixed: dragging a tab feels like carrying it&lt;/h3&gt;
&lt;p&gt;Tab drag-reorder was rebuilt. Three things were wrong with it:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;It never let go.&lt;/strong&gt; The release was only heard by the strip itself, so a drag that ended anywhere else (over the grid, over a panel) left the tab still attached; the next time the pointer crossed the strip, with no button held, the tab shuffled again. The drag is now followed at the window and ends wherever you let go.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;It landed in the wrong slot.&lt;/strong&gt; The drop target was computed from a tab width the drag worked out for itself, and its arithmetic had forgotten the search and split glyphs at the end of the strip. Past a handful of tabs it disagreed with the strip you were looking at. There is now one definition of the slot pitch and both read it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;It juddered.&lt;/strong&gt; The target came from &amp;quot;which slot is the pointer in&amp;quot;, but the strip reorders under the pointer, so the tab chased itself across every boundary. It is now measured from the press: half a slot of travel per swap, in either direction, and a drag out and back leaves the tab exactly where it started.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;The chip you carry was redrawn to match the rest of the app: chrome tokens instead of the terminal colour scheme, the tab's own pitch and ordinal and mono face, the accent rule the active tab wears, and a neutral depth shadow: it floats, so it casts a shadow, and never a glow. It also sits where the tab would be rather than 50px left of the pointer, which is where the old ghost drew itself whenever the sessions drawer was pinned. The pointer reads the strip as a grab surface while a tab is in hand.&lt;/p&gt;
&lt;h3&gt;Fixed: a rebuilt shell starts on a clean screen&lt;/h3&gt;
&lt;p&gt;The &amp;quot;session rebuilt&amp;quot; line, the new prompt and (for an agent session) the program restarted into it were all being drawn on top of the screen the dead process left behind: interleaved with its corpse, with the cursor stranded wherever that process had parked it. A pane whose shell has just been replaced now clears its screen first. The scrollback above is untouched: that is history, and it is the reason to resurrect a session at all.&lt;/p&gt;
&lt;h3&gt;Fixed: a repainting pane's log is recognised even when it starts mid-repaint&lt;/h3&gt;
&lt;p&gt;The last hole was the one that could not be seen from inside the bytes. A pane's retained log is capped, so for a program that repaints continuously the window almost always begins *inside* the repaint, the sequence that entered the alternate screen was evicted hours ago, and nothing that remains says what it is. The filter had nothing to recognise, so it replayed the lot: fragments of frames scattered across the screen, which is what a resurrected agent session looked like.&lt;/p&gt;
&lt;p&gt;The daemon watched those bytes go past, so it is the one that knows. It now records which panes were repainting when it last saved the session, and a log read back for those panes is discarded rather than replayed. Logs written before this carry no such record; the ones belonging to agent sessions were moved aside on this machine, since their contents could never have been shown.&lt;/p&gt;
&lt;h3&gt;Fixed: no program's modes outlive the program&lt;/h3&gt;
&lt;p&gt;The clicks that printed &lt;code&gt;64;51;11M&lt;/code&gt; had one more hiding place: the chunk in which a program takes the alternate screen usually turns mouse reporting on in the same breath, and that chunk was still being kept as history. Replay it at a pane now running a fresh shell and the mirror believes the mouse is wanted again.&lt;/p&gt;
&lt;p&gt;An alt-screen span now leaves the log whole: the entering chunk with it, and the same filter runs over logs read back off disk, so sessions recorded by older builds are cleaned as they load. On top of that, every pane's mirror is handed a switched-on terminal (no alternate screen, no mouse reporting, no bracketed paste, cursor shown, attributes and scroll region cleared) before a single line of history is replayed onto it.&lt;/p&gt;
&lt;h3&gt;Fixed: a rebuilt session no longer types mouse codes at you&lt;/h3&gt;
&lt;p&gt;Scrolling in a session the daemon had rebuilt printed streams of &lt;code&gt;64;47;22M&lt;/code&gt; into the shell. The modes belonged to the program that died with the old daemon: a full-screen TUI turns mouse reporting on and every viewer's mirror still held them, because a mirror is not restarted when the shell under it is replaced. The wheel dutifully sent mouse reports; the fresh bash had never asked for any, so it printed them as text.&lt;/p&gt;
&lt;p&gt;A rebuilt session now says outright that its terminal is new: leaving the alternate screen, mouse reporting, bracketed paste and application cursor, cursor visible, scroll region and attributes reset: sent to every viewer just before the &amp;quot;session rebuilt&amp;quot; line.&lt;/p&gt;
&lt;h3&gt;Fixed: repaint frames are no longer kept as history&lt;/h3&gt;
&lt;p&gt;Output a program writes while it holds the alternate screen is one screen being redrawn, not scrollback. It was being retained anyway: kept in the replay log, persisted to disk, and handed back on the next attach or resurrection, where it could only arrive as fragments of frames. It is now sent live and not kept, which also stops a single TUI session from filling the retained log within minutes, as the roadmap noted it did.&lt;/p&gt;
&lt;h3&gt;Fixed: reattaching to a full-screen program no longer shows torn glyphs&lt;/h3&gt;
&lt;p&gt;Opening Termuna onto a session running Claude Code (or vim, or htop) painted a mangled screen: fragments of several redraws stacked on each other, words fused together, nothing readable.&lt;/p&gt;
&lt;p&gt;The cause is one the roadmap already described for phones, which the desktop had been spared only because it used to stay attached from the moment a session began: until launch started walking into an existing session. A joining viewer builds its screen by replaying the daemon's retained output, and for a program that repaints in place that window holds the tail of a repaint stream: half-drawn frames with no beginning. Replaying it can only produce garbage.&lt;/p&gt;
&lt;p&gt;So the daemon no longer replays it. It now tracks which panes hold the alternate screen (watching the mode sequences go past: it still does not emulate), replays each such pane's history only up to the moment the program took the screen, puts the viewer's mirror on the alternate screen, and nudges the pty size so the program repaints itself in full. The viewer gets one clean frame, drawn by the program, at the right size, and the shell history from before is intact underneath, back on screen the moment the program exits.&lt;/p&gt;
&lt;h3&gt;Added: eleven things a daily-driver terminal is expected to have&lt;/h3&gt;
&lt;p&gt;Surveyed against Tabby (the terminal the maintainer was switching from) and picked what earns its weight:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Paste guard.&lt;/strong&gt; A multi-line paste shows what is about to run, line count, character count, the text itself: before a single newline reaches the shell, because a paste is keystrokes and the second command runs before the first can be read. Every paste is also normalised: CRLF and lone CR become LF (a Windows clipboard would otherwise submit twice) and trailing whitespace goes. &lt;code&gt;[terminal] warn_on_multiline_paste&lt;/code&gt;, &lt;code&gt;trim_paste&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Search options.&lt;/strong&gt; Match case, whole word and regular expressions, as three toggles beside the hit counter. A half-typed pattern finds nothing rather than erroring.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Zoom that stays zoomed.&lt;/strong&gt; Ctrl+Shift+plus/minus now writes the size to the active appearance profile, and Ctrl+Shift+0 returns to what that profile says.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Copy current path&lt;/strong&gt; (Ctrl+Shift+P): the focused pane's working directory to the clipboard.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Word separators&lt;/strong&gt; (&lt;code&gt;[terminal] word_separators&lt;/code&gt;): what a double-click treats as one word. The default keeps paths and URLs whole.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Reopen closed tab&lt;/strong&gt; (Ctrl+Shift+R), five deep.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Broadcast input&lt;/strong&gt;: type into every pane of a tab (Ctrl+Shift+I) or every tab of the session (Ctrl+Alt+Shift+I): four servers, one keystroke. A warm border and a line above the grid say it is on; esc or a click in any pane ends it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Maximize pane&lt;/strong&gt; (Ctrl+Shift+Z): the focused pane fills the tab and gives it back on the next press. The others keep running.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Directional pane focus&lt;/strong&gt; (Ctrl+Alt+arrows): the pane actually to the left, by geometry, not the next one in the tree.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Notifications&lt;/strong&gt; for background tabs: one when a quiet tab stirs, one when a command rings the bell. Both off by default: &lt;code&gt;[terminal] notify_on_activity&lt;/code&gt;, &lt;code&gt;notify_when_done&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Progress in the status bar.&lt;/strong&gt; Long commands announce themselves in the only language they all speak (&lt;code&gt;47%&lt;/code&gt;) and the status bar reads it back. &lt;code&gt;[terminal] detect_progress&lt;/code&gt;.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;Also: a &lt;strong&gt;minimum contrast floor&lt;/strong&gt; (&lt;code&gt;[terminal] min_contrast&lt;/code&gt;, WCAG ratio) that lifts unreadable foregrounds: the fix for a scheme's own dark blue on a dark background. Off by default at 1.0: a colour scheme is its author's work until the user says otherwise.&lt;/p&gt;
&lt;h3&gt;Changed: the right button belongs to the terminal, always&lt;/h3&gt;
&lt;p&gt;Right-clicking inside a full-screen TUI (Claude Code, vim) forwarded the click to the application and left no way to reach copy, split, search or close-pane: exactly when a user wants them. Mouse reporting now gets the left button, the drags and the wheel; the context menu is ours in every mode, no modifier needed. This matches the terminals people switch from (Tabby intercepts right-click before its emulator sees it) and costs almost nothing: TUIs binding button 3 are vanishingly rare, and one still exists: shift is not needed for the menu any more, so the hint below simply teaches selection.&lt;/p&gt;
&lt;h3&gt;Added: the status bar says who owns the mouse&lt;/h3&gt;
&lt;p&gt;Drag inside a full-screen TUI that tracks the mouse: Claude Code, vim, htop, and nothing gets selected, because the application asked for those events and gets them. Termuna has always had the escape hatch (hold shift and the mouse is the terminal's again: select, copy, and shift+right-click for the pane menu), but nothing said so, so it read as a broken terminal.&lt;/p&gt;
&lt;p&gt;Now the first drag an application swallows lights the answer in the status bar, in place of the session name: &amp;quot;the app owns the mouse · hold shift to select, shift+right-click for the menu&amp;quot;. It leaves when the button does. No popup, no toast: the line that was already there simply answers the question being asked.&lt;/p&gt;
&lt;h3&gt;Fixed: a deleted (or revoked) shared vault now leaves the machine&lt;/h3&gt;
&lt;p&gt;When an owner deleted a shared vault, or revoked this account's membership: the other machines kept showing it forever, and worse: their next sync quietly *recreated* the vault in the cloud from local copies, owned by whoever synced first. Two changes close it:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Sync never creates a shared vault from a name's mere presence.&lt;/strong&gt; The only creations a sync performs come from a durable record of this device's own explicit &amp;quot;New vault&amp;quot; actions. Local copies of a vault someone deleted have no power to bring it back.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The cloud's shared-vault list is reconciled before the push.&lt;/strong&gt; Vaults it does not mention disappear locally, connections included. The list is never substituted with an empty default, so a failed fetch cannot masquerade as &amp;quot;everything was deleted&amp;quot;.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: the wordmark is back, on the chrome bar&lt;/h3&gt;
&lt;p&gt;The brand came off with the old picker and never landed anywhere. It now rides the chrome bar's top-left: beside a pinned drawer the wordmark claims the drawer's column and the panel toggles begin where the grid does; narrower layouts carry the compact mark.&lt;/p&gt;
&lt;h3&gt;Changed: the organization's owner manages its vaults here too&lt;/h3&gt;
&lt;p&gt;The cloud now grants an org's owner management over vaults shared inside that organization; the desktop follows the server's &lt;code&gt;manage&lt;/code&gt; verdict instead of the raw membership role. For an org owner, a teammate's shared vault is no longer read-only: hosts edit, members revoke, the vault deletes, while plain members keep the read-and- clone model.&lt;/p&gt;
&lt;h3&gt;Changed: the share popup says who owns the vault&lt;/h3&gt;
&lt;p&gt;The members list now shows everyone including yourself (&amp;quot;(you)&amp;quot;), owner first, each with their role chip, plus one line of the rule: members read and use, only the owner edits, shares and deletes. Revoke buttons appear only when you are that vault's owner: they were shown to everyone before, promising a verb the server refuses.&lt;/p&gt;
&lt;h3&gt;Changed: shared vaults are read-only for members&lt;/h3&gt;
&lt;p&gt;A vault shared with you is now yours to use, not to manage: matching the server, which refuses member writes since the same-day cloud change. The desktop knows its role in every shared vault from the sync: read-only vaults say so in the connections tree (&amp;quot;shared · read-only&amp;quot;, no members/delete buttons), their hosts' menus keep connect but disable edit/remove/share (&amp;quot;owner only&amp;quot;) and offer &lt;strong&gt;clone to My connections&lt;/strong&gt; instead: an editable copy in your own vault. Read-only vaults are dropped from move targets and the host form's vault picker, and sync no longer pushes into them (a round trip per profile for a stack of 403s).&lt;/p&gt;
&lt;h3&gt;Changed: the command palette grew into a real launcher&lt;/h3&gt;
&lt;p&gt;Ctrl+K is now organized like the launchers it competes with: a search row with its glyph, small-caps section headers, an icon on every row, and a footer that spells the keys (esc close · ↑↓ navigate · ↵ open). Three sections: &lt;strong&gt;actions&lt;/strong&gt; (tabs, splits, search, screens, settings), &lt;strong&gt;sessions&lt;/strong&gt; (every known session with its freshness: &amp;quot;live&amp;quot;, &amp;quot;3h ago&amp;quot;, and provider), and &lt;strong&gt;ssh connections&lt;/strong&gt; (every saved host as &lt;code&gt;user@host:port&lt;/code&gt;, one Enter from a shell on that machine). Before a keystroke the palette shows a curated front page: all actions, the five freshest sessions, the first five hosts; typing searches everything, label, meta and section alike, so &amp;quot;ssh prod&amp;quot; finds the prod box.&lt;/p&gt;
&lt;h3&gt;Fixed: opening links on Windows opens the browser, not Explorer&lt;/h3&gt;
&lt;p&gt;&amp;quot;open&amp;quot; on the continuity panel's share link (and every other link the app opens) went through &lt;code&gt;explorer &amp;lt;url&amp;gt;&lt;/code&gt;, which drops the URL's &lt;code&gt;#fragment&lt;/code&gt; (where a share link carries its decryption key) and then falls back to a File Explorer window. Links now go through the shell's own URL handler (&lt;code&gt;ShellExecuteW&lt;/code&gt;), fragment intact. Verified on the Windows QA machine: the share link lands in the default browser and the web viewer decrypts the live session.&lt;/p&gt;
&lt;h3&gt;Fixed: the Windows build compiles again&lt;/h3&gt;
&lt;p&gt;The cloud bridge's dead-link detection (&lt;code&gt;TCP_USER_TIMEOUT&lt;/code&gt;) is a Linux-only socket option and broke the MSVC target when it landed; it is now cfg-gated to Linux/Android. Windows keeps the keepalive schedule and relies on the relay-side heartbeat for blackholed links. Verified live on the Windows QA machine.&lt;/p&gt;
&lt;h3&gt;Fixed: the tab strip's verbs can no longer be pushed off screen&lt;/h3&gt;
&lt;p&gt;With enough tabs open, the search and split buttons (and eventually the +) slid out of the window: the strip trusted a width estimate, and the buttons' default padding made them wider than the estimate said. The right end of the strip is now a fixed section the tabs physically cannot displace: the flexible part is clipped instead, and the controls carry explicit padding so the tab math and the layout agree. Docs: CONFIG.md now also documents the &lt;code&gt;[quake]&lt;/code&gt; global-hotkey section.&lt;/p&gt;
&lt;h3&gt;Removed: the full-window picker; the app is the session window now&lt;/h3&gt;
&lt;p&gt;The old home screen (sidebar + full-page sessions/agents/mcp/ssh screens) is gone. The v6 session window already carried everything it offered: the drawer lists and switches sessions, the management screens open over the grid, so the picker had become a second, inconsistent way to show the same things. There is no longer any destination outside a session:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Launch always lands in a session.&lt;/strong&gt; The one the window was closed on when the daemon still has it (resurrected if it had gone idle); otherwise the newest live session; otherwise a fresh shell. The first launch opens at 1440x860: wide enough that the sessions drawer and the continuity panel both hold their columns, so the whole product is on screen at once. After that the window reopens at whatever size it was closed (the minimum stays 400x260; every narrower density still works).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Ending a session never strands the window.&lt;/strong&gt; &lt;code&gt;exit&lt;/code&gt; in the last shell (or killing the current session) walks into the newest other live session, or starts a fresh shell when it was the last one. A dormant session is never resurrected uninvited.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Failures live on the connecting screen.&lt;/strong&gt; A daemon that cannot be reached or an attach that fails shows the error in-window with retry and new-session actions: there is no picker to fall back to.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Quake can summon screens too.&lt;/strong&gt; A palette command opens agents/mcp/ssh/settings over the grid at any width now; quake no longer detours anywhere (the old behavior left for the picker).&lt;/li&gt;&lt;li&gt;The &amp;quot;back to sessions&amp;quot; actions (palette, chat header) now open the sessions screen over the grid: the full filterable list the old picker had, same rows, same badges.&lt;/li&gt;&lt;li&gt;Internals for future work: &lt;code&gt;Phase&lt;/code&gt; is just &lt;code&gt;Connecting&lt;/code&gt;/&lt;code&gt;Terminal&lt;/code&gt;, &lt;code&gt;app/picker.rs&lt;/code&gt; became &lt;code&gt;app/screens.rs&lt;/code&gt; (the management screens + modals, one router &lt;code&gt;view_nav_content&lt;/code&gt;), &lt;code&gt;PickerNav&lt;/code&gt; is &lt;code&gt;NavScreen&lt;/code&gt;, and the picker-only sidebar/rail/cross-fade/slide machinery is deleted. The &lt;code&gt;[ui] sidebar_collapsed&lt;/code&gt; key is parsed but ignored.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: one window, three densities (UI v6)&lt;/h3&gt;
&lt;p&gt;The session window now scales its chrome with its width (docs/design/termuna-ui-v6.html): power grows with the pixels you give it, and growing the window never adds chrome by itself:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Narrow (under 720px, quake):&lt;/strong&gt; the terminal grid and the status bar. Nothing else: the drop-down window costs zero chrome.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Default:&lt;/strong&gt; the tab strip and the grid, as before. The sessions drawer and the continuity panel open on demand as overlays (toggles in the tab strip / chat header) and give every pixel back on esc or a click outside.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Wide (1400px and up):&lt;/strong&gt; a pin turns each panel into a real column, sessions drawer on the left (live and dormant sessions, attention badges, agents/mcp/ssh/settings nav, account), continuity panel on the right (which screens hold the session now, the share link, the session's facts, the encryption state). Pins persist across restarts.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;From inside a session, the nav no longer leaves for the picker: agents, mcp servers, ssh, settings and account open in the centre column over the grid, behind a bar with the way back: the shells keep running underneath, and esc returns to them. Switching sessions happens in place from the drawer, including into an agent's chat.&lt;/p&gt;
&lt;p&gt;New: a status bar on every width: the mark, the session and where it is, this process's measured memory, and whether it syncs (e2e) or stays local.&lt;/p&gt;
&lt;p&gt;New: the window comes back the way you left it. Closing (the × or the WM's close) persists the unmaximized size, whether the window was maximized, and the session on screen; the next launch opens at that size, re-maximizes if needed, and walks straight back into that session: resurrecting it if it had gone idle. Leaving for the picker first means the next launch lands on the picker, and a session that no longer exists falls back there too.&lt;/p&gt;
&lt;p&gt;New: the window draws its own chrome bar (OS decorations are off), panel pins on the left, the window's name centred, the session's web link and minimize/maximize/close on the right. The bar drags the window, a double click maximizes, and invisible edge strips take over border resizing. Quake width shows no chrome at all, as before.&lt;/p&gt;
&lt;p&gt;Luna Dark now paints the grid the way the design draws it: ink-black background, quiet grey prose, and the chrome's accent family as the ANSI hues (green &lt;code&gt;#4fd6a8&lt;/code&gt;, blue &lt;code&gt;#6e9fd4&lt;/code&gt;, red &lt;code&gt;#d96a5f&lt;/code&gt;, yellow &lt;code&gt;#d6a24f&lt;/code&gt;): a default prompt looks exactly like the mockup. The ramp stays honest (green is green); the cursor keeps the luna brand teal. The active tab now merges with the grid and carries a 2px accent on its top edge; tab titles are set in the mono face. The default grid metrics are the design's too: 12.5px JetBrains Mono at 1.65 line height (existing configs keep whatever they say). Grid text is pixel-snapped, and bold text renders the real Bold face: the bundled bold TTFs carried a wrong weight class (558), which made the renderer lay synthetic bold over the already-bold outlines: every prompt came out smeared and overweight.&lt;/p&gt;
&lt;h3&gt;Fixed: a conversation you come back to comes back with itself&lt;/h3&gt;
&lt;p&gt;Three faults, one symptom: opening a chat you had left gave you an empty screen, or a screen where the agent looked like it was re-running your last message.&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;The conversation was never written to disk.&lt;/strong&gt; The session log is persisted while frames arrive, at most every three seconds, and the frames that matter are the last ones. A turn ends, nothing else is sent, and everything since the previous write is still only in memory, so a dormant chat came back empty. It is now written at the moments the flow stops: the end of a turn, an ask the agent is blocked on, and when the agent process goes.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&amp;quot;Continue&amp;quot; from the agents list did not continue anything.&lt;/strong&gt; The session id being resumed was dropped on the way into session creation, so it started a *fresh* conversation in the same directory: a stranger who had never met the work. It is passed through now.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;A resumed conversation now arrives with its history.&lt;/strong&gt; Such a session is new and has no log, and the CLI loads its history into the model's context without saying a word of it on the stream. The host reads the provider's own transcript once, at resume, and seeds the replay log with it (last 200 turns), so every client: desktop, web, phone: gets the conversation it picked. This is not the transcript *tailing* ADR 0005 removed: nothing follows the file, and every frame after the seed comes from the SDK stream as before.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;Seeded frames carry &lt;code&gt;termuna_replay&lt;/code&gt; (docs/PROTOCOL.md) and no client starts a turn on one. Without that, an old message read as a message being picked up and the composer sat &amp;quot;working&amp;quot; on a turn that had ended days ago, which is what &amp;quot;it re-sent my message and the agent worked on it&amp;quot; actually was.&lt;/p&gt;
&lt;h3&gt;Changed: an agent conversation is headed by the agent glyph&lt;/h3&gt;
&lt;p&gt;The chat header led with the brand mark, which says &amp;quot;Termuna&amp;quot;, and the window already says that. It now carries the same agent glyph that marks &amp;quot;agents&amp;quot; in the sidebar and every agent session in the picker, so one picture means one thing wherever it appears.&lt;/p&gt;
&lt;h3&gt;Fixed: the effort dial showed the wrong model's levels&lt;/h3&gt;
&lt;p&gt;The running model is reported by its *resolved* id, &lt;code&gt;claude-opus-5[1m]&lt;/code&gt;, which is a different string from the one &lt;code&gt;set_model&lt;/code&gt; takes, &lt;code&gt;opus[1m]&lt;/code&gt;. Matching the running model against the catalog by that second string found nothing, so the dial fell through to the &lt;code&gt;default&lt;/code&gt; entry and offered its levels instead of the running model's: five where Sonnet takes two, say. The catalog carries &lt;code&gt;resolvedModel&lt;/code&gt;; it is now read and matched on, here and in both other clients.&lt;/p&gt;
&lt;h3&gt;Fixed: a deleted connection stays deleted&lt;/h3&gt;
&lt;p&gt;Removing an SSH host took it out of the local vault and left the copy in the cloud alone, so the next sync: the one that runs at startup, pulled it straight back down. Delete, restart, and there it was again.&lt;/p&gt;
&lt;p&gt;The vault now records the deletion as well as performing it. A sync carries those records up before it pulls anything down: each one becomes a &lt;code&gt;DELETE&lt;/code&gt; of that item in the vault it belonged to, and only then is it forgotten. A deletion made offline, or against a shared vault this account may not write to, stays recorded and is filtered out of the merge, so the connection cannot come back while the deletion is still owed. Deleting now also syncs immediately when the cloud is configured, so the other devices hear about it without waiting for a restart.&lt;/p&gt;
&lt;p&gt;Verified end-to-end against a local cloud service: delete online (the cloud item goes with it), and delete with the service down, then restart with it back up: the connection stays gone and the deferred delete lands. Records are capped at 256 so one that can never be flushed cannot grow the file forever.&lt;/p&gt;
&lt;p&gt;The other half of that is a second device that still holds the connection and has not heard: on its next sync it would push the profile straight back. The relay now remembers deletions too (termuna-website, schema v12), refuses a push of a deleted id, and lists the deletions beside the items, so a sync also carries them *down*. A connection deleted on the phone disappears from the desktop, and a device that was offline through the whole thing cannot undo it when it returns. A deletion heard from the cloud is applied without being recorded again: we heard it there, so telling it back would be an echo. Against a server too old to send the list, nothing is dropped and the behaviour is exactly what it was before. Verified with a real GUI as one device and the API as the other, in both directions.&lt;/p&gt;
&lt;h3&gt;Fixed: half the model menu did nothing&lt;/h3&gt;
&lt;p&gt;Picking Opus or Fable had no effect at all: the daemon dropped the request before it ever reached the agent. Its model ids are validated by a checker written for shell words, and the CLI names two of its own models &lt;code&gt;opus[1m]&lt;/code&gt; and &lt;code&gt;claude-fable-5[1m]&lt;/code&gt;: brackets and all. Both were refused as junk.&lt;/p&gt;
&lt;p&gt;The refusal was silent, and the label went on truthfully showing the model that was still running, which is what made it look like the pick had been ignored rather than rejected. It had been rejected.&lt;/p&gt;
&lt;p&gt;Model ids now have their own check, wider by exactly two characters and no more: still not a shell sentence. The launch path that does build a shell line quotes the model, so a bracketed id cannot be read as a glob. This applied to starting a session as well as switching mid-conversation: launching *on* Opus was refused the same way.&lt;/p&gt;
&lt;h3&gt;Fixed: the model and effort you pick actually stick&lt;/h3&gt;
&lt;p&gt;Picking a model changed nothing on screen, and the composer sometimes went to &amp;quot;queue another message…&amp;quot; and stayed there. Two faults behind one symptom, both about a frame the CLI sends that we were reading wrong.&lt;/p&gt;
&lt;p&gt;The model label waited for a &lt;code&gt;system/init&lt;/code&gt; frame naming the new model. That frame comes once, when a session starts, and never again, so the label sat on whatever it said before, or on nothing. The daemon now asks &lt;code&gt;get_settings&lt;/code&gt; right after any &lt;code&gt;set_model&lt;/code&gt; or effort change and lets the answer be what the clients show: a request we sent is not evidence it was honoured.&lt;/p&gt;
&lt;p&gt;What the CLI *does* send back is a user-role frame reading &lt;code&gt;&amp;lt;local-command-stdout&amp;gt;Set model to sonnet (claude-sonnet-5)&amp;lt;/local-command-stdout&amp;gt;&lt;/code&gt; shaped exactly like our own echoed message, down to &lt;code&gt;isReplay: true&lt;/code&gt;. Only the wrapper tells them apart. We were reading it as a message being picked up, marking the agent busy, and then waiting forever for a &lt;code&gt;result&lt;/code&gt; that was never coming, because no turn had started. The chat log already filtered these; the busy flag did not, and now does.&lt;/p&gt;
&lt;h3&gt;Fixed: the chat says which effort it is on&lt;/h3&gt;
&lt;p&gt;The effort dial drew five identical dots with none of them filled, and the model row read &lt;code&gt;default&lt;/code&gt;, until the first message had been sent. Neither was a display bug: nothing had ever asked. The CLI does not volunteer its model until the &lt;code&gt;system/init&lt;/code&gt; frame, which does not arrive until a session starts, and it never volunteers the effort level at all.&lt;/p&gt;
&lt;p&gt;The daemon now sends a &lt;code&gt;get_settings&lt;/code&gt; control request right behind the &lt;code&gt;initialize&lt;/code&gt; it already sent. The reply says what the agent is actually running on, so the menu shows the real model and the real effort level before a word is typed, and the effort is named next to the dots, not only drawn, because five dots and a hover tooltip do not tell you which one you are on.&lt;/p&gt;
&lt;h3&gt;Changed: the agent's own catalog, instead of ours&lt;/h3&gt;
&lt;p&gt;The model picker listed four names Termuna had hard-coded: &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;sonnet&lt;/code&gt;, &lt;code&gt;opus&lt;/code&gt;, &lt;code&gt;haiku&lt;/code&gt;, and the slash menu ten commands from the same kind of table. Both were guesses that could only go stale.&lt;/p&gt;
&lt;p&gt;The daemon has always sent the CLI an &lt;code&gt;initialize&lt;/code&gt; control request at spawn, and the reply carries the CLI's own catalog. Termuna now reads it:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Models&lt;/strong&gt; come with the names and descriptions the CLI gives them, &amp;quot;Opus (1M context) · Opus 5 with 1M context · Best for everyday, complex tasks&amp;quot;, and the list is whatever that install actually offers.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Effort levels are per model.&lt;/strong&gt; &lt;code&gt;max&lt;/code&gt; is a real level and the hard-coded four had hidden it; a model that takes no effort now shows no dial rather than a dial that would be refused.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Slash commands are the CLI's own&lt;/strong&gt;: sixty of them here, including every skill and plugin the user has installed, which no table of ours could have listed. Whether picking one runs it or drops it in the composer follows the CLI's own argument hint: &lt;code&gt;&amp;lt;required&amp;gt;&lt;/code&gt; waits for you, &lt;code&gt;[optional]&lt;/code&gt; and &lt;code&gt;&amp;lt;optional …&amp;gt;&lt;/code&gt; go straight out.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;The static tables stay as the fallback for providers that answer no such request, and for the moment before the first one arrives. Names and descriptions are clipped to their columns, because iced draws unwrapped text at full length rather than shrinking it, and two unclipped columns in a fixed-width menu write over each other.&lt;/p&gt;
&lt;h3&gt;Added: the rest of the agent's control surface&lt;/h3&gt;
&lt;p&gt;The CLI's SDK protocol carries more than &amp;quot;say this&amp;quot; and &amp;quot;allow that&amp;quot;; Termuna now speaks the parts a person actually reaches for. Four additive &lt;code&gt;AgentCommand&lt;/code&gt; variants (&lt;code&gt;docs/PROTOCOL.md&lt;/code&gt;), each validated host-side and each mapped to the CLI's own control request:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Switch model&lt;/strong&gt; mid-conversation (&lt;code&gt;set_model&lt;/code&gt;). The menu lists the daemon's catalog for the provider plus whatever the CLI reports it is running, and the tick follows the CLI's &lt;code&gt;init&lt;/code&gt; frame rather than our last request: a refused switch must not leave the label lying.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Thinking&lt;/strong&gt; on or off (&lt;code&gt;set_max_thinking_tokens&lt;/code&gt;). A switch, not a budget: a person decides *whether* the model should think, and the daemon turns that into the SDK's number.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Effort&lt;/strong&gt;: whichever levels the running model takes (&lt;code&gt;apply_flag_settings&lt;/code&gt;), as a named row of dots in the modes menu.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Rewind the files&lt;/strong&gt; to before any of your messages (&lt;code&gt;rewind_files&lt;/code&gt;). A dry run goes first, so the card names the files it would restore before you commit to it; when the agent has file checkpointing turned off it says so in the agent's own words, and which setting turns it on. This restores *code*, not the conversation: the CLI has no conversation rewind on this channel, and pretending otherwise would be the wrong promise.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;Everything the CLI already reported and Termuna was discarding is now read: &lt;code&gt;system/init&lt;/code&gt; names the model, &lt;code&gt;system/status&lt;/code&gt; corrects the permission mode (so a &lt;code&gt;/model&lt;/code&gt; or a plan-mode exit shows up even though it happened without us), &lt;code&gt;result&lt;/code&gt; carries the model's context window, &lt;code&gt;compact_boundary&lt;/code&gt; empties it, and &lt;code&gt;TodoWrite&lt;/code&gt; calls are the agent's plan.&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;A context meter&lt;/strong&gt; in the header: &lt;code&gt;42% context&lt;/code&gt;, click to compact, instead of a token count with no denominator. It appears after the first answer, because that is when the CLI says how big the window is.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The agent's plan&lt;/strong&gt;, as a count in the header and a floating panel that does not reflow what you were reading.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Queued messages&lt;/strong&gt; are shown when an interrupt reports them still waiting.&lt;/li&gt;&lt;li&gt;The rewind affordance appears only on turns the CLI gave an id for; a message drawn optimistically now inherits that id from the echo instead of dropping it.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: the agent chat gets a composer that can do something&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Permission modes.&lt;/strong&gt; A control beside the send button puts the agent in &lt;strong&gt;manual&lt;/strong&gt; (asks before every edit and command), **edit automatically&lt;strong&gt;, or &lt;/strong&gt;plan** (explores and proposes, changes nothing). &lt;strong&gt;Shift+Tab&lt;/strong&gt; walks the three. The daemon and the wire have carried &lt;code&gt;SetPermissionMode&lt;/code&gt; since M11; until now nothing on the desktop sent it. &lt;code&gt;bypassPermissions&lt;/code&gt; is *not* offered, because the daemon refuses it from any client and a button that silently does nothing is worse than no button.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;A &lt;code&gt;+&lt;/code&gt; menu&lt;/strong&gt;: upload a picture from this machine, or start an &lt;code&gt;@&lt;/code&gt; mention to point the agent at a file.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;A command menu&lt;/strong&gt; on the &lt;code&gt;/&lt;/code&gt; button: everything the composer can do, in one filterable list: attach, mention, the provider's own slash commands, the modes, and the way back to sessions. Commands that need an argument (&lt;code&gt;/model&lt;/code&gt;, &lt;code&gt;/resume&lt;/code&gt;) land in the composer for you to finish; the rest are sent as they stand, because there is nothing left to type. Enter runs the first row the filter leaves standing.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Esc stops the turn&lt;/strong&gt;, and the working row says so: it now carries a live clock (&lt;code&gt;working · 12s&lt;/code&gt;) instead of a bare &amp;quot;working…&amp;quot;.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: the chat, on the v5 tokens&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The composer is one card: the message line, and under it the controls that decide what the message can do. The send square lights when there is something to send and becomes a stop square while the agent works; the placeholder says *queue another message…* rather than pretending Enter does the same thing it did a second ago.&lt;/li&gt;&lt;li&gt;An empty conversation shows the mark, the agent's name, and where to start, instead of a grey sentence apologising for being empty.&lt;/li&gt;&lt;li&gt;What you said sits in a quiet bordered block; what the agent said is simply the page. The filled accent bubble made every question shout louder than every answer. Prose is Inter whoever wrote it, code stays monospace and highlighted, and streaming text now arrives in the same furniture it settles into rather than flashing a box on the way in.&lt;/li&gt;&lt;li&gt;The header is icons and a hairline: the project, the token count, the terminal view where there is one, and the way back.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: the SFTP drawer, redrawn&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The files pane followed the redesign in `termuna_files_pane_redesign .html`: a header with sort, refresh and close; the host and path above the filter; &lt;strong&gt;folders&lt;/strong&gt; and &lt;strong&gt;files&lt;/strong&gt; as named groups; and a footer that says what is here (&lt;code&gt;127 items · 1.2 MiB&lt;/code&gt;) beside &lt;strong&gt;New&lt;/strong&gt; and &lt;strong&gt;Upload&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;Sizes live in a column of their own, right-aligned, so you can read down them. They are written short: &lt;code&gt;68.8 M&lt;/code&gt;, &lt;code&gt;2.6 K&lt;/code&gt;, &lt;code&gt;0 B&lt;/code&gt;, because &lt;code&gt;454.5 KiB&lt;/code&gt; wrapped to a second line and dragged the row with it.&lt;/li&gt;&lt;li&gt;Dotfiles are folded away behind &lt;code&gt;hidden · N entries&lt;/code&gt; and shown dimmed when you ask. They are most of a home directory and almost never what you came for.&lt;/li&gt;&lt;li&gt;Row actions appear on hover, in place of the size, so nothing shifts when they arrive and no row carries a pair of buttons it does not need. File icons are one page shape with a different mark inside, archive, code, patch, plain: tinted by kind: archives amber, configuration and code blue, everything else quiet.&lt;/li&gt;&lt;li&gt;The drawer is a card: 330px with a hairline, an 8px radius and a neutral shadow, floating over the terminal. Nothing is painted behind it but its own margin: a black strip beside the card would hide work the card itself is not covering. Folders carry the accent at rest and brighten under the pointer, the breadcrumb names the folder you are in rather than the path you climbed, the filter has its magnifier inside it, and each group folds its own dotfiles away behind its own count.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: one way out, one settle, and no work for a click that changes nothing&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The ⌂ button is gone from the tab strip. The rail's own &lt;strong&gt;sessions&lt;/strong&gt; is the way back, and two of them was one too many.&lt;/li&gt;&lt;li&gt;Screens cross-fade when you pick them: out to the background and back, 190ms end to end, with the swap under the curtain so neither screen is ever seen half-drawn. (iced has no opacity for arbitrary widgets; the fade is a page-coloured curtain drawn over the content, which costs no layout: the earlier version moved the content and juddered for exactly that reason.)&lt;/li&gt;&lt;li&gt;Entering a session collapses the sidebar instead of cutting to the rail, and leaving one opens it again. It is the same motion as collapsing by hand, labels and all, because it is the same slide.&lt;/li&gt;&lt;li&gt;Clicking the screen you are already on does nothing. It used to re-run that screen's loaders: a status line, a round trip to the daemon and a flicker, in exchange for no change.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: a live session keeps the rail&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Opening a session used to take the whole window and the nav with it: every other screen was behind a detach. A session now keeps the collapsed rail on its left: the mark, the five destinations as icons, the account disc, so sessions, agents, mcp, ssh and settings are one click away.&lt;/li&gt;&lt;li&gt;The rail in a session is always collapsed and has no expand handle: the width belongs to the terminal. Hovering an icon still names it.&lt;/li&gt;&lt;li&gt;Clicking one leaves the session for that screen. The session keeps running; the daemon owns it, which is the whole point of the daemon.&lt;/li&gt;&lt;li&gt;The terminal grid, the tab strip and the divider drags all measure from the window less the rail, so the PTY is sized to what you can actually see.&lt;/li&gt;&lt;li&gt;The &amp;quot;connecting…&amp;quot; moment now wears the chrome of wherever it is going: the rail on the way into a session, the full sidebar on the way back, so the sidebar no longer blinks out and back between two screens that both have one.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: the sidebar collapses to a rail&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;A round handle rides the seam between the sidebar and the work, at the wordmark's height, and takes the sidebar down to a 46px rail: the moon mark, then one icon per destination, then the account disc. Hovering an icon names it, and says its count too: a rail you cannot read is a rail you cannot use.&lt;/li&gt;&lt;li&gt;The sidebar slides between the two widths in 180ms and the handle rides the seam the whole way; its chevron turns through half a circle as it goes, so the button says it reverses itself rather than just swapping for its mirror image.&lt;/li&gt;&lt;li&gt;Labels appear and disappear on the width, not on a fraction of the animation: they are drawn while there is room for them, which is what keeps one from painting over the work beside it mid-slide.&lt;/li&gt;&lt;li&gt;The rail is a 16px glyph centred with a gutter's worth of air, no wider. Expanded, the glyph column lines up with the wordmark above it, as the mockup's &lt;code&gt;padding: 7px var(--gut)&lt;/code&gt; intends.&lt;/li&gt;&lt;li&gt;The choice is remembered (&lt;code&gt;[ui] sidebar_collapsed&lt;/code&gt;). A layout you chose that every launch undoes is not a choice.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: settings, laid out the way the design draws it&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;All four sections are the mockup's &lt;code&gt;.sr&lt;/code&gt; rows now: the name of the setting, one line saying what it does to you, and its control on the right, under hairline rules that group them (profile / type / cursor / preview, scrollback / clipboard / quake mode). Bare &lt;code&gt;−&lt;/code&gt; and &lt;code&gt;+&lt;/code&gt; buttons became one stepper, on/off buttons became switches, and the cursor shape became the same chips the rest of the app uses.&lt;/li&gt;&lt;li&gt;Keybindings gained a search box. Sixty actions is a list you search, not one you scroll.&lt;/li&gt;&lt;li&gt;About is the mockup's card: the wordmark, the build line (&lt;code&gt;0.1.0 · linux-x86_64 · iced 0.14 / wgpu&lt;/code&gt;), what the app is, and the three promises ticked off: offline-first, no telemetry unless you opt in, the relay never sees plaintext.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: the account screen the design asks for&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;&lt;code&gt;docs/design/termuna-ui-v5.html&lt;/code&gt; has drawn this screen all along and the app showed a grey card with two buttons instead. It is now the mockup's: who you are with the account disc, then the three settings that belong to an account: &lt;strong&gt;session sync&lt;/strong&gt; on a real switch, &lt;strong&gt;recovery kit&lt;/strong&gt;, &lt;strong&gt;billing&lt;/strong&gt;, and then every device holding a token for the account.&lt;/li&gt;&lt;li&gt;Devices come from &lt;code&gt;GET /v1/devices&lt;/code&gt;: host, platform, when it was last seen, and a &lt;strong&gt;revoke&lt;/strong&gt; for each one but the machine you are sitting at. Revoking that one would sign you out sideways with the config still claiming you were in; there is a Sign out button for it.&lt;/li&gt;&lt;li&gt;New device tokens are labelled &lt;code&gt;host · os-arch&lt;/code&gt; instead of &lt;code&gt;host (termuna desktop)&lt;/code&gt;, so the platform column has something true in it. Tokens minted before this keep their old label and are parsed as best they can be, never invented.&lt;/li&gt;&lt;li&gt;Session sync is a setting you can turn off from the app now. It writes &lt;code&gt;[cloud] enabled&lt;/code&gt; and tells the running daemon at once, rather than waiting for a restart to become true.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: the sidebar's foot is the mockup's&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Two stacked rows: a daemon line and a boxed &amp;quot;account&amp;quot; button with the whole mailbox under it: are now the one row &lt;code&gt;docs/design/termuna-ui-v5.html&lt;/code&gt; draws: a 26px account disc bearing the initial, the name beside it, and the daemon's own line beneath (&lt;code&gt;daemon · 4h 55m&lt;/code&gt; behind a live dot). It is one button because it is one destination.&lt;/li&gt;&lt;li&gt;The name and the picture come from the account. The app now asks &lt;code&gt;/v1/auth/me&lt;/code&gt; at sign-in and at launch, remembers what it says, and draws the real name and the real face. An account with neither (one created with an email and a password, rather than through Google) falls back to a name derived from the mailbox, &lt;code&gt;soldo.devices@gmail.com&lt;/code&gt; is shown as &lt;code&gt;soldo&lt;/code&gt;, because everything after the first separator is routing, and to the initial on the tinted disc, exactly as before.&lt;/li&gt;&lt;li&gt;The picture is masked to a disc before it reaches the renderer, which has no notion of a rounded image, and cached beside the session store keyed by its URL: a launch costs no network, and a changed face can never be served from a stale cache. Binary cost of the image pipeline and the JPEG decoder: 31.91MB → 32.52MB (+0.6MB); the installer budget is unaffected in practice.&lt;/li&gt;&lt;li&gt;Signed out, the disc is an empty ring and the name reads &amp;quot;sign in&amp;quot;, the slot is there, nobody is in it. The daemon reports either way, and a stale daemon still gets its extra line, because only that case needs an instruction.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: the host card is quieter, its menu is the app's own&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Clicking &lt;strong&gt;⋯&lt;/strong&gt; on an SSH host (or right-clicking the card) now opens the same menu the terminal opens on right-click: icons, hairline separators, the destructive item in warn red. It floats at the pointer instead of unfolding under the card and pushing the grid down. Connect, edit, duplicate, share, &lt;strong&gt;move to &amp;lt;vault&amp;gt;&lt;/strong&gt; and remove are all in it: the separate &amp;quot;move&amp;quot; strip and the inline confirm strip are gone, and removing a connection asks in the same card as every other question.&lt;/li&gt;&lt;li&gt;A host card is one line for the name and one for the address, always. Both were cut to a fixed character count that a narrow card could not hold, so a long &lt;code&gt;user@host&lt;/code&gt; wrapped and that card grew taller than its neighbours. The budget now comes from the card's real width, and neither line may wrap.&lt;/li&gt;&lt;li&gt;Hovering a host no longer paints the card. A tinted slab under a dozen cards is noise; the glyph and the name take the luna accent instead, which is the same &amp;quot;this one&amp;quot; the rest of the app uses.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: a vault you made is a vault you can unmake&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Shared vaults had no way out: the desktop could create one, share it and revoke a member, but never delete it. Every vault header now carries &lt;strong&gt;delete&lt;/strong&gt;, behind a confirmation that states the two things worth knowing before you press it: the connections in it move to Personal, and everyone you shared it with loses access. Deleting a vault is about who can reach a group of hosts; it never throws the hosts away.&lt;/li&gt;&lt;li&gt;The delete reaches the cloud (&lt;code&gt;DELETE /v1/vaults/:id&lt;/code&gt;, owner only), because a vault removed only on this device comes back on the next sync and the teammates never lost anything. With the cloud vault locked the app says exactly that instead of pretending; if the server refuses (a member cannot delete a vault out from under its owner) the vault goes back in the list rather than waiting for the next sync to contradict it. The personal vault is refused outright: it is where the other vaults' connections land.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;members&lt;/strong&gt; now opens in the same card as the rest of the app.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: one modal, one input, everywhere&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The forms that used to unfold inside a screen now open the way the start-agent dialog always did: a card over a dimmed page. **Add host&lt;strong&gt; / &lt;/strong&gt;Edit host&lt;strong&gt;, &lt;/strong&gt;New vault&lt;strong&gt; and &lt;/strong&gt;Add MCP server** are all the same card, so a long list can never push the form out of sight and a half-filled form can never scroll away from you.&lt;/li&gt;&lt;li&gt;Modals arrive and leave with a short, quiet transition: 150ms in, 110ms out, the card settling the last few pixels and the page darkening with it. A dismissed modal keeps its contents until it has finished leaving, so it never empties itself in front of you.&lt;/li&gt;&lt;li&gt;One text input in the whole app: the window's own colour recessed into the surface, a hairline that brightens under the pointer, the live accent only while it holds the caret. Search boxes, the command palette, the chat composer, the keybinding editor, the scrollback search and every field in every form now share it: as do the dropdowns, which are the same well with a handle.&lt;/li&gt;&lt;li&gt;Settings sections are the filter chips the agents and MCP screens use, not underlined tabs. One list of choices, drawn one way.&lt;/li&gt;&lt;li&gt;The cloud-vault line under &lt;strong&gt;Search hosts&lt;/strong&gt; (&amp;quot;synced 7 connection(s)…&amp;quot;) had no space above it and read as the search box's error message. It now sits in its own row behind a live dot.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: sessions no longer come back on their own (ADR 0006)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;A daemon restart takes every shell it owned with it. On its next start the daemon used to respawn them for any session live within the last 48 hours, restore the scrollback, re-bridge and report the session as live: up to 32 of them, without anyone asking. On the phone that was indistinguishable from a session that had never stopped, except that the build you left running in it was gone.&lt;/li&gt;&lt;li&gt;Either the process ran the whole time or it did not. Sessions now load dormant after a restart, with their history readable, and come back only when a human asks for them: from the picker, the phone or the web. &lt;code&gt;spawn_boot_resurrect&lt;/code&gt;, the 48-hour window, the 32-session cap and the &lt;code&gt;[cloud] keep_alive&lt;/code&gt; option are gone. (The config key is still parsed and ignored, so an existing &lt;code&gt;[cloud]&lt;/code&gt; section does not become invalid and take sync down with it.)&lt;/li&gt;&lt;li&gt;What you asked for, you can still see you asked for: a rebuilt session writes &lt;code&gt;── session rebuilt · new shell in &amp;lt;dir&amp;gt; ──&lt;/code&gt; into each pane before the new prompt, so restored scrollback above a fresh shell cannot pass for continuity.&lt;/li&gt;&lt;li&gt;The real fix for surviving an upgrade is a daemon that does not die (restart-in-place with the PTYs inherited); it is on the roadmap, and this change stops papering over its absence.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: &amp;quot;needs you&amp;quot; stayed lit after the ask was answered&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The amber badge is the relay's &lt;code&gt;attention_at&lt;/code&gt;, and the only thing that ever cleared it was a viewer's &lt;code&gt;Input&lt;/code&gt; frame. A managed agent session has no PTY, so nobody types into it: it is answered with &lt;code&gt;AgentCommand&lt;/code&gt;, and an ask answered on the host's own desktop never crosses the relay at all. The badge therefore stayed lit on the phone indefinitely, for an agent that had long since carried on.&lt;/li&gt;&lt;li&gt;The protocol gains &lt;code&gt;AttentionOver&lt;/code&gt; (additive, unit variant): the host says when a session stops wanting its user, whatever stood it down. The relay clears the flag on it, and also on a viewer's &lt;code&gt;AgentCommand&lt;/code&gt;, which is the same human being present.&lt;/li&gt;&lt;li&gt;A restart is the other half of it: a fresh daemon has no attention to retract, so it would have left the badge up forever. The bridge now states this the moment it attaches, which asks still stand, and whether the session wants its user at all: instead of leaving the relay holding whatever it was told last time.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: a permission ask answered on one device stayed open on the others&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Allow a tool call on the desktop and the phone kept showing Allow/Decline for it, indefinitely. Nothing in the agent CLI's stream says an ask is over: the answer is a &lt;code&gt;control_response&lt;/code&gt; Termuna sends *to* it, so a client that did not send that answer never learned the agent had moved on.&lt;/li&gt;&lt;li&gt;The daemon now states it. When the pending set changes: an ask arrives, one is answered, a turn ends and abandons what it was waiting on: it broadcasts a sequenced frame of its own (&lt;code&gt;{&amp;quot;type&amp;quot;:&amp;quot;termuna&amp;quot;,&amp;quot;subtype&amp;quot;:&amp;quot;permission_state&amp;quot;}&lt;/code&gt;) carrying the whole waiting set, and naming the one that just changed along with what the human chose. Carrying the set rather than only the transition is what makes it work for a client replaying history: the asks are in the log, and this says which of them still stand.&lt;/li&gt;&lt;li&gt;Desktop, phone and web all render a settled card without buttons, &amp;quot;allowed&amp;quot;, &amp;quot;declined&amp;quot;, or &amp;quot;answered elsewhere&amp;quot; for one that was resolved while they were away.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: after a daemon restart, an agent conversation stopped arriving in the cloud&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The relay stores frames by sequence number. The daemon's counter is recovered from the persisted frame log, but that log was only ever written by the PTY output pump, and a managed agent session has no PTY. Its counter therefore restarted at 1 on every daemon restart, so the relay overwrote its own stored history row by row and every frame the daemon sent was dropped as one it had already seen, until the count climbed back past where it left off.&lt;/li&gt;&lt;li&gt;Three changes, because one alone leaves a hole: the agent pump persists the session and its log on the same throttle the PTY pump uses; the sequence counter is also stored in the session snapshot (one integer, and it survives a log that was capped or never written); and on attaching to the relay the bridge winds its counter forward to the head the relay reports, so a daemon that recovered less than the relay kept never talks into a void.&lt;/li&gt;&lt;li&gt;The bridge also announces the agent's pending asks as soon as it attaches, which is what retires a card left over from before the restart.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: an idle session could go dormant in the cloud and never come back&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;A session nobody is typing in sends nothing for hours, and an idle TCP flow is what a home router or a carrier NAT quietly drops. The relay's end then closed: it logged the host detaching and set &lt;code&gt;live = 0&lt;/code&gt;, while the daemon's socket sat in ESTABLISHED believing it was still mirroring. It never reconnected, so every session read as dormant on the phone with the desktop running the whole time, and daemon queries (which route through a live session) timed out as &amp;quot;the desktop is taking a long time to answer&amp;quot;.&lt;/li&gt;&lt;li&gt;Observed on 2026-07-30: both bridges died at 21:52 with nothing in the daemon log, and were still dead 8 hours later against a daemon with 18h uptime.&lt;/li&gt;&lt;li&gt;&lt;code&gt;Payload::Heartbeat&lt;/code&gt; has been in the protocol for exactly this and nothing was sending it. The host now sends one every 20s per bridge, which keeps the flow warm: an idle flow being what gets dropped in the first place. It is unsequenced, so the relay broadcasts it and stores nothing.&lt;/li&gt;&lt;li&gt;The bridge socket also sets &lt;code&gt;TCP_USER_TIMEOUT&lt;/code&gt; (60s) and TCP keepalive. Keepalive alone would not have helped, and measuring showed why: the keepalive timer only runs on an *idle* socket, and the heartbeat means the socket is never idle: with the link blackholed the kernel sat in retransmit for over seven minutes without firing a probe. &lt;code&gt;TCP_USER_TIMEOUT&lt;/code&gt; is the one that bounds unacknowledged data, which is the case that actually applies.&lt;/li&gt;&lt;li&gt;Measured against production by blackholing the relay's address (a DROP rule, so packets vanish with no RST: the same shape as a NAT forgetting the flow): the link died at 06:47:38 and the daemon reported it at 06:48:55, &lt;strong&gt;77 seconds&lt;/strong&gt;. Traffic restored at 06:55:07; all four bridges were reattached by 06:55:52, &lt;strong&gt;45 seconds&lt;/strong&gt; later. Before this change the same test ran seven minutes with the daemon noticing nothing.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Removed&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;&lt;code&gt;AgentEvent&lt;/code&gt; and the transcript-tailing machinery behind it. Agent conversations no longer come from parsing the provider's own log file; see Added. Providers without a structured mode (Codex, Gemini, Cursor, OpenCode) still launch into a shell and are terminals with a program running in them (&lt;code&gt;AgentInfo.structured&lt;/code&gt; says which you get) but they no longer produce a chat lane until each grows an adapter.&lt;/li&gt;&lt;li&gt;Protocol v1. &lt;code&gt;PROTOCOL_VERSION&lt;/code&gt; is 2 and v1 peers are refused. The frames are CBOR-tagged by name, so a v1 peer would just see unknown variants and carry on showing an empty agent session: the version check turns that silent confusion into a clear refusal. Relays and clients must be rebuilt; existing agent sessions do not migrate.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: the wordmark is the brand's drawing, not an approximation&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;&lt;code&gt;termuna&lt;/code&gt; is drawn from outlines converted once from &lt;code&gt;termuna-website/assets/brand/logo-wordmark.svg&lt;/code&gt;, so the letterforms are the brand's own. No typeface ships for one word, and the logo cannot come out in the wrong face if a font fails to load.&lt;/li&gt;&lt;li&gt;The cursor keeps the SVG's exact geometry: 40x76 block, corner radius 6, bite r21 at the upper-right shoulder, all relative to the baseline, but not its x position. The drawing puts it four ems past the word because it was laid out for a wider Martian Mono than the one that ships; the mark means &amp;quot;the next character of a running session&amp;quot;, so it follows the last letter by a hair.&lt;/li&gt;&lt;li&gt;Inter is subset to the ranges the chrome can actually show (856 KB → 283 KB): Latin, Latin-1, Latin Extended-A for Croatian diacritics, punctuation, arrows and the geometric glyphs the UI draws with.&lt;/li&gt;&lt;li&gt;CI reports the binary size instead of asserting it. The assertion was a stand-in for the installer budget; the installer is still ~5MB and still asserted in &lt;code&gt;windows-exe.yml&lt;/code&gt;, so the proxy is what had drifted, not the promise. A real size pass is recorded in docs/ROADMAP.md.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: the desktop is being rebuilt on the v5 design&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;&lt;code&gt;apps/termuna/src/ui.rs&lt;/code&gt; makes &lt;code&gt;docs/design/termuna-ui-v5.html&lt;/code&gt; executable: the ink ladder, accents, metrics and type scale as tokens, one per CSS custom property. The app chrome used to take its colours from whatever terminal scheme was loaded, so it could not match a design at all, and looked nothing like the phone. The scheme now does the one job it should: painting the grid.&lt;/li&gt;&lt;li&gt;Inter ships beside JetBrains Mono. Prose in sans, anything a machine produced in mono: the same split as the phone.&lt;/li&gt;&lt;li&gt;Shell and sessions screen rebuilt: a sidebar with counts and a left accent rule on the active item; a row that reads running → name → what it is → when it was touched, with a rule down the live ones' left edge instead of a box, and actions held back until the pointer arrives.&lt;/li&gt;&lt;li&gt;The desktop chat's parser is written against the SDK stream instead of the transcript file it used to read. That file is gone, and its quirks were still in the code doing the wrong thing on fields the stream spells differently: compaction numbers were read as &lt;code&gt;compactMetadata&lt;/code&gt; where the stream sends &lt;code&gt;compact_metadata&lt;/code&gt;, so every one was silently dropped; subagent output was filtered by &lt;code&gt;isSidechain&lt;/code&gt;, which the stream never sets, so it leaked into the lane. Both fixed, a failed turn and a rule-denied tool now say so instead of vanishing, and the compaction divider carries its numbers. Unit-tested against real frame shapes.&lt;/li&gt;&lt;li&gt;The agent chat can answer. A &lt;code&gt;can_use_tool&lt;/code&gt; ask renders as a card with the tool, its full arguments and Allow/Decline; text streams into a live bubble the finished turn replaces; send becomes stop while the agent works. Verified live: the desktop approved a Write and the file appeared.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: desktop UI details from first use&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Sending a message looked like it had been swallowed: the chat waited for the CLI to echo it back (&lt;code&gt;--replay-user-messages&lt;/code&gt;), which only happens once the agent picks the message up: seconds later. It appears the moment you press Enter now, and the echo is dropped when it arrives so nothing shows twice.&lt;/li&gt;&lt;li&gt;The chat header no longer offers &amp;quot;terminal&amp;quot; for a managed agent. That session has no PTY, so the button opened an empty grid; a terminal-launched agent still has one and keeps the button.&lt;/li&gt;&lt;/ul&gt;
&lt;ul&gt;&lt;li&gt;A session row's actions appeared only sometimes. The clickable body was a button nested inside the hover area, and it swallowed the enter/exit events, so whether a row lit up depended on where the pointer crossed its edge: the first row, entered from the heading above, missed most often. One widget owns both the pointer and the click now, and the cursor turns into a pointer over the row, so what is clickable says so rather than leaving a tint to imply it.&lt;/li&gt;&lt;li&gt;Segmented controls (agents, mcp) had their buttons adjoining, so their borders doubled up and the set read as one crammed block. They are spaced, like the phone's.&lt;/li&gt;&lt;li&gt;The SSH host filter sits on the actions row beside &amp;quot;New vault&amp;quot; and &amp;quot;Add host&amp;quot;, the way the sessions filter does, instead of on a line of its own below them.&lt;/li&gt;&lt;li&gt;SSH host cards had less vertical room than every other row in the app, and none between grid rows.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: the start-agent dialog&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Rebuilt to the maintainer's redesign: the provider named in a chip beside the title, the working directory as a field you can &lt;strong&gt;type&lt;/strong&gt; rather than only browse to, a Browse button beside it, and the directories this machine has already run agents in listed underneath with their conversation counts: busiest first, one tap to pick.&lt;/li&gt;&lt;li&gt;Long paths are shortened from the left (the last components identify a project; the prefix is the same for all of them), so a row stays a row.&lt;/li&gt;&lt;li&gt;&lt;code&gt;esc&lt;/code&gt; closes it, which the footer now says and previously did not do.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Sidebar icons, drawn rather than shipped as a font: the mockup gives their paths on a 24×24 grid, so they stay the same source of truth as the colours.&lt;/li&gt;&lt;li&gt;A managed agent session is named after the directory it works in. It has no shell to set a title and no pane cwd to poll, so it used to stay &amp;quot;shell&amp;quot; forever and every agent session looked like every other; the chat header then paired that with the provider and read &amp;quot;claude claude&amp;quot;.&lt;/li&gt;&lt;li&gt;Opening any agent session goes straight to the chat. It has no PTY, so the chat-or-terminal question offered a choice one side of which opened an empty grid. &lt;code&gt;SessionInfo&lt;/code&gt; carries &lt;code&gt;sdk&lt;/code&gt; for that, which every client needs anyway.&lt;/li&gt;&lt;li&gt;Agents screen on the same anatomy: provider segments (a CLI that is not installed stays visible and plainly unavailable rather than vanishing), then resumable conversations named by the project they ran in, with the prompt beneath and the time on the right.&lt;/li&gt;&lt;li&gt;MCP and SSH screens on the same anatomy: a segmented provider control and a configured list with the endpoint and its kind; hosts as cards under collapsible vault headings that say how many and whether the vault is private or shared, with the ~/.ssh/config import as a bordered note rather than a button pretending to be a row.&lt;/li&gt;&lt;li&gt;Settings and account follow: underlined tabs instead of filled pills (a pill reads as a button you press, not a section you are in), group labels as small headings, the design's button language throughout, and an account identity block with the avatar initial and the org in the colour that already means &amp;quot;shared with others&amp;quot;.&lt;/li&gt;&lt;li&gt;The terminal's own chrome joins them: the tab strip, the search bar and the files drawer take the fixed tokens, and so do the picker's inline editors (add host, new vault, members). The colour scheme now paints the grid and nothing else, which is what makes one app out of a themed terminal and a designed shell.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Agent sessions are conversations now, not terminals with a CLI typed into them (ADR 0005, protocol v2). The daemon runs Claude Code as a managed process over its SDK protocol and mirrors the conversation as &lt;code&gt;AgentFrame&lt;/code&gt;s: streaming deltas, typed tool calls and results, compaction boundaries, cost. Clients answer with &lt;code&gt;AgentCommand&lt;/code&gt;, message, &lt;strong&gt;answer a permission ask&lt;/strong&gt;, interrupt, change permission mode, so a phone or browser can approve the tool the agent is blocked on instead of typing blind into a TUI it cannot read. &lt;code&gt;bypassPermissions&lt;/code&gt; is refused from remote clients on purpose. A managed session has no PTY and resumes by the CLI's own session id. Verified end to end through the daemon (&lt;code&gt;cargo test -p termuna-mux --test live_agent_session -- --ignored&lt;/code&gt;) and live from a phone through the production relay: a Write outside the working directory stopped the agent, the card appeared on the phone, Allow released it, and the file landed on the desktop.&lt;/li&gt;&lt;li&gt;Token deltas are sent live but never stored: &lt;code&gt;stream_event&lt;/code&gt; frames go out unsequenced, so they stay out of the replay log and off the relay's disk. A reader who joins later gets the conversation rather than a recording of its keystrokes.&lt;/li&gt;&lt;li&gt;&lt;code&gt;termuna-agent&lt;/code&gt;: a crate that drives an agent CLI over its own SDK protocol instead of its terminal. Claude Code in &lt;code&gt;stream-json&lt;/code&gt; mode speaks NDJSON both ways, so the conversation arrives as typed frames, streaming deltas, tool calls with their inputs and results, compaction boundaries, a result with cost, and, crucially, a &lt;code&gt;can_use_tool&lt;/code&gt; permission ask that Termuna can route to whoever is watching and answer. This is the foundation for replacing the transcript-tailing chat lane. Verified against a real Claude Code process: ask → answer → tool runs → typed result (&lt;code&gt;cargo test -p termuna-agent -- --ignored&lt;/code&gt;).&lt;/li&gt;&lt;/ul&gt;
&lt;ul&gt;&lt;li&gt;Cloud keep-alive: with sync enabled, sessions that were live when the daemon last stopped come back automatically on the next daemon start (or reboot) and re-bridge to the cloud, so you can continue them from the web or phone without the desktop app having to reopen each one first. Previously the daemon loaded every saved session dormant and only respawned one when a viewer opened it, so with the GUI closed nothing stayed live on the relay. Bounded and safe: only sessions active within the last 48h are respawned, capped at 32, staggered, and run off the startup path so cold start is unaffected; older sessions stay dormant (still viewable read-only from the dashboard). Opt-in with cloud sync (ADR 0002): nothing respawns unless &lt;code&gt;[cloud] enabled&lt;/code&gt;. On by default when sync is on; set &lt;code&gt;[cloud] keep_alive = false&lt;/code&gt; to opt out and keep the old behavior (sessions load dormant until reopened).&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Scrolling and selecting work in apps that turn mouse reporting on without taking the alternate screen: a CLI agent's TUI being the case that exposed it. Termuna forwarded every wheel notch and every drag to such an app, which ignores them, so the scrollback could not be scrolled and no text could be selected or copied at all. The wheel now scrolls the scrollback in the normal screen, and a drag becomes a selection there (the button is released for the application first, so a plain click still reaches it). Full-screen apps on the alternate screen (vim, htop, lazygit) keep the wheel and the drag, since they own the viewport and there is no scrollback behind them. Shift still bypasses reporting everywhere, as before.&lt;/li&gt;&lt;li&gt;Right-click opens the context menu, and middle-click pastes the primary selection, in the normal screen too. Both were forwarded to any app with mouse reporting on, which is how a selection could exist with no reachable way to copy it.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;TSP grows a daemon-query channel (&lt;code&gt;Query&lt;/code&gt;/&lt;code&gt;QueryReply&lt;/code&gt;, additive): viewers attached to a live session can ask the daemon for the installed agent CLIs, recent resumable agent conversations, and to launch an agent in a fresh session: everything E2E-sealed with the asking session's key, correlated by envelope id, never persisted by the relay. &lt;code&gt;SessionCreated&lt;/code&gt; replies carry the new session's share link inside the sealed payload so the asking viewer (the mobile app's agents screen) can open it immediately. The channel also covers plain session management: create, rename, kill, and resurrect-with-share-link, and opening vault connections: &lt;code&gt;CreateSshSession&lt;/code&gt; carries the resolved SSH target (host, auth, jumps, forwards) inside the sealed query, so a phone can open a saved connection Termius-style without any secret touching the relay. Spec: docs/PROTOCOL.md.&lt;/li&gt;&lt;/ul&gt;</description>
    </item>
    <item>
      <title>0.1.0</title>
      <link>https://termuna.com/changelog/#0.1.0</link>
      <guid isPermaLink="false">tag:termuna.com,2026:changelog/0.1.0</guid>
      <pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate>
      <description>&lt;p&gt;First public early-access release: the Windows installer and Linux build on &lt;a href=&quot;https://github.com/termuna/termuna/releases&quot; rel=&quot;noopener&quot;&gt;github.com/termuna/termuna&lt;/a&gt;. Everything below shipped in it.&lt;/p&gt;
&lt;h3&gt;Fixed: dock launch spinner no longer hangs for ~a minute (Linux)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The GNOME/Ubuntu dock kept showing the &amp;quot;launching&amp;quot; cursor for up to a minute after Termuna's window was already visible. iced/winit never completes X11 startup-notification (no &lt;code&gt;_NET_STARTUP_ID&lt;/code&gt; on the window, no completion message), so with &lt;code&gt;StartupNotify=true&lt;/code&gt; the launcher waited out GNOME's whole startup-sequence timeout. Set &lt;code&gt;StartupNotify=false&lt;/code&gt; in the desktop entry; the window appears instantly so no launch feedback is needed, and &lt;code&gt;StartupWMClass&lt;/code&gt; still groups the window under the icon.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: shared sessions no longer tear the terminal&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;When a session was open in more than one viewer (e.g. the desktop app and the web viewer) at different window sizes, the PTY size flapped between them and the TUI (a shell full-screen app, an agent) drew for one size while a mirror rendered another: garbled, misaligned output. The host now reconciles viewer sizes tmux-style (smallest-wins) and publishes the authoritative grid size in the layout snapshot; every mirror renders exactly that grid and letterboxes any extra window space, so the two never disagree. A web viewer's size stops constraining the terminal shortly after its tab closes (size heartbeat + TTL), so the desktop reclaims full width. Wire-compatible (the size rides the existing encrypted layout snapshot; the relay is unchanged).&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: all live sessions mirror to the cloud, not just the newest&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Cloud bridge: a session's mirror is now self-healing. Previously the &amp;quot;bridge already started&amp;quot; latch was never released and the bridge task returned on a transient broadcast-channel close, so a mirror that stopped for any reason was never restarted: over time only the most recently started session stayed live in the cloud even though several were live locally. The bridge now reconnects while the session is live and releases the latch when it truly stops.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: desktop chat text is natively selectable (plain, no button)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Desktop agent chat: every message is now a read-only editor, so you can drag to highlight any part and Ctrl+C to copy it: no &amp;quot;select&amp;quot; button, no per-message copy button. Markdown is stripped to clean plain text (no raw ** # ` or link syntax); iced can't both render markdown and allow selection, so on desktop we chose selection. (The web viewer keeps rendered markdown AND native selection.)&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: select part of a chat message; compaction shows for any provider&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Desktop chat: each message has a &amp;quot;select&amp;quot; toggle that turns it into a read-only editor so you can highlight and copy just part of it (Ctrl+C): markdown stays for reading, selection on demand. Web chat text is natively selectable (the per-message copy button is gone).&lt;/li&gt;&lt;li&gt;Compaction now shows a &amp;quot;⟳ conversation compacted&amp;quot; divider for any provider: detected from the continuation-summary preamble as well as Claude's /compact command.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: slash menu typing; web chat text is selectable&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Typing after &amp;quot;/&amp;quot; in the chat composer now works: the command menu floats over the composer (as a constant overlay layer) instead of reflowing the layout, which was dropping input focus after the first character. It filters live as you type.&lt;/li&gt;&lt;li&gt;Web chat: message text is selectable: select any part and copy it natively; the per-message copy button is gone.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: /compact (and slash commands) show in the chat&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Running &lt;code&gt;/compact&lt;/code&gt; now draws a &amp;quot;⟳ conversation compacted&amp;quot; divider in the chat lane (and &lt;code&gt;/clear&lt;/code&gt; a &amp;quot;conversation cleared&amp;quot; one); other local slash commands show a muted &amp;quot;▸ /command&amp;quot; marker. Detected from the agent's transcript (system/local_command records): earlier the chat ignored these entirely. Desktop and web.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: working directory uses a native folder picker&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;In the &amp;quot;start agent session&amp;quot; chooser, the working-directory field is now a click target that opens the native folder picker, prefilled at the current path.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: nicer &amp;quot;start agent session&amp;quot; chooser + directory ask&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The open chooser got a visual pass (glyph + title + description cards, subtle shadow, rounded) and now asks for a working directory when you start a NEW agent session: resuming a past conversation keeps its own folder, so it skips the field.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: cleaner picker: agents screen, scroll, copy feedback&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The agents screen dropped the model chips and directory input (they used the CLI defaults anyway); &amp;quot;continue a conversation&amp;quot; entries are now two-line cards: prompt on top, a muted &amp;quot;provider · folder · when&amp;quot; line below, so the list is far easier to scan.&lt;/li&gt;&lt;li&gt;The picker's scrollbar no longer clips the row controls (rename/kill, copy link): the scroll area reserves a right gutter on every screen.&lt;/li&gt;&lt;li&gt;The chat copy button flashes &amp;quot;copied&amp;quot; in place after a click (desktop and web).&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: MCP server management (picker &amp;quot;mcp servers&amp;quot;)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;A new picker section lists, adds, and removes the MCP servers your agents use, per provider (Claude Code, Codex, Gemini): reading and writing each provider's own config (&lt;code&gt;~/.claude.json&lt;/code&gt;, &lt;code&gt;~/.codex/config.toml&lt;/code&gt;, &lt;code&gt;~/.gemini/settings.json&lt;/code&gt;) and preserving everything else in those files. Add a stdio server (command + args) or a remote one (URL). Daemon-side (&lt;code&gt;McpList&lt;/code&gt;/&lt;code&gt;McpAdd&lt;/code&gt;/&lt;code&gt;McpRemove&lt;/code&gt;) so the web can reuse it later.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: command palette (Ctrl/Cmd+K)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;A fuzzy command palette opens from anywhere with Ctrl/Cmd+K: jump to a session by name, start a local or agent session, open connections or settings, and (in a session) switch chat/terminal, new tab, or back to the picker. Arrow keys + Enter, Esc closes.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: agent chat composer: slash menu + copy&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Typing &amp;quot;/&amp;quot; in the chat composer opens a menu of the provider's common commands (/compact, /clear, /model, /cost, /status…): pick one to drop it in. Every assistant (and user) message has a quiet copy button. Desktop and web.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: agent chat shows tool activity, thinking, and token use&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The chat lane is no longer just text: each tool call renders a card with its key argument (bash command, file path, grep pattern) and a monospace detail block; &lt;strong&gt;Edit/Write show a +/- diff&lt;/strong&gt;; tool results appear as muted &amp;quot;⤷ result&amp;quot; blocks (clipped). Reasoning shows as a muted &amp;quot;thinking&amp;quot; line. A running &lt;strong&gt;token count&lt;/strong&gt; sits in the chat header. Desktop and web.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: agent chat kept up with compaction; less noise&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The chat lane no longer freezes when the agent conversation is compacted or &amp;quot;resumed from summary&amp;quot;: Claude Code rolls to a new session file at that point, and the daemon now FOLLOWS the newest transcript in the session's directory instead of staying pinned to the launch file. A &lt;code&gt;/compact&lt;/code&gt; now shows a &amp;quot;⟳ conversation compacted&amp;quot; marker followed by the summary.&lt;/li&gt;&lt;li&gt;The chat drops CLI machinery that isn't a human turn: background-agent task-notifications and local slash-command echoes no longer appear as message bubbles.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: rich replies + images in the agent chat&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Assistant replies in the agent chat (desktop and web) now render as &lt;strong&gt;markdown&lt;/strong&gt; (headings, bold, lists, links, and code blocks) instead of raw asterisks and backticks.&lt;/li&gt;&lt;li&gt;You can hand the agent an image from the chat: **Ctrl/⌘+V pastes an image straight from the clipboard** (desktop and web), or use the &amp;quot;+&amp;quot; button. On the desktop the pasted image is saved locally and its path drops into the composer (visible and editable) so you see it worked; on the web/mobile viewer it travels E2E-encrypted to the host (new &lt;code&gt;AgentAsset&lt;/code&gt; frame, ≤12 MiB, relay sees only ciphertext) which saves it and references it to the agent.&lt;/li&gt;&lt;li&gt;The chat attach control is a quiet monochrome &amp;quot;+&amp;quot; (was a color emoji that clashed with the terminal look).&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: desktop agent chat UI + open-as-chat/terminal choice&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Opening an agent session (from a session row, &amp;quot;start agent session&amp;quot;, or &amp;quot;continue a conversation&amp;quot;) now asks how you want to work with it: &lt;strong&gt;Chat&lt;/strong&gt;: a readable conversation view with message bubbles, built from the agent's own transcript, with a composer that types into the live agent, or &lt;strong&gt;Terminal&lt;/strong&gt;, the agent's full TUI. Both drive the same session; a ✳/⌗ button flips between them at any time.&lt;/li&gt;&lt;li&gt;Agent CLI detection now also looks in the usual user install spots (&lt;code&gt;~/.local/bin&lt;/code&gt;, nvm/fnm/volta/bun node dirs, Homebrew) so Codex, Gemini, etc. are recognized even when the daemon's PATH is narrower than your shell's: no more false &amp;quot;not installed&amp;quot;.&lt;/li&gt;&lt;li&gt;The provider chips filter the &amp;quot;continue a conversation&amp;quot; list; Codex conversation titles skip the injected environment/AGENTS.md preamble and show the real first prompt.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: structured agent conversation mirrored to viewers&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;For agent sessions the daemon tails the agent CLI's own transcript (Claude Code JSONL under &lt;code&gt;~/.claude/projects&lt;/code&gt;, Codex rollouts) and mirrors complete lines as sequenced, E2E-encrypted &lt;code&gt;AgentEvent&lt;/code&gt; frames. The web viewer turns them into a readable chat lane (✳ button) beside the terminal: the phone-friendly way to follow an agent. Backfills recent history on attach; the relay sees only ciphertext.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: &amp;quot;needs input&amp;quot; attention on sessions&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The daemon watches every pane's output for a terminal bell (outside escape sequences, after 10s of input silence) or an explicit OSC 9/777 notification: the signals agents and long builds emit when they want you. The session flips to &amp;quot;● needs input&amp;quot; in the picker (amber, refreshed automatically) and an additive &lt;code&gt;Attention&lt;/code&gt; frame reaches viewers and the cloud relay, which can fan it out as a push notification to your phone. Typing anything stands it down.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: AI agent sessions (picker &amp;quot;agents&amp;quot; screen)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;New &amp;quot;agents&amp;quot; section in the picker: start a Claude Code, Codex, Gemini, Cursor, or OpenCode session with a model picker and working directory, or continue a past conversation: the daemon discovers resumable sessions in the providers' own transcript stores (&lt;code&gt;~/.claude/projects&lt;/code&gt;, &lt;code&gt;~/.codex/sessions&lt;/code&gt;) and relaunches them with the right resume flags. Uninstalled CLIs show dimmed with an install hint.&lt;/li&gt;&lt;li&gt;An agent session is an ordinary Termuna session whose first pane runs the agent's TUI: it survives GUI kills, mirrors E2E to the web viewer and phones, and resurrects after a daemon restart straight back into its conversation (&lt;code&gt;--resume&lt;/code&gt;/&lt;code&gt;--continue&lt;/code&gt;/ &lt;code&gt;codex resume --last&lt;/code&gt;).&lt;/li&gt;&lt;li&gt;All agent knowledge lives in the daemon (&lt;code&gt;ListAgents&lt;/code&gt;, &lt;code&gt;ListAgentSessions&lt;/code&gt;, &lt;code&gt;CreateSession{agent}&lt;/code&gt; over local IPC), so the web dashboard and future mobile apps can offer the same screen.&lt;/li&gt;&lt;li&gt;Sessions hosting an agent carry a &lt;code&gt;✳ provider&lt;/code&gt; badge in the picker.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: daemon health indicator&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The picker sidebar shows the mux daemon's state: teal dot with version and uptime when healthy, a warning when the daemon runs an older binary than the app (restart to update), red &amp;quot;daemon offline&amp;quot; when unreachable. Backed by a new local-IPC &lt;code&gt;Status&lt;/code&gt; request (pid, version, uptime, session counts), polled every 5s while the picker is visible.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: about tab in settings&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Settings gained an &amp;quot;about&amp;quot; tab: wordmark, version + platform, what Termuna is, links to termuna.com and the dashboard.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: per-tab colors + tab context menu&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Right-click a tab for its own menu: rename, close, close others / to-the-right, and per-tab colors: tab background, label color, and a terminal-background tint for the tab's panes (swatch presets, × clears back to the theme). Colors live in the session tree, mirror to viewers, and survive daemon restarts (&lt;code&gt;SetTabColor&lt;/code&gt;, additive).&lt;/li&gt;&lt;li&gt;The active tab now carries a thin accent underline: visible at a glance where the old 1px outline wasn't.&lt;/li&gt;&lt;li&gt;Dragging a tab shows a floating ghost chip under the cursor while the strip reorders underneath; the origin slot dims.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: tab bar, the full treatment&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Ctrl+1..9 jumps to the n-th tab; tabs show a discreet ordinal.&lt;/li&gt;&lt;li&gt;Middle-click closes a tab; double-click the empty strip opens a new one; the wheel over the strip cycles tabs; double-click a tab still renames it.&lt;/li&gt;&lt;li&gt;Drag a tab to reorder it (live, mirrored to every viewer).&lt;/li&gt;&lt;li&gt;Closing a tab with running child processes asks first: the daemon checks the shells' children and an inline strip offers &amp;quot;close anyway / cancel&amp;quot;. Idle shells close without ceremony.&lt;/li&gt;&lt;li&gt;Right-click the + button: &amp;quot;new session on host…&amp;quot; lists your saved SSH connections.&lt;/li&gt;&lt;li&gt;Context menu: &amp;quot;close other tabs&amp;quot; and &amp;quot;close tabs to the right&amp;quot;.&lt;/li&gt;&lt;li&gt;Ctrl+Tab toggles to the last-used tab (MRU); ctrl+shift+space opens a fuzzy tab switcher (type part of a title, Enter jumps).&lt;/li&gt;&lt;li&gt;Tabs carry a split badge (⊞n) when they hold panes, show the full title in a tooltip once truncated, and below ~30px/tab the strip switches to &amp;quot;active tab + dropdown&amp;quot; instead of unreadable slivers.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: tab strip is responsive; every tab closable on hover&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Ten tabs pushed the files/home buttons off-screen: tab widths were fixed. Tabs now share the space left of the right-side controls and shrink (down to slivers) as more open: +, files, and home never move. The strip recomputes from the live window size, so resizing or maximizing reflows it immediately.&lt;/li&gt;&lt;li&gt;Every tab shows its × when hovered (the active one always); the slot is reserved so nothing shifts on hover.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: bold text switched typefaces with variable fonts&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Bold from a variable-font family (Ubuntu Mono on modern Ubuntu is one file with a weight axis) fell back per-glyph to a different family, bold ls entries rendered in DejaVu. Families with a static Bold face keep using it; variable-only families now get SYNTHETIC bold (the classic double-strike, same as VTE/xterm): same typeface, same metrics, visibly thicker strokes.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: column alignment with non-bundled fonts&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;With a system font whose advance differs from JetBrains Mono's 0.6em (Ubuntu Mono is 0.5em), plain and styled runs drifted apart: ls output looked like mixed fonts with uneven gaps. The cell grid now measures the active family's real advance from its font file (fc-match + ttf-parser) instead of assuming 0.6.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: daily-driver interactions&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Double-click selects the word, triple-click the line (word charset matches Terminator's select-by-word set); the selection lands in PRIMARY as usual.&lt;/li&gt;&lt;li&gt;Ctrl+mouse wheel zooms the font.&lt;/li&gt;&lt;li&gt;Background tabs show an attention dot: muted for new output, red when the shell rang the bell (BEL); cleared when the tab is selected.&lt;/li&gt;&lt;li&gt;Ctrl+click now also opens OSC 8 hyperlinks (the cell's own link wins over heuristics) and local file paths with an optional :line:col, VS Code gets them via &lt;code&gt;code -g&lt;/code&gt; when installed.&lt;/li&gt;&lt;li&gt;Tabs can be reordered: ctrl+shift+pageup/pagedown or the right-click menu (new additive MoveTab command, mirrored to every viewer).&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: terminal settings tab + quake mode&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Settings gains a &amp;quot;terminal&amp;quot; tab: scrollback lines (new panes), mouse wheel speed, copy-on-select, and whether programs may set the clipboard (OSC 52).&lt;/li&gt;&lt;li&gt;Quake mode: a global hotkey (e.g. F9/F12, configurable there) shows/ hides the window from anywhere. Off by default; X11 and Windows (no Wayland global hotkeys yet).&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: vault security: revoke + key rotation, recovery kit&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;A shared vault's members panel now lists current members with a revoke button. Revoking rotates the vault key: every profile is re-sealed with a fresh key wrapped only to the remaining members, so revoked access truly ends (server swaps everything atomically and still sees only ciphertext).&lt;/li&gt;&lt;li&gt;Recovery kit: the account screen can generate a one-time recovery code (shown once, copied to the clipboard) that can recover the account key if the vault passphrase is lost. The code never leaves the machine; the server stores only a sealed blob.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: daemon idle policy (M3 leftover)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;With no live sessions and no attached viewers for 15 minutes, the daemon exits. Dormant sessions stay on disk; the next launch spawns a fresh daemon that loads them.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: Windows sessions survive the window (named pipes)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The mux daemon now runs out-of-process on Windows too, over a per-user named pipe (auto-spawned, detached, one instance per user). Closing or killing the window no longer ends your shells: reopening Termuna attaches straight back into the live session with scrollback replayed, exactly like on Linux. This closes the long-standing M3 item; the in-memory transport remains only as the test harness.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: Windows installer&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;A real Windows installer (NSIS, ~5 MB): per-user, no admin prompt, installs to %LOCALAPPDATA%\Programs\Termuna with a Start Menu shortcut and an Apps &amp;amp; Features entry; /S installs and uninstalls silently. Uninstall leaves user data (sessions, vault, config) in place.&lt;/li&gt;&lt;li&gt;The Windows exe is a proper GUI app now: no console window flashes on double-click (CLI flags still print when run from a terminal), and the moon-cursor icon is embedded (Explorer, taskbar, Alt-Tab).&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: consistent form styling and Tab focus&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Text inputs and other bare widgets now draw with the app's own theme everywhere. They used to fall back to iced's built-in theme, which follows the OS light/dark preference: on a light-mode Windows that meant white inputs floating in our dark chrome.&lt;/li&gt;&lt;li&gt;Tab / Shift+Tab move focus between inputs in every form (sign-in, connection editor, settings, search). Inside the live terminal Tab still belongs to the shell.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: shared connections work across devices&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;A connection whose SSH key path does not exist on this device now falls back to this device's default key (~/.ssh/id_*) instead of failing with a missing-file error. Profiles sync across machines and teammates, but private keys never travel with them: each device authenticates with its own key. A leading &lt;code&gt;~&lt;/code&gt; in key paths is now expanded on every platform.&lt;/li&gt;&lt;li&gt;Opening the ssh connections screen triggers an immediate silent vault refresh, so a teammate's fresh share appears right away instead of on the next minute tick.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: share dialog says who can't receive a share&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Teammates who haven't published an encryption key yet (never unlocked the vault in their app) are shown as unselectable with the reason, instead of being silently skipped at grant time: &amp;quot;shared with 0 teammate(s)&amp;quot; can no longer happen by surprise.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: background vault sync&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Shared connections now arrive on their own: while you are signed in with the vault unlocked (or the passphrase remembered on this device), the app quietly re-syncs every vault once a minute, so a teammate's share shows up without pressing sync or restarting. Offline failures are silently ignored: nothing interrupts.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: Windows actually works&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Terminal query responses (cursor-position reports, device attributes) from the pane emulator are now written back to the shell's pty. ConPTY probes the terminal on startup and blocks all further output until the probe is answered, so every Windows pane sat frozen at an empty grid; the same fix answers vim/tmux probes over SSH on every platform.&lt;/li&gt;&lt;li&gt;Windows now defaults the renderer to DX12. Hybrid-GPU laptops with dated Vulkan drivers picked Vulkan on the discrete GPU and presented into the void: a stale first frame (the app looked unclickable) and a render loop spinning a full core. An explicit WGPU_BACKEND still wins.&lt;/li&gt;&lt;li&gt;Session persistence, the connection vault, and config.toml resolve to real Windows locations (%LOCALAPPDATA%\termuna, %APPDATA%\termuna) instead of an XDG-shaped relative path that landed in whatever directory the process started from (and failed with access denied).&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: settings tabs, color picker, Tango Dark&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Settings are organized into tabs (appearance / keybindings): room to grow without a wall of controls.&lt;/li&gt;&lt;li&gt;Custom colors got a real picker: color wells that expand into curated swatches (theme default, grays, luna, the Tango ramp) plus hue/ saturation/lightness sliders with a live preview and hex readout.&lt;/li&gt;&lt;li&gt;New built-in theme &amp;quot;Tango Dark&amp;quot;: the GNOME/VTE default palette on a near-black surface: the classic Terminator look. (Any installed monospace font, e.g. Ubuntu Mono, is already offered in the font dropdown.)&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: resizable splits, appearance profiles, keybinding editor&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Split dividers are draggable: grab the strip between panes to resize (the cursor shows a resize arrow); the new SetSplitRatio command keeps every viewer and the daemon on the same layout. Clicking into a pane focuses it in every mouse path (including apps that own the mouse).&lt;/li&gt;&lt;li&gt;Appearance profiles (Terminator-style): named presets holding theme, font, cursor, and custom foreground/background overrides (#rrggbb over any theme). New / duplicate / remove in settings; &amp;quot;set as default&amp;quot; picks what the app uses everywhere. Old flat configs migrate into a &amp;quot;default&amp;quot; profile automatically.&lt;/li&gt;&lt;li&gt;Keybindings are editable in settings: every action with its combo, Enter applies (chords supported), saved to config.toml.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: mouse reporting, OSC 52, URL clicks, PRIMARY selection&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Mouse reporting: clicks, drags (deduped per cell), and wheel are forwarded to applications that request them (SGR 1006 and legacy X10 encodings), so htop/vim/tmux respond to the mouse. Shift bypasses reporting for local selection, as everywhere else.&lt;/li&gt;&lt;li&gt;OSC 52 clipboard, write-only: programs (vim/tmux over SSH) can set the local clipboard; read requests are never answered, so nothing can exfiltrate what you copied.&lt;/li&gt;&lt;li&gt;Ctrl+click opens http(s) links under the cursor (wrapped lines joined).&lt;/li&gt;&lt;li&gt;Finishing a selection copies it to the PRIMARY selection and middle-click pastes it: the Linux terminal convention.&lt;/li&gt;&lt;li&gt;New core stress test: alt-screen x resize x clear (the most re-broken bug in Warp's public history) runs in CI.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: terminal input correctness (vim/less/htop basics)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Bracketed paste: when the running program enables it, pasted text is wrapped in paste markers (end marker stripped from the payload), so a multiline paste no longer executes line by line in the shell and vim stops auto-indenting pastes.&lt;/li&gt;&lt;li&gt;Application cursor keys (DECCKM): arrows and Home/End switch to SS3 encoding when full-screen programs ask for it.&lt;/li&gt;&lt;li&gt;Modifier-encoded keys: ctrl/alt/shift + arrows, Home/End, Delete, Insert, PageUp/Down now send the xterm CSI-1;m forms (ctrl+arrow word jump in shells works), and F1-F12 are wired up.&lt;/li&gt;&lt;li&gt;Mouse wheel in the alternate screen scrolls the application (arrow keys, three per notch) instead of doing nothing: vim, less, and htop scroll from the wheel now.&lt;/li&gt;&lt;li&gt;New research note docs/research/warp.md: basics comparison against Warp plus a bug-lesson checklist mined from its public issue history.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: session names and one-key access on the dashboard&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The daemon now wraps each cloud session's content key to the account's vault public key (the same key custody as the SSH connection vault) and pushes the session title sealed with the content key; renames refresh it. The relay stores blobs it cannot read.&lt;/li&gt;&lt;li&gt;On termuna.com/app, one vault-passphrase unlock now shows every session's real name and lets Open work in any browser: no more pasting a share link per session. New additive TSP frames (AccountInfo, SessionMeta); old peers ignore them.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: &amp;quot;copied ✓&amp;quot; feedback on copy link&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Clicking &amp;quot;copy link&amp;quot; on a session row now flashes the button to &amp;quot;copied ✓&amp;quot; for two seconds, so you know the share link is on the clipboard.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: closing the last tab returns home&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Closing the only tab (or exiting the last shell) quit the whole app, a leftover from before the picker existed. The end of a session now returns to the sessions screen.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: confirmation before removing a connection&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;&amp;quot;remove&amp;quot; on a host no longer deletes immediately: an inline &lt;code&gt;remove &amp;quot;&amp;lt;name&amp;gt;&amp;quot;?&lt;/code&gt; confirm (red remove / cancel) appears under the card first.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: quieter ⋯ on host cards&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The ⋯ (actions) affordance no longer paints a hover box inside the card; the dots just brighten on hover.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: host actions: move and share respond again&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Clicking &amp;quot;move&amp;quot; or &amp;quot;share&amp;quot; on a connection did nothing: they opened their panel state but left the actions strip open, and the actions branch renders first, so the new panel never appeared. Both now close the strip and swap to their panel.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: vault picker in the host form&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Adding or editing a connection asks which vault it belongs to (a dropdown next to the port field); Personal is preselected for new hosts, and editing keeps the host's current vault.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: picker actions in the heading row&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;&amp;quot;+ new local session&amp;quot;, &amp;quot;+ add ssh host&amp;quot;, and &amp;quot;+ new vault&amp;quot; moved from the bottom of their lists into the heading row, top-right: long lists were pushing them out of sight. Their editors (host form, vault name) now open directly under the heading for the same reason.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: files panel survives re-attach&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Re-attaching to an SSH session from the picker lost the files button: the viewer forgot which connection profile the session belonged to. The daemon now reports each session's SSH destination (&lt;code&gt;user@host:port&lt;/code&gt;) in the session list, and the viewer maps it back to the vault profile on attach, so SFTP stays available.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: SFTP files panel: upload and mkdir&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Upload now opens a native &amp;quot;open file&amp;quot; dialog and sends the chosen file to the current remote directory. Previously one text box served both upload and mkdir, so &amp;quot;upload&amp;quot; did nothing unless you happened to type a full local path into it: unclear, and it looked broken.&lt;/li&gt;&lt;li&gt;That box is now purely a folder search: it filters the listing, and the mkdir button stays disabled until the search names a folder that doesn't exist yet, then it arms as &amp;quot;mkdir &amp;lt;name&amp;gt;&amp;quot; and creates it.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: Linux desktop integration&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;&lt;code&gt;scripts/install-desktop.sh&lt;/code&gt; installs the binary plus a launcher entry and the moon-cursor icon into the hicolor theme, so Termuna shows up in app search and the dock with its own icon. The window now sets &lt;code&gt;application_id&lt;/code&gt;/WM_CLASS to &lt;code&gt;termuna&lt;/code&gt;, matching &lt;code&gt;StartupWMClass&lt;/code&gt; for correct dock grouping. New &lt;code&gt;apps/termuna/assets/&lt;/code&gt; (icon SVG + .desktop).&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: luna teal rebrand + app icon&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The default theme is now &amp;quot;Luna Dark&amp;quot;: near-black surface with luna-teal accents (#2dd4bf, the brand color) on the cursor, active tab, focused pane border, and selection. The ANSI ramp keeps honest terminal semantics: green output stays green. &amp;quot;Luna Light&amp;quot; replaces &amp;quot;Phosphor Light&amp;quot;; the old &lt;code&gt;phosphor-*&lt;/code&gt; theme ids still resolve so existing configs keep working.&lt;/li&gt;&lt;li&gt;The window/taskbar icon is the moon cursor: a luna-teal block cursor with a circular lunar bite (luna is in the name: ter-muna): drawn procedurally, no image assets or decoders.&lt;/li&gt;&lt;li&gt;The sidebar wordmark matches the website: &amp;quot;termuna&amp;quot; with the moon cursor as its eighth character (vector-drawn canvas mark), replacing the plain dot.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: close tabs&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The active tab now has an inline × (red on hover) that closes it, every pane in the tab goes; closing the only tab ends the session and returns to the picker. Also available as &amp;quot;close tab&amp;quot; in the right-click menu (which now says &amp;quot;home&amp;quot; instead of &amp;quot;sessions&amp;quot;).&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: leaner terminal tab bar&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The tab bar's right side is now a single ⌂ home button that returns to the picker (where sessions, connections, and settings live). The &amp;quot;copy cloud link&amp;quot;, &amp;quot;sessions&amp;quot;, and ⚙ buttons are gone: the cloud link stays available on the session's row in the picker. &amp;quot;files&amp;quot; still shows for SSH sessions.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: scrollback survives reboot&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;A session's terminal output (scrollback) is now persisted to disk (the last ~1MB per session, written on a 3s throttle only while output flows) and replayed on resurrection. Combined with the existing tab/split tree + per-pane working directories, resurrecting a session after a reboot brings back the same layout, the same directories, and the prior on-screen history, then a fresh shell continues from there. Running processes still don't survive a reboot (a reboot kills them); this restores the view, not live process state. New per-session &lt;code&gt;.log&lt;/code&gt; file; covered by &lt;code&gt;scrollback_survives_daemon_restart&lt;/code&gt;.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: manage sessions from the picker&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Rename a session inline in the picker (&amp;quot;rename&amp;quot; → type → Enter/save), and end one for good with &amp;quot;kill&amp;quot;. Rename and kill work on live and dormant sessions alike; killing deletes the persisted tree so it does not come back on the next daemon start. A rename mirrors to any attached viewer and, for cloud-bridged sessions, syncs end to end via the session tree. New &lt;code&gt;MuxRequest::{RenameSession,KillSession}&lt;/code&gt; and &lt;code&gt;Session::rename&lt;/code&gt;.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: launch always lands on the picker&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Starting Termuna now always opens the session picker (sessions, saved hosts, cloud sign-in), even on a fresh install with nothing saved. It no longer auto-spawns a local shell on launch; &amp;quot;+ new local session&amp;quot; is one click away.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: two-pane connection manager&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The picker is a two-pane layout with a persistent left sidebar (lowercase nav: sessions, ssh connections, settings, and an account entry pinned at the bottom showing the signed-in email). The sidebar stays visible on every section, including settings, and shows a live session count.&lt;/li&gt;&lt;li&gt;New account screen (sidebar → account): signed-in email, organization and team-member count, and a sign-out button (clears the local token and any remembered passphrase, and stops the daemon's cloud bridge). The cloud sign-in / status moved here out of the SSH Connections section, where it didn't belong. Signed out, the sidebar item and the pane say &amp;quot;sign in&amp;quot; instead of &amp;quot;account&amp;quot;.&lt;/li&gt;&lt;li&gt;Hosts render as a responsive multi-column card grid (name + user@host:port with a ⋯ actions button inside each card), grouped under collapsible vault headers; the column count follows the window width. A card opens its actions on the ⋯ button or a right-click. Resizes cleanly: the sidebar stays fixed, the grid reflows.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: host card grid on wide windows&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Maximizing the window squeezed host cards into overlapping slivers: the grid computed its column count from the full window width while the content column is capped, so it asked for more columns than fit. Both now derive from the same cap. Card texts are truncated so a long host can never paint over its neighbor, and the card hover tint is the dark selection color instead of a bright accent fill that swallowed the text.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: full connection management&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Every saved host now has an actions menu (⋯): edit, duplicate, move to vault, share, and remove. Editing reopens the form pre-filled and round-trips auth, jump hosts, and port forwards losslessly.&lt;/li&gt;&lt;li&gt;Adding or editing a host now just saves and returns to the list; connecting is a separate, explicit click (no more auto-connect on save).&lt;/li&gt;&lt;li&gt;A search box filters hosts by name, host, or user (and expands all vaults so matches show). Deleting a host is finally possible.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: Termius-style vaults (connection organization)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The connection manager is now organized by vault: connections live in a named vault (Personal by default, plus any shared vaults you create). The picker renders a collapsible Vault → Host tree (connections sorted by name) with per-vault counts. Vaults are the single grouping level; the earlier per-connection folders were dropped as redundant (the &lt;code&gt;group&lt;/code&gt; field remains in the format but is no longer used by the UI).&lt;/li&gt;&lt;li&gt;&amp;quot;＋ new vault&amp;quot; creates a named vault; each connection has a &amp;quot;move&amp;quot; action to reassign it between vaults. &lt;code&gt;termuna-vault::Profile&lt;/code&gt; gains a &lt;code&gt;vault&lt;/code&gt; field; assignment persists to the local vault.&lt;/li&gt;&lt;li&gt;Cloud sync is per-vault and action-driven: unlock the vault once with your encryption passphrase (asked inline in the connections section, never uploaded), and from then on changes sync automatically: moving a connection pushes it to its vault, and a vault's &amp;quot;members&amp;quot; button grants teammates access (the vault key is wrapped to each member's public key). Each connection uploads into the cloud vault named by its &lt;code&gt;vault&lt;/code&gt; (Personal → personal vault, others → same-named shared vaults, created on demand). Replaces the old all-or-nothing &amp;quot;share with team&amp;quot; and the separate passphrase box / &amp;quot;sync all vaults&amp;quot; button.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: remember vault passphrase (opt-in)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The cloud-vault unlock is now a discreet &amp;quot;🔒 unlock cloud vault&amp;quot; link that expands into the passphrase field on demand, instead of a prompt that shows on every launch.&lt;/li&gt;&lt;li&gt;A &amp;quot;remember on this device&amp;quot; checkbox stores the passphrase in the OS secret store (GNOME Keyring / macOS Keychain / Windows Credential Manager) so the vault auto-unlocks on launch. Off by default; the passphrase still never touches the relay or plaintext disk.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: settings panel (appearance)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;In-app settings (⚙ in the picker and the tab bar): pick a color scheme, font family, font size and line-height, and cursor style, with a live preview. Changes apply immediately and persist to &lt;code&gt;config.toml&lt;/code&gt; (only the appearance keys are rewritten; keybindings/cloud are kept).&lt;/li&gt;&lt;li&gt;Seven built-in color schemes: Phosphor Dark (default), Phosphor Light, Dracula, Nord, Gruvbox Dark, Solarized Dark, Tokyo Night. The configured theme is now actually applied on launch (it was ignored before).&lt;/li&gt;&lt;li&gt;Font family is selectable from installed monospace fonts (detected via fontconfig) plus bundled JetBrains Mono.&lt;/li&gt;&lt;li&gt;Cursor style: block (default), bar, or underline, with an optional blink. Unfocused panes still show a hollow cursor.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: tab and session titles&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Tab titles now follow the shell's own title (OSC 0/2, e.g. &lt;code&gt;user@host: ~/dev/project&lt;/code&gt;) as the authoritative source, forwarded to the daemon so it mirrors to every viewer and the cloud. The cwd-based auto-title is only a fallback for shells that set no title, so titles no longer flip-flop between the shell title and a bare &lt;code&gt;~&lt;/code&gt;. New &lt;code&gt;Session::set_shell_title&lt;/code&gt;, &lt;code&gt;Tab::shell_title&lt;/code&gt;, and &lt;code&gt;SessionCommand::SetTabTitle&lt;/code&gt;. Tab labels show more of the title.&lt;/li&gt;&lt;li&gt;The picker shows a renamed session by its name alone (`prod cluster · live&lt;code&gt;); auto-named sessions keep the &lt;/code&gt;N tabs, N panes` summary so they stay distinguishable. &lt;code&gt;Session::rename&lt;/code&gt; now marks the title manual and &lt;code&gt;SessionInfo&lt;/code&gt; carries &lt;code&gt;renamed&lt;/code&gt;.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Terminal column alignment: styled runs (e.g. colored directory names in &lt;code&gt;ls -l&lt;/code&gt;) no longer drift left and overlap the column beside them. The cell width was rounded to a whole pixel while the renderer advances glyphs by the true fractional width, so long runs drifted off the grid; the cell width is now the exact glyph advance.&lt;/li&gt;&lt;li&gt;A failed session-list refresh (e.g. a kill/rename an older daemon rejects) no longer blanks the picker: the existing sessions stay visible and the error is shown instead of looking like everything died.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: M9 cloud connection vault (desktop)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Save your SSH connections to Termuna Cloud, end to end encrypted, and share them with your team. A vault passphrase (never leaves the machine) unlocks an account keypair; connections are sealed with a vault key and, for sharing, that key is wrapped to each teammate's public key. The relay only stores ciphertext.&lt;/li&gt;&lt;li&gt;Picker gains, once signed in: a vault passphrase field with &amp;quot;sync connections&amp;quot; (push local, pull cloud, merge) and &amp;quot;share with team&amp;quot; (share with everyone in your organization).&lt;/li&gt;&lt;li&gt;New crate module &lt;code&gt;termuna-sync::vaultcrypto&lt;/code&gt; (X25519 + Argon2id + sealed boxes); desktop client &lt;code&gt;apps/termuna::cloud_vault&lt;/code&gt;. See docs/adr/0004-connection-vault.md.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;macOS tab auto-titles now use the last persisted working directory (like Windows) instead of a live procfs read; dropping the darwin-libproc dependency unblocked the vault crypto crates.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: M7 (desktop UX: the app explains itself)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Interactive cloud sign-in: a &amp;quot;termuna cloud&amp;quot; section in the picker signs in or creates an account, mints a device token, writes config.toml, and flips the daemon to cloud mode at runtime: sessions start syncing immediately, no manual config and no restart. Each synced session shows a &amp;quot;copy link&amp;quot; button; the terminal gets a &amp;quot;copy cloud link&amp;quot; chip.&lt;/li&gt;&lt;li&gt;Tabs are titled by the shell's working directory (&lt;code&gt;~&lt;/code&gt;, &lt;code&gt;lightweight-cli&lt;/code&gt;, …) and follow &lt;code&gt;cd&lt;/code&gt;; double-click (or right-click → rename) gives a tab a permanent name. New &lt;code&gt;RenameTab&lt;/code&gt; protocol command.&lt;/li&gt;&lt;li&gt;Right-click context menu on the terminal: new tab, split right/down, rename tab, copy, paste, close pane, sessions.&lt;/li&gt;&lt;li&gt;A &amp;quot;sessions&amp;quot; button in the tab bar detaches back to the picker: the session list, ssh connection manager, and cloud sign-in are always reachable now (previously the first launch dropped you straight into a terminal with no way back).&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: cross-platform builds + CI (green on all three OSes)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Windows: agent auth is cleanly unix-gated, the GUI falls back to the in-process mux (sessions end with the window until named-pipe transport), home-dir resolution uses HOME/USERPROFILE. The workspace now compiles and tests green on Windows for the first time since M3.&lt;/li&gt;&lt;li&gt;macOS: procfs-only field no longer trips -D warnings.&lt;/li&gt;&lt;li&gt;CI: perf-budget job asserts binary &amp;lt;30MB, daemon socket &amp;lt;500ms, daemon idle RSS &amp;lt;30MB on every push.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Fixed: memory budget (M2 closeout)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Measured the idle-memory budget honestly: with 3 tabs idle the GUI is 83MB PSS and the daemon 4MB: 87MB total, under the 100MB budget. The previously reported 157MB was RSS, which double-counts ~70MB of shared, evictable GPU-driver (Mesa/LLVM) and libc code pages; private memory is ~26MB. Added scripts/mem-budget.sh so the number is reproducible (same isolated-daemon, 3-tab scenario). For scale: an Electron terminal on the same machine idles at ~492MB PSS.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: M5 alpha (cloud continuity)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Cloud bridge in the mux daemon: with &lt;code&gt;[cloud] enabled&lt;/code&gt; in config, every live session connects outbound (WSS) to the Termuna Cloud relay as its TSP host. Output and layout are sealed with a per-session XChaCha20-Poly1305 content key before leaving the machine; viewer input and commands are opened and applied through the same paths as local viewers. Share link &lt;code&gt;https://termuna.com/s/&amp;lt;id&amp;gt;#k=&amp;lt;key&amp;gt;&lt;/code&gt; carries the key in the URL fragment, which browsers never send to the server. Sync stays opt-in and off by default (ADR 0002).&lt;/li&gt;&lt;li&gt;Session content keys persist with the session (reboot keeps the URL); reconnect resumes from the relay's acknowledged head with capped backoff.&lt;/li&gt;&lt;/ul&gt;
&lt;ul&gt;&lt;li&gt;&lt;code&gt;[cloud] token&lt;/code&gt; in config: a device token from the termuna.com dashboard binds the daemon's sessions to your account (Authorization header on the relay connection). Free plan allows 1 concurrent live cloud session; Solo removes the limit.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: M5 alpha&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Content crypto switched from seq-derived nonces to random 24-byte nonces prepended to the ciphertext: seq nonces would repeat across multiple writers sharing the session key (host + viewers), which is catastrophic for Poly1305. Interop-tested against the web viewer's JS implementation.&lt;/li&gt;&lt;li&gt;TSP ciphertext fields now encode as CBOR byte strings (&lt;code&gt;serde_bytes&lt;/code&gt;) instead of integer arrays: smaller frames, sane browser decoding.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: M3 (persistence: the mux daemon)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;&lt;code&gt;termuna-mux&lt;/code&gt; crate: a daemon (&lt;code&gt;termuna --daemon&lt;/code&gt;, auto-spawned by the GUI, detached so it outlives it) that owns every PTY and the authoritative session tree. Transport: TSP frames over a Unix socket (0700 runtime dir) with a small handshake (list/create/resurrect/attach); in-memory duplex transport for tests and socketless platforms.&lt;/li&gt;&lt;li&gt;Sessions survive the window: killing the GUI leaves shells running; reattaching replays the daemon's retained output log: full screen and scrollback reconstructed. Verified live on X11.&lt;/li&gt;&lt;li&gt;Reboot resurrection: every structural change persists the session tree and per-pane working directories (procfs) to disk; after a daemon restart, sessions appear as dormant and one click restores the whole layout with fresh shells in their saved directories. Verified live.&lt;/li&gt;&lt;li&gt;Session picker on startup: live/dormant list with tab/pane counts, plus new-session; shell &lt;code&gt;exit&lt;/code&gt; cleanly ends the session everywhere.&lt;/li&gt;&lt;li&gt;TSP gains the &lt;code&gt;Command&lt;/code&gt; payload (&lt;code&gt;SessionCommand&lt;/code&gt;: NewTab, Split, ClosePane, SelectTab, FocusPane, ResizePane): viewers request structural changes, the host applies and broadcasts a &lt;code&gt;Layout&lt;/code&gt; snapshot. The GUI is now literally the first TSP viewer; the web dashboard will reuse this path unchanged.&lt;/li&gt;&lt;li&gt;Scrollback search: Ctrl+Shift+F opens a search bar; case-insensitive scan over the full scrollback with match counter and jump-to-match (Enter walks older matches). 40k lines scan in well under 500ms in the test suite: the anti-benchmark being the popular Electron terminal's search hang.&lt;/li&gt;&lt;li&gt;4 mux integration tests (attach/type roundtrip over real shells, split broadcast, detach/reattach replay, persistence + resurrection across a daemon restart); 39 tests total across the workspace.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Changed: M3&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The GUI holds no PTYs anymore: &lt;code&gt;pane.rs&lt;/code&gt; mirrors daemon state (emulator + cache + selection per pane) and all input/structure flows through TSP frames. Focus and tab selection are session state shared by all viewers, applied by the daemon.&lt;/li&gt;&lt;li&gt;Startup with the daemon hop measured at 288ms to first output frame (debug build, cold daemon spawn): still inside the &amp;lt;300ms budget.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added: M2 (windowed terminal)&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Windowed GPU terminal (iced 0.14 + wgpu): canvas grid renderer with run-batched text, per-pane geometry caching, block cursor (filled when focused, hollow otherwise), underline/strikethrough, selection overlay, and a scrollback position indicator.&lt;/li&gt;&lt;li&gt;Tabs UI with OSC-title tracking and nested split panes rendered straight from the &lt;code&gt;termuna-session&lt;/code&gt; tree; pane focus follows clicks and hotkeys.&lt;/li&gt;&lt;li&gt;Default hotkeys (all remappable, two-step chords supported): Ctrl+Shift+T/W new/close, Ctrl+Shift+D/E split right/down, Ctrl+Shift+arrows focus, Ctrl(+Shift)+Tab tabs, Ctrl+Shift+C/V copy/paste, Shift+PgUp/PgDn scrollback, Ctrl+Shift+plus/minus font size.&lt;/li&gt;&lt;li&gt;TOML config at &lt;code&gt;~/.config/termuna/config.toml&lt;/code&gt; (font, theme, keybindings, scrollback) with graceful fallback on parse errors.&lt;/li&gt;&lt;li&gt;Phosphor Dark theme (brand palette as ANSI-16), bundled JetBrains Mono (Regular/Bold/Italic/BoldItalic, OFL).&lt;/li&gt;&lt;li&gt;Mouse: click to focus, drag to select, wheel scrollback; typing jumps back to the live view.&lt;/li&gt;&lt;li&gt;&lt;code&gt;termuna-core&lt;/code&gt;: render-snapshot API (palette-resolved RGB cells, cursor, styles) and scrollback scrolling behind the facade; &lt;code&gt;TermEvent&lt;/code&gt; re-export.&lt;/li&gt;&lt;li&gt;&lt;code&gt;termuna --inline&lt;/code&gt; keeps the M1 engine harness; the window is now the default entry point.&lt;/li&gt;&lt;li&gt;Verified live on X11: bash, colored ls, htop full-screen TUI, splits, tabs, scrollback. Release-build measurements on Linux/X11: cold start to first shell output &lt;strong&gt;176ms&lt;/strong&gt; (budget &amp;lt;300ms ✓), binary &lt;strong&gt;14.8MB&lt;/strong&gt; (installer budget &amp;lt;30MB ✓), RSS ~157MB with a shell running: above the &amp;lt;100MB budget, dominated by wgpu/driver allocations; memory optimization is an open M2 item (tracked in docs/ROADMAP.md).&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Cargo workspace with the M1 engine crates: &lt;code&gt;termuna-protocol&lt;/code&gt;, &lt;code&gt;termuna-session&lt;/code&gt;, &lt;code&gt;termuna-pty&lt;/code&gt;, &lt;code&gt;termuna-core&lt;/code&gt;, &lt;code&gt;termuna-sync&lt;/code&gt;, and the &lt;code&gt;termuna&lt;/code&gt; binary.&lt;/li&gt;&lt;li&gt;TSP v1 wire format (CBOR frames, per-session sequence numbers, resume semantics) with roundtrip and version-rejection tests.&lt;/li&gt;&lt;li&gt;Session model: tabs and nested split trees with focus handling, revision-counted snapshots for cloud mirroring, full unit coverage.&lt;/li&gt;&lt;li&gt;Portable PTY layer (Unix pty verified by a live shell test; Windows ConPTY path compiles, pending manual QA).&lt;/li&gt;&lt;li&gt;VT emulation core wrapping &lt;code&gt;alacritty_terminal&lt;/code&gt; behind a swappable facade.&lt;/li&gt;&lt;li&gt;Sync engine skeleton: XChaCha20-Poly1305 content encryption with seq-derived nonces, injectable transports, ack/resume watermarks.&lt;/li&gt;&lt;li&gt;Inline dev harness: &lt;code&gt;cargo run&lt;/code&gt; executes your shell through the engine and logs engine-ready time (startup budget instrumentation from day one).&lt;/li&gt;&lt;li&gt;Project documentation: ARCHITECTURE, PROTOCOL (TSP draft), ROADMAP (M1–M6), competitive research (Tabby, Termius, tech stack), ADR 0001 (tech stack) and ADR 0002 (offline-first, opt-in sync), CLAUDE.md.&lt;/li&gt;&lt;/ul&gt;
&lt;h3&gt;Decided&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;Stack: Rust everywhere; alacritty_terminal core; iced 0.14 + cosmic-text UI (M2); local mux daemon for persistence (M3); axum cloud (M5). See ADR 0001.&lt;/li&gt;&lt;li&gt;Product: offline-first, no mandatory account, full export at every tier, no AI in the input path. See ADR 0002.&lt;/li&gt;&lt;/ul&gt;</description>
    </item>
  </channel>
</rss>
