# Changelog All notable changes to tty7 are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [26.9.4] - 2026-09-29 ### Added - **The New Tab menu names the agents you actually run** (#955). Beside Local and SSH, an Agents section lists up to three agents that have been launched or seen running, most-used first — the same short list the shells get. A row opens that agent in a new tab (hold ⌥ to split) and types its `agent_launch` command, or the bare binary when none is set. Agents that are installed but not in those rows are one click away: **Other Agents…** opens Search Everywhere already filtered to `agent`. The section is absent when this machine has no agent to offer. **New Agent Tab** (⌘⇧A) is unchanged: it still starts the one used last. - **Find in files, from the right panel's Files tab.** Its field now matches file names and file contents at once, in two sections. Type and the active tab's project — the same roots the tree shows — is searched as you go, with content hits grouped by file, a count per file, and each match highlighted in its line. Click a hit to open the file in the built-in editor at that line and column; Enter searches again. Toggles for match case, whole word and regular expressions sit at the end of the field. The walk honours `.gitignore` with or without a repository, skips dot-directories, binary files and files over 1 MB, and says so when a cap (2,000 lines, 100 per file, 20,000 files, ten seconds) cut it short. In a remote workspace the search runs on the remote machine through `tty7-server`; a server that predates it is never sent the request — the tab asks for the server to be updated instead of showing an empty result. - **A GitHub tab in the right panel: issues and pull requests, read-only.** It follows the focused pane's repository, binds to its `github.com` remote (`upstream` over `origin` in a fork, with a menu to pick another), and lists open or closed issues or pull requests with state glyphs, labels and relative times; a label click filters by it. A row opens the detail — description and comments as Markdown, and for a pull request its branches, size and changed files, each of which opens its patch in the diff overlay. Sign-in reuses the GitHub CLI (`GH_TOKEN`, `GITHUB_TOKEN`, then `gh auth token`, found even from a Finder launch); public repositories work signed out, and a private repository or a spent rate limit says to run `gh auth login`. Screenshots pasted into an issue are shown; images from any other host are shown as links rather than loaded, and non-web link targets are disabled. The tab talks to `api.github.com` only while you use it. - **Reorder the active tab from the keyboard** (`MoveTabLeft` / `MoveTabRight`). The tab moves one slot past its neighbour — the keyboard form of dragging it in the tab strip or the sidebar — and wraps past either end, so one held key walks a tab the whole way down the list. On a left tab bar the same pair reads as up and down, and the tab stays inside its sidebar group, wrapping at the group's ends. Shipped unbound like the pane-swap pair: bind it under Settings → Keyboard shortcuts ("Move Tab Left" / "Move Tab Right"), in `config.json`, or run it from the palette. - **Quick launch for the coding agents on your PATH** (#955). Every agent tty7 recognises whose binary is on `PATH` is a palette command — "Agent: Claude Code", "Agent: Codex", … — ordered by how often and how recently it was launched or seen running, and each is bindable as `LaunchAgent:`. "New Agent Tab" (⌘⇧A on macOS, unbound elsewhere, where Ctrl+Shift+A is select-all) starts the one used last. A launch always opens a new tab in the current tab's directory and types the agent's command into its shell once the pane exists, never into a pane that was already there, so detection, status and resume work as for a hand-typed agent and quitting it returns to the shell. The command is the agent's bare binary unless `agent_launch` in `config.json` gives it one (`"claude": "claude --dangerously-skip-permissions"`); a wrapper named there is detected as that agent without an `agent_commands` entry, including a script run under its interpreter, and the daemon picks up an edit without a restart. A running agent's pane menu has "Set Current Launch Args as Default", which writes the flags it was started with — minus the session it resumed and any prompt — into `agent_launch`. A remote workspace cannot be asked for its `PATH`, so there the list is the agents already seen running in that workspace. - **Tabs can be put to sleep, and woken where they were** (#762). Right-click a tab → Hibernate, or "Hibernate Tab" in the command palette, stops every process in the tab to give its memory back while the tab keeps its place in the sidebar, faded and marked with a moon. Selecting it wakes it — or right-click → Wake to warm it up without switching. Waking is the restore a reboot already runs: each pane comes back in its old directory and shell, opens on the screen it left, and a supported agent with a captured session is resumed. A sleeping tab stays asleep across app and daemon restarts; the tab on screen is always awake, so the last awake tab cannot be put to sleep. `tty7 pane ls` shows a sleeping tab's panes as `asleep`, and `tab ls`/`pane ls --json` carry a `hibernated` flag. Offered only where the machine's server can do it (this build or newer). - **A port forward can be switched off without losing its rule** (#439). A forward could only be removed, so pointing one local port at a different remote target meant deleting the rule and typing the other one in again. Each forward in the Ports section now has a switch: off releases the listener and keeps the rule in the list, on binds it again from the rule it was made from. Switching one on while another switched-on forward holds its port is refused with a notice naming that forward, rather than failing at bind time. A switch flipped in the panel on a rule that came from a saved host is written back to that host, so the next connection opens the same set; a rule saved switched off is listed but not opened. Profiles saved before this load with every rule on. - **Windows file paths are links** (#965). `C:\Users\me\a.png`, `c:/Users/me/a.png` and `\\server\share\a.png` underline and open like any other file path, with a `:10:2`, `(10,2)` or `#L10` location kept. The path is found when Chinese prose is glued straight onto it (`图片已保存到:c:/Users/me/a.png`) and inside a Markdown link (`![chart](c:/out/chart.png)`), and is handed on with backslashes so Explorer opens and reveals it — a forward-slashed path used to open Documents instead. In a WSL pane a drive path is looked up under `/mnt/`. A letter and a colon alone (`a:b`, `C:`, `C:notes.txt`) is never read as a drive. - **A quoted path may contain spaces** (#965). `"C:\Program Files\app\app.exe"`, `'/Users/me/My Docs/a.txt'` and the same in backticks are one link, with a `:10:2` or `(10,2)` location read inside the quotes or just after the closing one. Only quotes do this: an unquoted space still ends a path, and quoted prose that is not written like a path — no separator, a space at either end, over 260 characters — is left alone. - **"Open with Default App" in a file link's right-click menu** (#965). It hands the file to whatever the OS has it associated with, whatever `link_file_open` makes "Open" do. Shown for files on this machine only, and not when "Open" already uses the system opener. - **The code editor talks to language servers** (#1002). Opening a local file starts the server for its language — rust-analyzer, typescript-language-server, pyright (or pylsp), gopls or clangd, whichever is on `PATH` — one per project root, shared by every buffer under it. Diagnostics are underlined with their message on hover and counted in the status bar, and a Problems list (⌘⇧M, Ctrl+Shift+M elsewhere) gathers the errors and warnings of every open file. Completion, hover, signature help, go to definition (F12 or ⌘-click), Find All References (⇧F12), code actions (⌘.), Format Document (⇧⌥F) and Rename Symbol (F2) work across files; the keys are bound inside the editor only, so terminal programs keep F2 and F12. A server missing from `PATH` costs only its language, with a one-line hint in the status bar; a server whose last file closed shuts down after a grace period, and one that crashes is restarted a few times. Files on a remote workspace get no server. `editor_lsp` turns it all off. - **Multiple cursors, git change markers, symbols and a split in the code editor** (#1002). ⌘D, ⌘⇧L and ⌘⌥↑ / ⌘⌥↓ add cursors, and the status bar counts the selections. The gutter marks lines added, modified or deleted against the file's staged version, on remote workspaces too; ⌥F5 / ⇧⌥F5 step through the changes, ⌥F3 or a click on a marker peeks one, and Revert Change undoes it as one step (`editor_git_gutter` turns the markers off). Go to Symbol (⌘⇧O) lists the file's outline, previewing each symbol as it is arrowed through, and once a query is typed adds the language server's matches from across the project; a breadcrumb row over the text shows the path and the symbols around the caret. Back and forward (Ctrl+− / Ctrl+⇧+− on macOS, Alt+← / Alt+→ elsewhere) walk a per-tab history of jumps. ⌘\ (Ctrl+\ elsewhere, bound only in the editor so the terminal keeps SIGQUIT) opens the file in a second group on the right, saved with the tab. The right-click menu gains Toggle Comment, Move, Duplicate and Delete Line and Go to Matching Bracket, and case transforms, Trim Trailing Whitespace and Join Lines are bindable commands. **Settings → General → Editor** holds the change markers, language servers, soft wrap and rendered Markdown. - **Quick open, Go to Line, New File and Save As in the code editor** (#984). Go to File (⌘O on macOS, unbound elsewhere) finds any file in the active tab's project by fuzzy name — on local, SSH and WSL workspaces alike, skipping what the Files tree hides — and `name:line[:col]` jumps to that spot. Go to Line is Ctrl+G. Save As uses the native panel locally and a path bar on a remote workspace, and the files each tab has open are remembered across restarts. Loading and saving keep a file's encoding (UTF-8, GB18030, Windows-1252), byte-order mark and CRLF line endings as they were, indentation is detected, `.editorconfig` is honoured, and the status bar shows indentation, encoding and line ending. - **A local file pasted or dropped into a remote pane is uploaded there** (#1004). Copying a file in Finder and pasting it into an SSH pane, or dropping it on one, pasted this machine's path, which the remote program cannot open; only clipboard images were uploaded. Every local path handed to a pane now takes one route: on an SSH host it is uploaded into a directory of its own under `~/.cache/tty7/clipboard`, so it keeps its real name, and the remote path is pasted; in a WSL pane it is rewritten to `/mnt//…`; locally it is pasted as before. Folders upload recursively, a large file appears at its path only once complete, a transfer is given up only after it stops making progress, and staged pastes older than a week are pruned. Rows dragged from a remote Files tree are no longer taken for local files. - **The GitHub tab shows a pull request's checks, reviews and merge state** (#1000). The detail leads with the check runs and commit statuses on the head commit, failures first, with durations and links to their logs; then one row per reviewer; and a line under the branches such as "Waiting on 1 check", "Merge conflicts" or "Ready to merge". While a check is running the checks are re-read every 20 seconds when signed in. The list pins the pull request of the pane's branch under the repository row, found through the branch's upstream so a fork's branch resolves under the fork's owner. Long comment threads, descriptions, reviewer lists and file lists fold behind a "show all" row. - **The GitHub tab tries every signed-in `gh` account** (#987). Only gh's active account was used, so a private repository owned by an organisation that another signed-in account belongs to read as a 404. On a 404, 401 or 403 the tab now retries with gh's other `github.com` accounts and remembers, per owner, the one that got through. A token from `GH_TOKEN` or `GITHUB_TOKEN` is still used on its own. - **A link a table wrapped inside its cell opens whole** (#1001). Markdown tables — Claude Code's among them — wrap a long cell by hand, one bordered row per line, so hovering or clicking a wrapped URL only ever got the fragment on one row. A link now carries on from a row into the next when its text reaches the cell's right border and resumes at the left one, with the borders lined up; only `│`, `┃` and `║` count as borders, never a bare `|`. The hover underline covers just the cell's text on each row. - **Qoder CN CLI support** (by @ArnoChenFx in #988). The mainland-China build of Qoder (`qodercn`, `qoderclicn`, `qoder-cn`) is recognised, reports its status through hooks and has its past sessions listed, honouring `QODERCN_CONFIG_DIR`. - **Prime Agent and Empryo are recognised, and Antigravity reports its status** (by @kalpakprod in #975). Prime Agent (`prime-agent`) resumes and forks its sessions and reports status through the same extension bridge as Pi. Empryo (`empryo`) is detected and resumed with `--session`; it gets no status hooks yet, because it strips tty7's variables from hook processes. Antigravity CLI (`agy`) gets a `tty7` hook set in `~/.gemini/config/hooks.json`, beside the user's own, so its tab shows working and done. - **A working agent's status dot blinks.** It alternates every 600 ms between its colour and a paler blend of it, so a column of tabs shows at a glance which agents are still going. The repaints stop once no agent is working. ### Changed - **Every pick-one setting is a dropdown.** Settings drew a single choice two ways — a row of segmented buttons for most, a dropdown for the language, the window backdrop and the rest — with nothing to say which got which, and the segmented rows ran to whatever width their labels made, so the right-hand column never lined up. All of them are dropdowns now, one width. A value set by hand off the presets (scrollback, the notification threshold) shows as a checked "Custom (N)" at the foot of the menu, as the extra segment did. - **The mouse wheel no longer zooms the font by default.** ⌘ (Ctrl elsewhere) plus the wheel used to resize the font, and ⌘ is held for so much else that the text jumped size mid-scroll. `mouse_zoom_modifier` now defaults to `none`; pick a modifier in Settings to have the wheel zoom again. ⌘+ / ⌘− are unchanged. - **The command palette is now Search Everywhere, with tabs.** ⌘ P (Ctrl ⇧ P elsewhere) opens one search over **All**, **Terminals**, **Sessions**, **Hosts** and **Commands**, walked with ⇥ / ⇧ ⇥, keeping what is typed. **Terminals** lists every open tab of every workspace, this window's most recently used first, and jumps to it wherever it lives; the shells and agents it can open follow. **Hosts** holds the saved SSH hosts and whatever address or `ssh …` command line is typed, which replaces the separate input *SSH: Add Connection…* used to open. **All** shows the best few rows of each, the tab with the best match first, and folds the rest into a row that opens its tab; before anything is typed it leads each section with its recent rows and tops it up to five from the rest, so a fresh window is not empty. With nothing typed, ⌘ P ⏎ goes back to the tab you were just in. The keybinding action is still `TogglePalette`, so a custom binding keeps working. - **Resume a past agent session from Search Everywhere.** Its **Sessions** tab lists past sessions of Claude Code, Codex, Gemini CLI, Qwen Code, Qoder, CodeBuddy, Pi, Oh My Pi, Kimi Code, Copilot CLI, Droid, Cursor CLI and OpenCode — those that ran in the focused tab's directory first — by the title the agent gave them, with directory, branch and age, and ⏎ resumes one in a new tab in the directory it ran in, with the agent's configured launch flags. In a remote workspace the list is that machine's sessions, and they resume there. ⌘ E (Ctrl E elsewhere) on a session opens what else can be done with it: fork it, for the agents that can, copy its id, or *Remove from List*, which only hides it from the tab (`hidden_agent_sessions` in `config.json`) and leaves the agent's history alone. Only the ends of each transcript are read, in the background, and remembered until the file changes; OpenCode's and Cursor's SQLite stores are opened read-only. The control dialect moves to v12, so each remote host needs one Update Server, which ends the sessions on it. - **The command-line ghost suggests what you ran last, not what you ran most.** It was the top prefix match by frecency, where run count and the current-directory bonus outweighed recency, so after `git commit -m x` typing `git c` still offered `git checkout main` because that had run 20 times here. The ghost is now the newest entry that extends the line, preferring one run in the current directory and falling back to the newest anywhere, and it skips commands whose last run exited non-zero, so it names what ↑ recalls. Ctrl+R still ranks by frecency. Re-running a command now moves it to the newest history entry instead of adding a second copy (before, only an immediate repeat was collapsed), so ↑ steps onto each command once, the same as after a restart. - **The sidebar groups tabs by repo automatically; pin what you want to keep** (#955). Groups now come in two halves. Below, every tab you have not pinned is filed under its git repository, and an SSH tab under the host it is on rather than its remote path — `/home/ubuntu` on two machines used to share a header. Above sit the groups you keep, each marked with a pin beside its name, in the order you drag them into, until you delete them. A pinned group can keep a folder: a tab whose working directory enters it joins it (the deepest folder wins when they nest, and a worktree of a pinned repo counts), while a tab you drag out stays out until it leaves the folder and comes back. Pin an auto group with the pin on its header or by dragging the header up among the pinned ones; pin a folder by dropping it from Finder, with **Pin as Group** in the Files panel, or with **Open Folder as Group…** in the palette. **New Group** in the palette, on a tab's right-click or on a right-click in the empty sidebar below the tabs makes a label group. Deleting a group closes nothing — its tabs go back to auto grouping — and dragging a tab below the pinned groups does the same for one tab. Groups, their order and which are folded are stored with the workspace, so every window onto it agrees. **Settings → General → Auto grouping** replaces the three-way *Sidebar grouping* choice; off, unpinned tabs sit in one flat list under the pinned groups. Scratch is now **Ungrouped**. - **The New Tab menu names three shells, not every one the machine has.** A stock macOS box reports nine, so the `+` menu opened on a column of `csh`, `tcsh` and `ksh` that almost nobody runs, above the SSH hosts people came for. The Local section now lists the default shell, always first, and then only shells that have actually been opened, by frecency — three rows at most, the way the SSH section already caps its hosts. The rest sit behind an "Other Shells…" row that opens the command palette filtered to them: every shell is now a palette command, "Shell: {name}", and running one does what its menu row does, splitting instead when ⌥ (Alt) is held. The row is left out when the menu already names the whole list. - **Saved SSH hosts live in `servers.json`, beside `config.json`** (#911), so `config.json` can be synced between machines for its colours and keys without carrying a list of servers. `ssh_profiles` and `ssh_profile_frecency` move across by themselves the first time a new build reads an older `config.json`: `servers.json` is written first (mode `0600`), and only once it has landed are the two keys taken out of `config.json` — nothing else in that file is touched, including keys this build does not know. When both files hold hosts, `servers.json` wins and the stale copy in `config.json` is dropped at its next save. A `servers.json` that cannot be parsed is kept aside as `servers.json.corrupt` and saving is refused until it is repaired, the same rule `config.json` has. Hand edits to `servers.json` hot-reload. Passwords and passphrases stay in the OS keychain. - **`tty7 exec` runs a command in a pane that already exists and hands back its result** (#839). `run` makes a new pane and `send` does not wait, so every script that wanted a command's exit code from a shell it already had ended up as `send`, a sleep, a `capture` and a guess, or an `echo $? > /tmp/rc` side channel. `tty7 exec %3 -- cargo test` types the line at the pane's prompt, follows the shell integration's marks to the command's end, prints what it printed — as text, the way `capture --plain` reads it, or escapes intact with `--raw` — and exits with its exit code. `--timeout` gives up with 124 and leaves the command running; a pane with no prompt marks, or one that is not at a prompt, is refused before anything is typed instead of waited on. - **`tty7 send` takes its text from stdin or a file, and can paste it** (#838). A token passed as `send %1 "$TOKEN"` sits in the CLI's command line, where any local user reads it in `ps`, and in the caller's shell history; `--stdin` and `--from-file` send the bytes exactly as read and never echo them in `--json`. `--paste` sends the text the way a paste into the window does — in bracketed paste when the pane has switched it on, so a multi-line text arrives as text instead of running line by line, with any ESC stripped so the text cannot close the paste itself. A pane without the mode gets the same unframed paste the GUI would send it, and `--json` says which one went. The server now reports each pane's bracketed-paste mode in `tty7 procs`. - **The source control panel's changed files can be filtered and shown as a tree** (#473). A filter field sits above the list: every word typed has to appear somewhere in a file's path, in any order and any case, and the group headers' counts and their stage/unstage/discard-all buttons follow what the filter leaves on screen. The tile at its end switches the list to a directory tree — directories before files, a chain of directories that hold nothing but the next one compacted into a single row (`crates/core/src`), each directory row foldable and counting the files beneath it. A filter opens every folded directory while it is set. The choice between list and tree is remembered in the config. Both are built from the status the host already sends, so a remote repository gets them unchanged. Searching the diff text itself is not part of this. - **The cursor at the shell prompt can have its own shape** (#958). A new `prompt_cursor_style` setting (Settings → Appearance → Cursor) takes `follow`, `block`, `bar` or `underline`. `follow`, the default, keeps `cursor_style` everywhere, exactly as before. Any other value is used at the prompt, drawn by tty7's inline editor or by the shell's own line editor, and leaves `cursor_style` to the programs the shell runs — so `bar` with `cursor_style: "block"` gives kitty and ghostty's bar-at-the-prompt, block in a TUI that never sets a shape itself, such as Claude Code. A shell prompt in vi mode keeps its own insert/normal shapes. - **Agent status catches interrupts, stale turns and permission prompts** (#1003). Claude Code's `Stop` hook skips user interrupts, so after Esc or Ctrl+C a pane stayed on working until the next prompt. Now an Esc or Ctrl+C on a pane whose agent is mid-turn, with no hook event in the second after it, ends the turn; and a turn that has been working for 30 minutes without a hook event goes idle. These guessed states raise no notification or unread badge, and the agent's next real event overrides them. Claude Code and Codex now report a permission prompt or a question the moment it opens rather than seconds later (Codex never did), and Codex leaves waiting once the approved tool has run. The "finished" notification waits 1.5 seconds and is dropped if the next turn starts first. Existing hook installs are refreshed at launch. - **Settings open in a window of their own, rebuilt.** Settings used to cover the workspace, hiding the terminal a setting was being tried on. It is a separate window now; opening it again raises it, and closing it with unsaved forms or a theme draft still asks first. The page is rebuilt with a searchable sidebar, per-page modified counts and a *Modified only* switch, and an inline Reset on each changed row. Theme menus have a live preview, search and swatches, and font menus draw each family in itself. Recording a shortcut that another action already has asks Replace or Cancel instead of taking it over silently. SSH hosts are one column of recent hosts, with *Show all* by source and a search, and open in place. Integrations gets an agent search and a menu per agent (Reinstall, Reveal hook file, Uninstall). General gains startup and restore; updates and the server move to About. (#973) - **The window is redesigned.** The side rails share the window's fill, divided by a hairline; the current tab is a neutral selected step with a semibold title instead of a blue wash, and tab titles are spelled out in full wherever they fit. The right panel's tabs are words — Info, Files, Changes, GitHub. The workspace switcher, Search Everywhere, the Changes tab, the diff overlay, dialogs, menus, the SFTP browser and the home page follow the same design: neutral selection instead of accent washes (the switcher's hard-coded blue row is gone), an inverted neutral primary button, lighter scrims and headings no longer in capitals. The default Light theme takes a new ink and accent. (#973, #993) - **Search sits in the middle of the title bar** (#990). The centred path repeated what the sidebar already says; it is now a field-shaped **Search…** button, with its shortcut, that opens Search Everywhere — centred over the terminal column on Windows and Linux. The trailing `…` menu, which held only Search Everywhere and Settings, is gone: both keep their shortcuts and the macOS menu bar entries. - **Every confirmation is tty7's own dialog, and failures say what failed** (#985). Closing a busy tab fell through to a system alert (NSAlert on macOS, TaskDialog on Windows) while every other question was one of tty7's cards; all confirmations are that card now, on every platform, with Return and Escape keeping their meaning and a destructive answer painted red. About twenty failure notices were one plain line such as `push: fatal: …`; they are error toasts now, titled with what failed ("git push failed") and the reason beneath. The delete confirmation says the file is removed for good rather than moved to the Trash. - **The code editor's header, menus and find bar match the rest of the window** (#1005, #992). The file strip keeps only the files most recently brought forward (three docked, seven filled, fewer with side panels open) in a stable order; a `+N` button lists every open file with a filter, keyboard navigation, *Close saved* and *Close others*. A Dock / Fill switch replaces the header's right-click menu, and a filled editor leads with a pill naming the terminal it covers that docks it back. The text's right-click menu, which was the OS's own menu, is drawn like tty7's other menus and offers Attach to Agent (the selected lines ride along as `#L3-9`), Find, Go to Line and the file's own items; file tabs get Close, Close Others and Close to the Right. The find bar is one filled pill with the match count and a clear current-match colour, and ⌘⌥F / ⌘R (Ctrl+H elsewhere) opens it with the replace row. ### Removed - **The Window & Tabs settings page.** Its three tab settings — New tab position, Tab bar position and Auto grouping — are a **Tabs** group on **Settings → General** now, below Startup & restore. - **The *Open diff preview from sidebar counts* setting** (`sidebar_diff_preview`). A tab's `+N −M` in the sidebar, and the Info panel's `changes` row, always open the diff overlay; the stalls on large working trees that the switch was a way around were fixed alongside it. The key is ignored if a `config.json` still has it. - **The *By repo or folder* grouping mode, "Group Automatically", and groups stored by name.** A folder you want grouped is pinned instead, and dragging a tab below the divider is the way back to automatic grouping. Hand-made groups and folds from earlier versions are not carried over. The control dialect moves to v11, so each remote host needs one Update Server, which ends the sessions on it. - **Editing a saved host's authentication, proxy, port forwarding and advanced options in Settings.** The rebuilt SSH page edits Alias, HostName, User, Port, IdentityFile and ProxyJump; values already saved for the rest are kept and still used. (#973) ### Fixed - **Quick launch found only the agents in the GUI's own bare `PATH`.** The app starts with the launchd environment (`/usr/bin:/bin:/usr/sbin:/sbin`) and fills it in by running the login shell, but only as a *login* shell — which reads `.zprofile` / `.bash_profile` and never `.zshrc` / `.bashrc`, where most people export their `PATH`. Anything installed under `$HOME/.../env/node/bin`, `~/.local/bin` or an `rc`-only directory was invisible: `Agent:` rows for it disappeared from Search Everywhere and the `NEW Agent Tab` menu, and a matching `agent_launch` entry did not help, because the override's own program is looked up on the same `PATH`. The probe now runs interactively (`-i -l`, fish unchanged — it reads its config in every mode), reads the PATH between markers so an `rc` that prints cannot be mistaken for it, and gives up after 5 seconds so a stuck `rc` cannot keep the app from opening. - **Chinese UI copy reads like Chinese** (#980). Half-width `?` and `,` in the hard-reset confirmation, the passphrase, Finder and server naming used two words each, and three quote/dash styles were mixed; these are now one each, and the most literal translations (file errors, update and version-mismatch dialogs, SSH prompts) are rewritten. The fork error that pointed to "Settings → Agents" — a page that is called Integrations — now names it correctly in every language and in the `tty7` CLI. - **Return runs the top row after a search that found nothing.** Backspacing from a query with no results to one with some — or opening the search already filtered, as the New Tab menu's *Other Shells…* row does — left no row selected, so Return did nothing until an arrow key was pressed. - **A typed `ssh -p 2222 me@box` is no longer offered as an address.** The address parser read everything before the `@` as the user name and offered to connect as `ssh -p 2222 me`; a line with spaces is now always taken as an `ssh` command line. - **The character under the cursor no longer disappears in vim and Neovim** (#966). An input-method composition with nothing visible in it — Windows IMEs can leave one behind — was still painted at the cursor, as a cell of the theme's background with an underline under it, covering both the character and the block cursor on every cell the cursor moved to. A composition that has nothing to draw is no longer painted, so the cell keeps its character and the block cursor draws it in reverse video as usual. - **Nerd Font icons from a fallback font come out at the text's size** (#866). With a Nerd Font icon face such as Symbols Nerd Font Mono behind a primary that lacks the icons, an icon followed by a space on the same background — every icon in a coloured Powerline or p10k segment — was held to one cell and shrunk to about two thirds of the text's height. Icons with a plain space after them were already drawn full size, so one prompt had icons in two sizes. A lone Private Use Area glyph supplied by a fallback face is now fitted, aspect ratio kept, to its cells and one em of height, taking the blank after it when that blank paints no background or the icon's own and when doing so makes it bigger, and centred in the row; a face whose icons ink less than a cell grows (at most 2×). The Powerline separators, CJK, emoji, other text and the primary font's own icons are drawn as before. Not on Linux, where the text system reports a glyph's advance box rather than its ink. - **A host with `Compression yes` prints again** (reported by @RedwindA in #997). It negotiated `zlib@openssh.com`, and the SSH library tty7 used cut every packet that inflated past twice its compressed size, so the zlib stream desynced right after authentication: the session showed as connected and never printed a byte. The library is now pinned to a build with the upstream fixes. Re-importing an ssh config now also updates the algorithms of hosts that already exist. - **The OpenCode plugin loads on OpenCode 2.x** (reported by @ArnoChenFx in #999). 2.x only loads a plugin's default export shaped `{ id, setup }` and rejected tty7's with a load error. The generated plugin now serves 1.x and 2.x, follows 2.x's event API, and starts its helper without `sh`, which Windows does not have. - **The code editor no longer loses edits** (#984). A save truncated the file in place, so a crash or a full disk mid-save destroyed it; local saves now go through a temporary file renamed over the target, falling back to an in-place write only where a rename would change something visible. The same file open in two tabs is one buffer, not two diverging copies. Closing a tab, its last pane or the window, or quitting, asks about unsaved files (Save / Cancel / Discard) instead of dropping them. Renaming or deleting a file in the Files tree follows the open buffer, so a save no longer recreates the old path, and a save asks before overwriting a change made elsewhere. Undoing back to the saved text clears the unsaved mark, and a reload keeps undo. - **Typing Chinese through an input method no longer crashes the code editor** (#993). When a re-parse ran past its time budget, stale highlighting could put a style boundary inside the committed character and panic the app. - **Selecting text in rendered Markdown no longer blanks it out** (#991). The selection was painted over the glyphs, so a multi-line selection in, say, a pull request's description became a solid grey block. - **The Processes and Ports sections see the pane's shell on a busy Mac** (reported by @zcyc in #731). The macOS process list read the pid count as a byte count, so on a machine with about 700 processes only the newest 60 or so were seen; a pane whose shell started earlier showed no processes and no ports, with nothing flagged. - **A Files tree on a WSL distro stays in step with it** (reported by @zt449569708 in #942). Copying a downloaded file onto the distro's share no longer brings a `name:Zone.Identifier` file with it; a file removed from the shell inside the distro leaves the tree (the share accepts a change watch and never reports a change, so it is now polled every 2 seconds); and right-clicking a row selects it. - **The quit confirmation on Linux is no longer cut off mid-sentence** (reported by @XuJinNet in #920). Linux has no native dialog, and the fallback set the message and detail as single unbreakable lines in a fixed-width box. Its text wraps now. - **Fork no longer picks up the previous agent's session id** (by @CodeHourra in #957). When the foreground agent in a pane changed, or another agent's hook wrote to the same terminal, the old session id stayed with the new agent. - **Windows no longer freeze for a round trip when a remote watch closes.** Closing a watch on a remote workspace waited for the server's answer, often on the UI thread, so every window stalled for a round trip — up to five seconds on a quiet link. The request is now sent without waiting. - **A burst of pane output no longer holds up the UI.** The reader parsed each batch, and a reconnect's whole replay of up to 8 MB, in one hold of the grid lock that every UI handler touching the grid waits on. It now lets go every 64 KB. - **A command named in the search leads over a session that mentions it** (#985). Typing `worktree` and Return resumed a past agent session instead of opening New Worktree Tab: the fuzzy matcher spent the first letter on its leftmost occurrence and scored a title containing the whole word as a poor match, and session titles often open with a command's name. The matcher now keeps the best alignment, and on the All tab sessions yield to an equally good command. - **Menu items bound to Return or Tab show the right shortcut on macOS.** A menu item bound to ⌘↵, such as Toggle Fullscreen, read as ⌘E. (#973) ## [26.9.3] - 2026-09-23 ### Added - **A tty7 server can be updated from the command palette, here or on the remote host** (#931). Between dialect bumps a remote host kept whatever binary already sat at `tty7-server-c{control}p{protocol}` — `replace` re-uploaded only on a dialect mismatch — so a daemon-side fix never reached it, and "Update Server" restarted the same old file. "Update tty7 server on "{machine}"…" is listed when the workspace is on an SSH or WSL host whose server tty7 installs (not a `--stdio` program) and forces the upload: the new binary lands under a temporary name, must answer our dialect, and only then is renamed over the published path and the daemon cycled. An upload that fails that probe leaves the old file and the running server alone. The confirmation says every session on that machine ends. "Update tty7 server on this computer…" restarts the local daemon onto the build the app already ships, through the same handoff-or-restart choice, and says "already running this build" instead when the last probe reported our protocol and build. The build string is the crate version, so two nightlies of one version look identical. The remote update goes through the local daemon's router, so a local daemon older than this asks for itself to be updated first rather than dropping the request silently. - **The SSH host editor takes a password, a key file and its passphrase** (#875). A host could be described in full in Settings and still not be connectable from there: there was no password box on the form, so storing one meant connecting and ticking "remember", and correcting one meant failing first. The key file sat two disclosure triangles deep under Advanced as a textarea of paths. An Authentication block now carries a masked password with a reveal toggle, seeded from the keychain so a stored one can be read back, corrected or cleared; identity files with a Browse… button that writes the path back as `~/.ssh/...`; and a key passphrase stored against the key's contents, the account the connect-time prompt already uses. Which boxes appear follows the method, on the same split the handshake uses — a password for Auto and Password, passphrases for Auto and Key, nothing for Agent, GSSAPI or 2FA — since a box outside it would store a secret nobody is ever offered. Nothing secret reaches the config file, which is why Save could not see a typed password; the dirty check folds the secrets in now. A password is filed under the endpoint, so saving a new address moves it and leaves nothing behind unless another host still dials the old one. Test dials with what is on screen rather than only what is stored. Labels now sit against their fields rather than across the page from them. - **CodeBuddy CLI support, and Cursor CLI reports its session and status** (#936, requested by @lgc653 in #892 and by @rrpolanco in #741). CodeBuddy gets detection through all three of its binaries (`codebuddy`, `codebuddy-code`, `cbc` — the last shared with the COIN-OR solver), hooks in `~/.codebuddy/settings.json` or `$CODEBUDDY_CONFIG_DIR`, status, resume and fork. It emits `SessionStart` with `source: "compact"` in the middle of a turn, which is filtered out so the pane does not fall back to idle. Resume and fork drop the stale session and worktree flags from the replayed launch, and `--no-session-persistence` disables both. `cursor-agent` gets hooks in `~/.cursor/hooks.json`. Cursor ignores a hooks file without `"version": 1`, so tty7 writes it when it creates the file or finds the key missing, and never overwrites a version the user set; a versionless file holding our hooks reports Outdated so a refresh repairs it. Its payloads call the session `conversation_id` and carry `workspace_roots` instead of `cwd`, and both are read. A turn opens on `beforeSubmitPrompt` and closes on `stop`, with `postToolUse` counted as activity; mapping `postToolUse` to the turn's start instead froze the activity counter, shortened turn timing and hid turns that ran no tools. `cursor-agent -p` does not send `beforeSubmitPrompt` or `stop`, so a print-mode run records its session id but shows no status dot. - **The Info panel says how far away a remote pane's shell is** (#862). The Session table named the machine but never the distance to it, so a remote workspace gone slow looked exactly like one that had not. A `latency` row now shows the round trip of a `Ping` on tty7's control link. Only `Ping` is timed: every other request does work on the far side, and timing a large `ReadFile` would read as a network gone seconds slow. The row takes a ping of its own every two seconds while the panel is open, rather than the keepalive's, which only fires on an idle link. It is held per host, so moving between two panes of one machine keeps the number; a dropped link keeps its last measurement, since that is when someone is reading the row; and before the first ping returns it reads `—` rather than `0 ms`. The measurement is client-side, so it works against every server that can connect. - **A file in a remote workspace's tree can be downloaded from its context menu** (#937, requested by @jerryokk in #723). The tree took uploads by drag and drop but had no way to bring a file back. Download sits where a local tree has Reveal in Finder and saves straight into `~/Downloads`, as the SSH pane's SFTP panel already does, numbering a second copy `name (2).ext` rather than overwriting the first. A file over the ~63 MB single-frame cap uploads share is refused before anything crosses the wire, with a pointer to scp or rsync. Files only; a folder would need a recursive walk or an archive. - **Windows portable mode keeps its data beside the executable** (#941, requested by @netcatty in #852). If the folder holding `tty7-app.exe` contains both the `.tty7-portable` marker and a `data\` folder, `data\` is the config directory — settings, themes, sessions, scrollback, history, and the daemon's socket and lock — and the `tty7.exe` beside it resolves the same directory from outside tty7. The marker alone is not enough because every portable ZIP has shipped it since #330 as the updater's cue: switching on it would land every existing ZIP user on an empty directory with their settings still in `%APPDATA%\tty7`. Creating `data\` is the opt-in. `--config-dir` and `$TTY7_CONFIG_DIR` still win over it, a portable copy is never handed the installed tty7's legacy tree, and an in-place update leaves `data\` alone. - **Next Tab and Previous Tab step to the neighbouring tab without the switcher** (#934, requested by @rrpolanco in #867). `NextTab` (Ctrl+Tab) goes through the recent-tab switcher, whose quick tap commits to the last-used tab, so repeated taps bounced between two tabs and never reached a third. New `SelectNextTab` / `SelectPrevTab` step along the order the strip or grouped sidebar shows, wrapping around, with no popup. They take `⌘⇧]` / `⌘⇧[` on macOS and Ctrl+PgDn / Ctrl+PgUp elsewhere, where `Ctrl+Shift+]` / `[` already move between panes — which means those two chords no longer reach the PTY, and vim's `:tabnext` binding with them; unbinding the two actions gives them back. The tmux preset's `prefix n` / `prefix p` move to the new actions, since the switcher never committed there with no modifier held. `NextTab` / `PrevTab` keep their config names and are now labelled "Recent Tab Switcher"; the palette's Next/Previous Tab entries, which already cycled, now follow the shown order rather than the raw index. - **Stroke thickening on macOS can be turned off** (#939, requested by @xiaozhaodong in #720). `font_thicken` (default on, so nothing changes unasked) under Appearance → Terminal text → Thicken strokes stops CoreGraphics smoothing from dilating glyph strokes, so text renders at the face's own weight whatever its colour, like Ghostty's `font-thicken` and iTerm2's thin strokes. It sets `AppleFontSmoothing = 0` in the process's volatile argument domain before gpui first reads it — nothing is written to disk and no other app is touched, and with the setting on a hand-written `defaults write ... AppleFontSmoothing` keeps working. gpui caches the value once, so a change applies after a restart. - **The code panel's Markdown preview and word wrap are actions** (#935, requested by @swaynehales in #754). The two status-bar toggles were mouse-only. `ToggleDocumentPreview` and `ToggleDocumentWrap` are in the palette and bindable, with no default key, and the buttons call the same code. The last state is remembered and applied to the next file opened; preview only applies to Markdown, and a file opened at a line target, such as a clicked `README.md:42`, opens as source so the cursor can be seen. - **An orphaned pane can be put back on screen with `tab new --pane %N`** (#831, reported by @xAlisher in #716). The only verbs for an orphan were the two that kill it. The tab is built from the live pane registry rather than the tree, because closing a tab drops the pane from the tree at the moment it orphans it; the ssh spec, agent and shell are therefore not recovered. A pane the server is not running, or one a tab already holds, is refused. With no workspace named, the pane goes back to its recorded owner. `pane ls --all` now names this way back before the two ways to kill. - **`tty7 capture --tail N` keeps the last N lines of the answer** (#855, reported by @rrpolanco in #841). It composes with `--scrollback` and `--plain`. The daemon still replays the whole ring, so it saves the pipe, not the wire. - **Qoder CLI support** (by @WhiteDG in #873). - **A Pi pane reports that it is waiting for you** (by @netcatty in #878). The bridge now maps Pi's `ui_prompt_start` to a permission request or question and `ui_prompt_end` back to working, each guarded so an Oh My Pi fork without the hook loses only that event. - **Crush support** (by @akhenakh in #881). Crush only offers a pre hook. ### Changed - **Settings is organised around what is being configured, and search results can be edited** (#915). The groups are now General, Appearance, Terminal, Keyboard & Mouse, Window & Tabs, SSH, Integrations and About; About keeps app information and update status, and update preferences, proxy and tty7 server controls move to General. Search matches configuration keys and localized aliases, edits an ordinary setting in place, filters to modified settings, resets a setting on its own, and is navigable from the keyboard; shortcuts and larger forms keep their own editors. Ordinary settings save as they change and show a failed write with a retry. Theme drafts and SSH forms get an explicit Save and Cancel, and leaving unsaved work asks Save, Discard or Keep Editing. Terminology is aligned across English, Chinese and Japanese. - **The native chrome has one colour and icon system, and agent marks carry their brand colour** (#914, #916). Primary actions, navigation selection, keyboard selection and status colours are separate roles, and UI status no longer borrows the terminal's ANSI palette. Toolbar glyphs share one set of sizes, and file names, paths, labels and selected rows are told apart. The default Dark palette is charcoal with a restrained blue accent, and the default left sidebar is 260px wide; a saved width is kept. The first pass turned every agent in the rail into the same grey disc, which took away the quickest way to tell one agent from another down a column. The mark itself is now painted in its brand colour with no disc, at about a third of the coloured area, so it no longer outweighs the status dot beside it; a colour that cannot be seen on the surface under it — Codex's and Grok's black on a dark theme — is lifted, keeping its hue. Resting toolbar tiles drop to the caption ink, so `+` and the panel toggle are no longer the darkest marks in the sidebar. - **A keybinding in config adds a chord beside the default instead of replacing it** (#887, reported by @rrpolanco in #868). `"NextTab": "cmd-shift-]"` silently removed Ctrl+Tab: the keymap held one chord per action and a configured one overwrote the slot. A string now adds; `""` still unbinds; a list such as `["a", "b"]` is the exact set, replacing the default, and `[]` unbinds. Every existing file still loads, and a save writes back the shape it read. Configured chords are installed after every shipped one, because gpui settles two bindings on one chord in favour of the one added last, which let a user's `"NewTab": "ctrl-tab"` lose to `NextTab`. Settings → Keybindings shows every chord an action has; recording a shortcut sets it and writes the list form, and a chord taken from another action removes only that chord from it rather than emptying the action. - **Workspaces are numbered in a stable order** (#938, requested by @Morphone0429 in #760). `SelectWorkspace1`–`9` and the Window menu numbered workspaces most recent first, so switching to one moved it to slot 1 and a number never meant the same workspace twice. The number now follows the order this client first had each workspace. A synced remote workspace this client has never opened takes no number — otherwise connecting to a busy devbox would take the next free slots — and on first open it takes the next free one without shifting the others. The Window menu lists the first nine in that order, open and closed together; a tenth and later, and never-opened synced ones, are reached from the switcher. The switcher stays most recent first, because the Ctrl+Tab hold-and-release depends on it, and shows each row's number. Deleting a workspace moves the ones after it up a number. - **Panel text stays readable across themes and sizes** (#933). Compact section headings take one size on both sidebars, Session labels take the body size, and muted text is checked against the surface it actually sits on — content, sidebar or popover. A Git file name shrinks with an ellipsis and shows its full path on hover, the branch name keeps room in a narrow panel, and rows and settings navigation grow with the interface font. Static process IDs and separators are quieter, and settings description rows no longer look clickable. Floating surfaces share one radius and one selection treatment. - **An SSH connection proves its remote server once, not once per pane** (#824, reported by @qiudaomao in #695). Every new pane re-ran the full installer probe on a connection that was already up and serving — `uname`, a control probe that spawns the server binary, a walk of `/proc` for the running build, and two SFTP reads, all serial and all before the pane's own channel opened. The answer is now kept on the connection, so a reconnect starts over by construction. It is dropped before a replace or restart, and when a routed link closes without the remote sending a byte. A failed probe is not remembered, and a build mismatch the probe found is raised again for every pane rather than only the first. Round trips per later pane go from five to none; the wall-clock gain on a real link has not been timed. - **The Windows binaries no longer need the Visual C++ redistributable** (#907, reported by @ra9fael in #902). A default MSVC build links `VCRUNTIME140.dll`, which is not part of Windows, so on a machine that had never installed the redistributable tty7 failed before `main`. Every machine that builds tty7 has it, so nobody positioned to notice could. The CRT is now linked statically, set in the repository's cargo config so CI builds under the same flag a release does. A check that reads the PE import tables directly, without `dumpbin`, runs in CI and over the packaged ZIP and installer payload. - **The control dialect is v10, so remote hosts pick up the daemon fixes** (#930). No message changed. A remote server's filename carries the dialect number and tty7 keeps whatever binary already sits at it, so without a new number the daemon-side fixes in this release would never reach a remote host. Each remote host shows the Update Server prompt once, and accepting it ends every session there. Locally, the first launch after the update shows the daemon mismatch prompt once; where the daemon supports handoff, restarting keeps the shells. - **The SFTP Files panel opens where the shell is now** (#932, requested by @loinky in #826). It started at the shell's directory only the first time; every later open went back to wherever you last browsed. A fresh open now prefers the shell's current directory, then the last folder browsed, then the login directory. Switching panes with the panel still open returns to the folder browsed there, as before. - **The prompt editor starts input on its own row when the prompt leaves too little room** (#940, requested by @imeilige in #767). A 119-column prompt in a 143-column pane left 24 columns for the first row of input. When fewer than `max(20, cols / 3)` columns remain, input starts at column 0 of the row below — but only if that at least doubles the room, so a short prompt in a narrow pane stays put. Clicks, Up/Down, the completion menu and the IME candidate window all follow the moved start. It applies only to the built-in prompt editor; with `prompt_editor: false` the shell lays out its own line. - **The Close Pane / Tab menu item is labelled Close** (#910). The palette and the Keybindings page keep the full name, where it stands alone. ### Fixed - **The ssh password prompt stops coming back after a reconnect** (#827, #924, reported by @sparklive in #820). Three defects stacked on one dropped link. Evicting a dead connection removed its slot from the cache, and that slot's mutex was the only thing serialising dials to a host: every pane on the link evicted the slot the one before it had just made, ran its own handshake and raised its own sheet, so answering one only uncovered the next. The slot now stays and only what it points at is dropped — one handshake, one prompt, and the other panes reuse the connection it made. Closing the password sheet failed only the `password` method, and on any host that also offers `keyboard-interactive` that is the same question asked again; closing it now ends the attempt. A declined remote-workspace reconnect went into backoff and dialled again within a second, forever; it now suspends the machine and the strip offers **Retry**. A password the server rejects still falls through to the next method, and closing a key-passphrase sheet still just skips that key. Separately, a routed prompt had no idea whether anyone was still waiting on it: one left over from an attempt that had timed out stayed parked or on screen, and typing into it sent the secret into a dropped channel. Such a prompt is now retired, an on-screen one taken down, and the sheet waits no longer than the daemon's handshake does (120s rather than 180s). - **A pane that comes back from a workspace switch keeps its full-screen program** (#832, reported by @Infiniverse in #711). The replay arrived whole; the client threw it away a few frames later. An attach ends with the pane's shell state, and a client told the shell is at a prompt scrubs a leftover alternate screen with `?1049l` — right for a `vim` whose `ssh` dropped, wrong for the alternate screen the replay has just rebuilt, which it swapped away to reveal the banner and the launch command underneath. The agent carried on drawing differential updates into a grid that no longer held what they were differences from. That prompt state is only as fresh as the last OSC 133 mark, and a program that sends none of its own leaves it set; the live path already refused such a mark, and the replay now asks the pty the same question and reports a prompt only when nothing but the shell owns it. - **A pane restored after a server stop no longer types mouse reports into its new shell** (#853, reported by @hhdebb in #850). A restored pane replays the killed program's raw bytes, so its `?1002h` was executed again against a shell that never asked for it, and every pointer move landed on the prompt as an SGR report. The preamble appended after the restored output now also turns off mouse reporting and its encodings, focus reporting, bracketed paste, application cursor keys and the kitty keyboard flags. It clears them unconditionally rather than folding the snapshot for what was left on: the program that set them is dead, the shell is brand new, and turning off a mode that is already off is a no-op everywhere, where a fold that misread one sequence would bring the bug back silently. - **A full-screen program keeps its terminal modes across a reconnect** (#828, reported by @shihuaidexianyu in #774). A reattach replays the pane's output ring, which drops from the front at 8 MiB, so a program that set its modes once at startup — `btop` — came back on the primary screen with no mouse reporting, and the wheel scrolled a screen that should not scroll. The daemon now folds the bytes it hands the ring into a small mode tracker and re-sends, ahead of the ring, the modes the ring itself can no longer speak for: alternate screen, mouse reporting and encoding, alternate scroll, DECCKM, focus reporting, bracketed paste. Only those, because re-sending a `?1049h` the ring still carries made the ring's own copy a no-op and swept the shell scrollback ahead of it into the alternate buffer. Cursor visibility and autowrap are deliberately not restored, and a pane adopted across a daemon restart starts with an empty fold. - **`:wq` on a remote pane lands where it was typed** (#828, reported by @shihuaidexianyu in #774). The repair that puts back a cursor ConPTY parked during a repaint was decided at compile time, so a Windows client applied it to panes on a remote `tty7-server` and to native-SSH panes, neither of which has a conhost in the way — and `wq` went two rows above the `:`. It is now decided per pane from the route it was opened on, with the remote context answering what the route cannot. - **Panes opened after tty7 restarts or updates keep Local Network access on macOS** (#917, reported by @jgrund in #909). macOS attributes Local Network, camera, microphone and similar decisions to a process's responsible process, inherited at spawn — and the pane daemon was spawned by the GUI and outlives it. After an in-place update, a crash or a force-quit, new shells answered to a GUI that no longer existed, so Homebrew `node` or `python` got `EHOSTUNREACH` on the LAN while `/usr/bin` tools, being exempt, kept working. The daemon now re-execs itself in place at startup with the responsibility disclaimed: same pid, same descriptors, but its own responsible process. It always re-execs once rather than asking who it answers to, since a process whose responsible parent has exited reports itself — exactly the orphaned state this has to repair. Because it also runs on the far side of a handoff, **Restart Server** repairs a daemon started by an older build without losing panes; shells already open keep the attribution they were born with. The call is private SPI resolved at run time, and startup carries on as before if it is missing. - **F1–F12 reach the shell** (#835, reported by @steelywing in #834). No function key was ever encoded, on any platform. They now send `xterm-256color`'s own table, modified forms included, under both the legacy and kitty encoders. Two swallows sat in front of the encoder as well: the prompt editor dropped a named key it had no binding for, and F3/Shift+F3 were eaten by Find Next/Previous with no find bar open — they now hand the key back. Under the kitty protocol F3 is `CSI 13~`, since the letter form collides with a cursor position report, and F13–F24 are sent in the protocol's private-use range; the legacy path still sends nothing above F12, because terminfo already spends `kf13` onwards on the modified F1–F8. F11 stays fullscreen, now as a pinned decision. - **A tab stops wearing the title of a program that has exited** (#908, reported by @rrpolanco in #889). An OSC 0/2 title was last-writer-wins forever, in the daemon's record and in the window, so after Claude Code, vim or lazygit exited the tab kept its last title over the pane's own directory. A title written between a command's `133;C` and `133;D` now belongs to that command and is retired by its `D`; a title set at a prompt, or one a shell writes for itself — which comes after the `D` — stands, and a pane with no shell integration behaves exactly as before. A window reattaching mid-command whose `C` has rolled out of the replay ring takes the replayed prompt state as proof a command owns the pane, so it retires those titles too. - **Unread badges only mark turns the reader has not seen** (#888, #890, reported by @rrpolanco in #870). Switching workspaces, reopening from the tray and restarting the app all build new views over live panes, and a new view saw an agent that was already `Done` as a turn finishing just now — so every agent tab outside the active one got its `1` back. The daemon now counts finished turns, and views leave a read mark per pane for the life of the app: a rebuilt view whose mark matches takes the badge back as the reader left it, while a turn that finished while it was away still badges. After a restart, with no marks, a reattach adopts the replayed status as its baseline instead of an edge; a relink keeps the view it already had, so a turn that finished while the link was down still badges. Against a remote server older than the turn count, a later turn finishing while away looks like the one already seen until that server is updated. - **A symlinked config file stays a symlink** (#918). Every save wrote a temp file and renamed it over the path, which replaces a symlink with a plain file — so a `config.json` linked into a dotfiles repo quietly stopped syncing the first time the sidebar was dragged. The link chain is resolved first, relative and dangling targets included, and the rename lands on the real file; a loop falls back to the old behaviour. It covers every file written that way, `views.json`, `window.json` and the machine stores among them. - **One unreadable keybinding no longer resets the whole config, and a shortcut can be left unbound** (#906, reported by @leoatchina in #901). A single `keybindings` value serde could not read failed the entire `config.json`, which was quarantined, the app started on defaults, and the next settings write saved those defaults over everything the user had written. That line is now logged and skipped on its own. And nothing in the app could unbind an action: ⌫ on a Keybindings row reset it to the shipped chord, a no-op on a row nobody had touched, so Alt+1…9 could not be handed back to vim. ⌫ on an unrecorded row now writes the empty list, the row shows `—` and gains the **Reset** button, and the capture hint says so. - **A tty7 retired to the tray comes back from the Dock** (by @fish2lab in #880). With its last window closed the process stays up with its Dock icon, but a click on the icon went to a reopen handler tty7 never registered. The reopen now activates a window if one exists, and otherwise restores the workspace that retired, the same way the tray's **Show tty7** does. Why a window can vanish across display sleep is not explained by this. - **Reaching for ⌘ mid-flick no longer zooms the font to its minimum** (by @wenlingang in #913). A trackpad's momentum tail carries the modifiers held when each event is delivered, so pressing ⌘ while a scroll coasted turned the rest of it into dozens of zoom steps, and the clamped size was saved. A gesture now decides scroll or zoom at its first event and keeps that answer to the end of its tail, a zoom whose modifier is released included; each new gesture decides for itself. A wheel still decides notch by notch. - **The prompt gets its cursor shape back when a program exits** (#922, reported by @rrpolanco in #837). nvim and vim restore terminfo's `Se` on the way out, which for `xterm-256color` is `\e[2 q`, an explicit steady block, so every prompt after them stayed a block whatever `cursor_style` said. The reader notes the cursor style at `133;C` and restores it at `133;D` if the command left a different one, back to following the configured default when that is what the prompt had. `D` is the first thing the precmd writes, so a vi-mode plugin still has the last word; a shell without integration keeps the old behaviour. - **Narrowing a pane under a prompt leaves one clean prompt** (#929, reported by @coolmanj in #654). With something right-aligned on the prompt line, the reflow wrapped it onto a second row and left the cursor on the tail, and the shell's SIGWINCH redraw — up by the rows it last drew, then clear below — started from there, stranding the old prompt's head; a drag in and out filled the pane with them. A column change at a prompt on the primary screen now clears the cursor's line from its first row and puts the cursor where the shell's redraw expects to start. Only the last line of a multi-line prompt is handled. - **A resize whose echo went missing is sent again** (#923, reported by @rrpolanco in #893). The grid reflows only when the daemon echoes a size back, but a size once asked for was never asked for again, so a lost resize or echo left the pane painting a grid shorter than its bounds — blank bottom rows — until a divider drag asked for a different size. Past a grace period, a grid that does not hold the requested size gets it sent again. One proven way for the two to drift, not established as the report's. - **A replay never starts in the middle of an escape sequence** (#927, reported by @404KSG in #857). The replay ring evicted at an arbitrary byte, so a cut through a `133;C` mark came back as `33;C` on every reattach and a cut through a CJK character as a replacement glyph; eviction now continues until the front is outside every sequence and on a character boundary. The OSC 5522 clipboard sniffer dropped a held `ESC ]` when another escape interrupted it, so a read boundary could turn `ESC [31m` into a literal `[31m`. Neither is proven to be the report's duplicated lines. - **A tab comes back to the pane focus was last in** (#854, #921, reported by @rrpolanco in #843 and #869). The pane to return to was sampled as the tab was switched away from, which found nothing whenever focus was off the panes — and a switch through the switcher first restored focus to the first leaf and then wrote that down. It is recorded as focus arrives now, carried across a connecting slot becoming its pane. Un-zooming hands focus to the pane that was zoomed rather than to the record, and zooming with focus off the panes zooms the pane the tab remembers instead of its first. - **Shift+Enter and Ctrl+J insert a newline in native Windows Codex** (by @doitian in #895). They sent LF through ConPTY, which reports it as Ctrl+Enter, a chord Codex ignores. A local ConPTY pane now gets explicit Ctrl+J key events — one LF for VT readers — while remote ptys keep plain LF and a negotiated kitty encoding still takes precedence. - **Leaving an ssh session no longer leaves `^U` at the prompt** (by @spragginsdesigns in #877). The typeahead record kept lines already submitted, so draining `exit` still asked for a line wipe with nothing to replay. Enter now discards the submitted record, and Ctrl-D on an empty line ends input the way Ctrl-C does; with text on the line it is an edit, and text typed during a gap still gets its wipe. - **Holding Backspace in a WSL pane holds one steady bell flash** (#884, reported by @seahoe in #874). bash rings on every key-repeat at an empty line, and each flash armed its own 150 ms timer, so an older bell's timer blanked a newer flash and the pane strobed. Only the latest bell's timer clears it now; a single bell looks as before. - **Dim text stays readable on light themes** (#886, reported by @AaronNick in #858). SGR 2 was a fixed 66% fade, which on a light background dropped Catppuccin Latte's foreground from 7.06:1 to 3.18:1 and bright black to under 3:1 on every light builtin. A dim cell on a light background is now held at 4.5:1, never darker than its undimmed ink; dark backgrounds keep the old fade byte for byte, and the legible-palette switch turns it off. The unreadable Claude Code text in the report was its dark theme's truecolor, which tty7 renders as sent — `/theme auto` there picks the light one. - **`tty7 wait --until free` answers on remote and SSH panes** (#856, reported by @rrpolanco in #840). A pane routed to a remote daemon had no local tree to walk, and a pane running `ssh` has one that never empties, so both read busy forever. The far shell's prompt marks now answer freeness there, and a remote pane whose far shell has no shell integration ends the wait with `status: unknown` and a reason instead of hanging. On a local pane a prompt mark can only turn busy into free, so `free` now means "will take input", a nested shell's prompt included. - **`tty7 capture` no longer answers a live pane with nothing after a resize** (#855, reported by @rrpolanco in #841). A resize opens an empty segment in the replay ring, and the default form returned the newest segment — that empty placeholder. Empty segments are skipped now. That is all it fixes: on Unix the segment a resize opens holds the prompt's repaint, so the default form can still return just a prompt, and anything reading a pane under the GUI should ask for `--scrollback`. `--json` reports the replay's `bytes`, and a replay that renders blank says so on stderr. - **The Ports panel says when it could not look** (#830, reported by @zcyc in #731). The process walk stopped at the 64 rows the panel draws, and it visits the newest child last, so the server started a moment ago was the one most likely to be cut; the walk now goes to 512 and only the drawn list is trimmed. A missing `lsof` (it lives in `/usr/sbin`, which a hand-written `PATH` drops), one wedged past three seconds, a Windows table that would not answer, or a tree holding another user's process each used to read as "nothing is listening". The panel and `tty7 procs` now say which, and say nothing extra when the probe worked. - **Walking into a workspace the machine already has keeps its name, and the machine tree keeps earlier copies** (#831, reported by @xAlisher in #716). A name typed for one workspace was sent as a rename to whichever workspace the window settled on, so arriving at an existing one renamed it after the arriving client; the name now carries the workspace it was typed for and is spent only on the create it rode with. `machine.json` was replaced whole on every write, so the write that lost a layout erased the only copy; up to three earlier generations are now kept beside it, at least five minutes apart, and a tree that will not load is recovered from the newest one that parses. - **The Files panel lists a WSL pane's directory** (#925, reported by @zt449569708 in #896). Its POSIX cwd was read as a Windows path, so the panel said "Could not be read" and drops into it failed. It now goes through the distro's `\\wsl$` share, and a cwd no host here can read roots nothing. - **Native SSH tabs group by their remote folder** (#926, reported by @rrpolanco in #891). Under repo-or-directory grouping they all landed in Scratch; the cwd the remote shell reports now files them under its folder. Repo grouping still keeps them in Scratch. - **A fullscreen window on Windows and Linux drops its dead window buttons** (by @hhdebb in #864). Minimize, maximize and close stayed on the title bar in fullscreen, lit up under the pointer and did nothing. They go now and the room they held comes back to the strip; the row itself stays, and entering fullscreen says how to leave. macOS is unchanged. - **An agent's spinner is taken off the tab title it writes** (by @hhdebb in #865). Braille spinner frames and Claude Code's `◐◑◒◓` and `✳` are stripped from the front of a title, only with whitespace behind them and never from a name the user typed. - **`font_fallbacks` applies to panes already open** (by @hhdebb in #879), and changing `font_family` rebuilds the chain for the new family instead of carrying the old one over, bold and italic faces included. - **The terminal is reported as the focused element to assistive clients** (by @hhdebb in #872). A screen reader had nothing to announce, and a dictation tool decided its paste had failed when it had not. - **A GitHub rate limit on the update check says when to retry** (by @ayamir in #861), for a 429 as well as a 403. - **Settings rows keep their shape.** A long Codex failure note on the Agents page lost its width cap and squeezed the label to one character per line (by @wenlingang in #898); keybinding action names in CJK wrapped every few characters over the rows below and now stay on one line (#928, reported by @XuJinNet in #919). - **A docked document takes the window's right edge on macOS.** With the detail panel closed, the panel toggle and the app menu tile stayed at the end of the terminal column's strip, stranded in the middle of the window beside the document's own header. They now give way to the docked column, and the tab strip gets their width back; ⌘J and the palette still reach both. - **The file tree sits on the right panel's rail** (#859), so its root lines up with the search field above it and a selected row is as wide as it is under Info and Source Control. - **The docs explain why a shell started by hand stops reporting its directory** (#825, reported by @hardboydu in #698), and no longer promise a process-inspection fallback on SSH or Windows panes. ## [26.9.2] - 2026-09-10 ### Added - **A path is read out of the prose glued around it.** File detection used to take the whitespace-delimited token under the cursor, peel a bracket off each end and hope, which everything a build tool writes onto a path defeated: `--file=src/main.rs`, `note:src/x.rs`, a diff's `a/`, `ls -F`'s `src@`, a tree glyph with no space behind it. It is now a short ordered ladder of readings — left cuts name the prefixes that actually occur and stack against each other, right cuts trim sentence punctuation balanced-aware so `report(1).pdf` survives, and at most eight readings are tried per token. Location parsing grows `app.ts(10,2)` and `main.rs#L10` beside `:10:2`, and a path carrying no line number of its own takes one from beside it, so `File "handlers.py", line 214` lands on the line rather than the top of the file. Hovering no longer needs the modifier: a resolved link underlines at 45% as soon as the pointer reaches it and only turns solid with a hand cursor once the modifier is down. Right-clicking a path opens a menu about that path — open, show in the file manager, copy path — and a file the built-in editor cannot read is handed to the desktop instead of refused. - **A tab can be put in a sidebar group by hand.** Groups were derived and nothing else: the sidebar read a tab's cwd and filed it under the repo it found, so tabs that belong together for a reason the cwd cannot see — a few ssh sessions, three forks of one project, the two panes an investigation is spread across — had no way to sit together. A tab now moves into a named group from its context menu, and a stated group is never recomputed, so the probe no longer drags a hand-placed tab home on the next frame. Dragging a tab onto a custom group moves it there as well; a lifted tab fades every block that cannot take it, since a repo group's membership is decided by its tabs' cwds and "put this tab in tty7" is not a request the sidebar can honour honestly. Custom headers carry an asterisk, renaming rewrites every tab in the group in one pass, and "Group Automatically" gives a tab back to the probe. - **Sidebar groups fold** (#804). A group folds shut when its header is clicked and stays shut across launches. A live search outranks the fold — a row a query matches shows whatever its group says. Folded rows register no rectangle, so a pane cannot be dropped into a group that is shut, and the header still counts every row the group has. - **A remote pane's listening ports are detected and forwarded** (#787 for Windows). A remote workspace's ports were never listed: the pane lives in the peer's registry and the query went to this machine's daemon, which has never heard of it, so the answer was an empty list — indistinguishable on screen from a pane serving nothing. The peer answers now, gated on a feature so an older server says "I cannot tell you" rather than "nothing is listening". With the ports visible the forward stops being something to think about: a port opens on a click and a new one is forwarded unasked, at the same number where that number is free here. Ports and Forwards were two sections that never mentioned each other; a row is now a port, and the forward is where that row says it comes out. - **tty7 can be the system's default terminal on macOS** (by @ayamir in #818). LaunchServices hands `ssh:`, `x-man-page:` and script opens to tty7. The URL's authority is read off the parsed URL rather than through Quick Connect's `user@host:port` reader, so a port, a path and percent escapes all survive; `x-man-page://3/printf` carries its section; and an external open that arrives while a window is still pulling its layout is parked rather than inserted, which is what used to turn a single tab into the whole workspace. - **A bindable Close Window action** (by @bytehello in #778, reported by @rrpolanco in #773). 26.9.0's tray-retire model made closing the last window the "keep the daemon, drop the UI process weight" gesture, but that path was reachable only from the OS red close button. `CloseWindow` is that action, with no default key. The decision behind a window close — detach the workspace, and on the last window retire to the tray if an icon is actually up, otherwise quit — moves out of the close handler so the button and the action share it. In the palette it sits beside Quit, because that pair is the point: both end the window in front of you and only one takes your shells with it. - **Opening a new window is a bindable action** (#793, from @jerryokk's #710). Registered globally as well as on the render root, since with the tray icon on — the default — closing the last window retires to the tray and leaves no window to dispatch it. - **The pointer can take a range of diff lines and copy it** (#794, requested by @LittleSource in #721). The selection is keyed on the row's file and id rather than a flat list index, so collapsing a file above the selection does not re-point it. - **A titleless tab is named after its working directory** (#792, requested by @rrpolanco in #740). - **The tab whose pane is zoomed says so** (#782, reported by @rrpolanco in #752). - **The host behind a connected tab can be edited from it** (#801, from @junyi-deep's #438). - **TraeCode CLI support** (by @ayamir in #807). ### Changed - **The sidebar has a text hierarchy, and colour is on state.** Every line sat at the same 4.5:1 grey — tab title, branch line, group header, search placeholder — so the only things that stood out were twelve identical agent discs and twelve copies of the same `+94 −26`, neither of which says which tab matters. Titles rise to a 7:1 floor with a cap that keeps the selected label its step above the rest; captions stay resting. The rail gets its own selection ladder, leaving the window's signed-off rung untouched. Diff counts render in a resting ink — same hue, blended toward the caption, walked back to the 4.5 floor where needed. A group whose rows all share one branch and diff says so once on its header, and rows with no status yet do not vote, so ⌘T no longer flips the group twice while the poll comes back. The workspace switcher card takes the same treatment: badges become words in the caption ink, only "taken over" keeps its warning colour, and the keyboard cursor takes the rung the ladder set aside for it instead of sitting one step under a hovered row. - **The window's buttons show only under the pointer.** The new-tab, sidebar, right-panel and app-menu tiles were on screen at all times, so a window resting at the edge of the eye carried four buttons nobody was reaching for. Each group now paints only while the pointer is over the bar it belongs to. The window mark beside them stays put — it identifies the window rather than doing anything. The tiles keep their place in the layout and only lose their paint, so revealing a group never shifts what is beside it. With the detail panel open the strip's two tiles stay drawn, because they then stand over the panel's own header, whose tab tiles are painted whenever the panel is. - **The inline controls are flat, and a tooltip's chord looks like a chord** (#803). Every field and button carried a faint lift that nothing else here has; this chrome separates surfaces with low-contrast fills and hairlines, so a control sitting a millimetre above the panel was the one place claiming depth. Panels that really do float keep their shadow. A chrome tile's shortcut now goes in the tooltip's own key-binding slot — set apart on the right, a size down, in the caption ink — instead of being pasted into the label to read as one odd sentence. Settings gets one field width at 260px rather than three picked where they were written, and the Program row's shell picker stops drawing a fill that met the field's border top and bottom and looked like a patch stuck over its right end. - **The right panel's title row keeps its tiles and underlines the current tab.** The row hid its two trailing tiles until the pointer entered it while the three tab tiles beside them were always drawn — a row that grew two buttons on hover. The current tab is said with a bar under the glyph rather than a fill, because the fill was the same grey the hover state paints, so the lit tab and the tile under the pointer read as the same thing. - **A pane's frame is never queued behind the grid lock.** One UI thread paints every pane in every window, and the thread holding the grid lock is the pane's own reader part-way through feeding a batch of output into the emulator, so waiting for it wired one pane's write speed to the frame rate of the whole window. A frame that cannot have the lock paints the one before it instead — unfair rather than queued on purpose, since a painter that queued would make the reader wait for a frame it is not going to get anyway. Each pane therefore owns its own cell buffer rather than sharing one scratch buffer, and the keyboard context and selection flag read frame-cached copies rather than locking once per caller. - **The diff overlay draws its patch as a virtualised row list** (#799). The overlay built its whole patch as a nested element tree on every frame — a card per file, a header per hunk, six elements per line — so a few hundred lines of diff rebuilt tens of thousands of elements tens of times a second and the window stalled. It is one row per line now, built only for the rows on screen, and a change to one file splices just the rows it touched rather than resetting the list and losing the scroll position. The rows below the fold are counted at the 19px both views already give a line of a patch, so the scrollbar stops reading an 800-line patch as one viewport. The cards cannot survive that flattening, so the rows take the source control panel's own measurements instead of gpui-component's default container language. - **A frame's header and payload go on the wire in one write** (#797, from @ivydt08's #713). - **A directional pane move remembers where it came from per pane, not per direction** (#781, reported by @rrpolanco in #738). The per-direction array meant a two-step walk clobbered the first step, so Left, Left, Right, Right ended in the wrong pane. ### Removed - **The Info panel's CONVERSATION outline is gone** (#703, #759). The list of an agent's turns, and the click that scrolled a pane back to where one started, are both taken out, along with the anchors the client kept for them. The OSC 777 events the hooks send still drive the tab's status dot; nothing else read the rows the outline was built on. Output batches now split for one reason, so a replayed snapshot parses in a single pass again. ### Fixed - **A line the shell is still holding is submitted as itself** (#800, from @junyi-deep's #433). The held seed is adopted at the editor's own doors rather than at submit time, so a recalled history entry, a ctrl-U, a ghost suggestion or a completion is no longer glued to the front of the gap text. It also closes a paste-provenance hole: releasing a hold pushed its contents into the typeahead record as plain text, dropping the paste mark, so a paste made during a gap that outlived the hold window came back looking typed and was submitted raw through the shell's binding table. - **A plain single line is submitted as typed, not as a paste** (#790, reported by @failable in #660). - **A pane notices it came home from ssh without waiting for output.** The probe that clears a pane's remote context only runs when the reader has bytes in hand, and the prompt a shell draws after a command is the last output a pane produces until the user types again — so an `ssh` that exited inside the poll interval left the pane reporting itself as remote indefinitely. Most visibly ↑ read the remote history list, which for a host with no history of its own is empty, so ↑ appeared dead until some unrelated output arrived. A prompt mark that survives the foreground suppression is the shell saying the command is over, so the probe runs right then. Switching history scopes also dropped the list it was leaving; each scope's list is parked now, capped at four. - **macOS notifications stop polling Notification Center from the UI thread.** The crate behind them noticed a click by parking the sending thread and adding, per outstanding notification, a repeating half-second timer on the main run loop that made a synchronous XPC round trip. A banner nobody clicks stays in Notification Center, so its timer never went away: sampled with nine outstanding, a fifth of the UI thread was inside that XPC and every window juddered. The click now arrives through a delegate of our own and nothing runs on the main thread until the user clicks. The identifier carries the pid too, so a banner left over from a previous run reveals nothing. - **A ligated run stands over its own cells** (#785, reported by @rrpolanco in #751), and every ligature feature is named off rather than just `calt` (#788). - **A solo glyph stays inside its cell when the next one is taken** (#783). - **A shift-punctuation chord folds into the key the platform reports** (#784, reported by @rrpolanco in #750). Only half fixed: the US glyph table means secondary-shift-`]`/`[` stay unpressable on German, French and Nordic layouts. The control-code guard runs over the folded spelling too, so ctrl-shift-2 no longer installs ctrl-@ beside it. - **A machine whose profile is gone is named, not spelled as a UUID** (#786, reported by @shihuaidexianyu in #485). - **A repository is keyed by one spelling of its root** (#796). - **A pane's paths are read in its own host's spelling** (#795). - **The pointer can finish a Ctrl+Tab gesture the keyboard started.** Letting go of Ctrl over the workspace list slammed the panel shut and picked a tab, so switching workspaces by hand — the thing the pointer was on its way to do — was unreachable. A release with the pointer on the card but off the tab column now drops the hold and leaves the panel up; over the tab column it still commits. Two macOS consequences of holding Ctrl go with it: the search box no longer answers a mid-gesture click with Cut/Copy/Paste, and a tab row picked with the mouse arrives on the right button. - **A dismissing click is spent on the dismissal.** The switcher's scrim covers the whole window, the tile that opens it included, and its mouse-down closed the switcher and then carried on down to whatever sat beneath — for that tile, straight back into the toggle that reopened it, so clicking it a second time looked like it did nothing. - **The workspace tile answers a hover.** Its only hover state was a fill the palette derives one step off the surface, which on the rail is barely a change at all, and the name, the monogram and the chevron each pinned their own ink, so the button's hover never reached them. The text steps up to full strength now, the way a group header's does. - **An untouched rename box no longer names the tab** (by @hhdebb in #849, reported in #848). The box opens holding the tab's label as rendered, so it is never empty, and it commits on blur as readily as on Enter — so opening it and clicking away stored that label as the tab's name, which stops following the pane. The box is read against what it was seeded with; an emptied box still clears the name, which is the only way to give a tab back to its pane. - **An agent's status dot sits outside its disc** (by @hhdebb in #846, reported in #845). The dot places itself with negative offsets so it overhangs the avatar's edge, but it was a child of the element carrying the radius, so everything past the circle was clipped along the arc and the badge came back as a crescent. - **A long branch no longer eats a group's name.** A header handed its overflow to the name and the branch by flex shrink, which splits it in proportion to what each asked for — so the longer string took the smaller cut and a heading came out as `DEL…` beside thirty characters of branch. The branch takes what it wants up to half the line, the name keeps the rest above a floor, and each is elided into its own share. A group of one row lifts its branch onto the header too (#836), and outside a repo a row's working directory rides on the title's line rather than growing a second one (#851). - **A folded sidebar group hides its active row too** (#806). A fold left the active tab's row on screen, so folding the group you are working in drew a shut chevron with one row hanging under it and a header counting rows that were not there. - **A custom group stays out of the workspace subject path.** The window titles itself after the last component of the group most of its tabs are in, and a custom group is a name rather than a path, so a hand-grouped workspace would have been titled `custom:work` and one grouped as `work/urgent` chopped to `urgent`. - **The agent disc is painted solid again.** The resting tint from the hierarchy work read as a disabled tab rather than a quieter one: a column of 16% discs looked like a list of agents that had been switched off, and the brand hue is how the eye tells a Claude row from a Codex row before it reads either title. - **An agent's mark has one colour, not one per draw site.** The tab strip and the tray icon each decided it on their own, so TraeCode came out green on a tab and white in the tray. The colour lives on the agent now and both sites read it. - **The floating notices stack, and the forms have their keyboard back.** The remote input notice and the ssh status strip both placed themselves at the same spot, so a remote workspace whose ssh link had also dropped drew them on top of each other; the anchor is a column now. The managed port-forward form had no keyboard contract at all — no Return, no Escape, and it opened cold. The four sftp edit forms took the focus into a box they owned and dropped the box without handing it back, so naming a folder and pressing Escape left the caret on an element that had stopped rendering. And `Override` on the changed-host-key sheet — the one control that can accept a key that no longer matches — carried no colour at all; it greys until "yes" is typed and then goes red. - **The right panel's tab underline reaches the rule that closes the row** on Windows and Linux, where the wrapper around the tiles is only as tall as a glyph, so the bar floated a few pixels above the line. - **A seat that is coming back is waited for during a reap.** A seat is not free the same instant its holder is confirmed dead: the kernel releases the lock while tearing the process down, and a descriptor a `fork` left behind holds it a moment longer. One `EWOULDBLOCK` used to end the attempt and nothing ever revisited the file, so a dead pid stayed in it for good. - **The Ports panel names the right machine.** Its fallback said "This machine's tty7-server is too old", which reads as the local one; the server that cannot answer is the far side's. - **Turning off mouse reporting says what it costs** (#780). - **The lockfile edges #799's merge walked back are restored** (#802). The merge re-resolved `Cargo.lock` and pointed twenty consumers at older copies of dependencies already in the tree. No `version =` line moved, so the change was invisible to the usual scan of a lockfile diff. ## [26.9.1] - 2026-09-07 ### Fixed - **A remote server that will not start now says why** (#774). A remote workspace could sit in a loop nobody could get out of: every reconnect failed with "started but nothing was answering on the control socket after 15s", the strip showed a copy bar frozen at 100%, and no button was offered. A daemon whose control listener would not open logged one line and kept running — and a running daemon holds the single-server lock, so every later start stood down at once and every probe failed, forever. Whether something else is serving is now settled by connecting rather than by reading the errno, and a daemon that cannot listen exits. The reason is kept too: the remote daemon's output and exit status land beside the binary, stamped with the launch's own nonce so a restart never reads the outgoing daemon's status as the incoming one's, and a start that has already failed no longer waits out the full timeout. On the window side, an automatic reconnect retires its install progress instead of drawing an install still in flight, and a long error no longer stretches the status card past the window and takes the retry button off screen with it. - **A stale pane socket no longer stops every later daemon** (#779). A daemon that died without unlinking its Unix socket left a file `bind` refuses, so the client launched a daemon, it exited on the bind, and the client launched another — forever. The removal is now decided by the single-server seat rather than the pidfile: holding the seat means nobody else can be serving that config dir, so anything still at the endpoint belongs to a process that is gone. Where there is no seat, a socket that answers is refused rather than removed. ### Changed - **The control dialect is v9.** v8 added the project verbs; this build takes them back out and speaks v7's messages again, message for message — but the number does not go back with them. v8 is deployed, and a number that moves backwards stops being an identity: a 7 on the wire would mean either "before projects" or "after them" depending on which build put it there, and the handshake has nothing but the number to tell the two apart. ## [26.9.0] - 2026-09-04 ### Added - **Remote programs can copy images to the local clipboard over SSH.** A saved host can opt into OSC 5522 clipboard writes under **Advanced → Security → Remote clipboard images**. PNG, JPEG, GIF and WebP transfers are decoded out of band, capped at 16 MiB, validated before they reach the system clipboard, and never enter scrollback or replay after reconnecting. The permission is off by default; clipboard reads and SVG writes remain unsupported. - **Documents dock beside the terminal** (#625). Opening a file, toggling the code panel or opening a diff no longer covers the workspace: the document takes a column to the right of the terminal — half the space between the sidebar and the right panel by default — and the pane you were reading stays visible and typeable underneath none of it. Reviewing a file while an agent talks stopped being a toggle loop. Drag the divider for any width, double-click it to cycle a third, a half and two thirds, or use **Document: Third / Half / Two-Thirds Width** in the palette. Right-click the document's header for **Fill window** — the old overlay, unchanged, and per tab, so a file read over the whole window in one tab leaves the agent beside its own in the next. The terminal keeps its floor through all of it, and a window too narrow to seat both fills for that file only, without changing what any tab chose. New in `config.json`: `document_ratio`, and `document_layout` for what a fresh tab starts as. - **A tab can be dropped into another tab, as a pane of it** (#621). Drag a tab by its chip or by its sidebar row, out over the panes, and it lands where the highlight says — the same reading as dragging a pane, minus the middle, which for a tab means "split this pane the way it is longest" rather than "trade places". A tab that was itself split arrives with its panes still arranged the way you left them and takes one share of the row or column it joined. Nothing restarts on the way over: a shell mid-command, an SSH session, an agent halfway through a turn all carry on, and only the tab they were in goes away. Picking a tab up no longer switches to it, so the tab you drop into is the one you were already looking at; a plain click still switches. - **And back out again: a pane dragged onto the tab bar becomes a tab of its own** (#621). Take a pane by its grip up to the strip — or out to the sidebar, wherever the tabs are — and a caret says which two tabs it would go between. The last pane in a tab is offered nothing, being a tab of its own already. - **Give the prompt back to the shell** — a new **Settings → Input → Prompt → Prompt editor** switch (`prompt_editor` in `config.json`, on by default). Turned off, tty7 stops editing the shell prompt: every keystroke there — printable keys, arrows, IME commits, paste, ⇥ and ⌃ R — goes straight to the PTY, so zsh's ZLE, bash's readline and fish's reader own the line and the keys bound in a dotfile behave exactly as they do outside tty7, history traversal included. Previously the only way to get there was to hide the shell's own name from tty7 so integration never armed. Shell integration is untouched by the switch: prompt boundaries, working directory, exit codes, notifications and `tty7 procs` keep working. Tab completion and history search are menus tty7 opens inside that editor, so both grey out while it is off rather than sitting there doing nothing. It reaches open panes immediately, and a half-typed line is handed to the shell rather than dropped on the way over. - **A host can be proved without spending a tab on it.** **Test** in the SSH host form dials the host exactly as Connect would — proxy, jump host, host key and authentication, all on the daemon — and reports back in place: `Connected and authenticated in 640 ms`, or what the handshake stopped to ask for (a password, a key passphrase, a keyboard-interactive answer, a host key nobody has accepted yet, or one that is not the key the server gave before), or the failure verbatim. A test never rides an existing connection — one would answer for the credentials *that* connection was made with, so a password typed wrong would come back green — and it never enters the connection cache, so it leaves nothing open and holds nothing up. An edit to the form drops the answer, which was about the host as it was typed a moment ago (#438). - **The host form is reachable from wherever the machine is on screen.** Right-clicking a machine in the workspace switcher offers **Edit Host…** for a saved host, or **Save as SSH Host…** for one reached by address or by a `~/.ssh/config` alias; the switcher's **Add SSH Host…** now opens the form instead of the settings list you then had to find `+` on. A connection dialled by hand and worth keeping becomes a saved host from the palette — **SSH: Save Connection as Host…**, prefilled from the live session. The one thing that cannot come along is an ad-hoc `-J` hop, and that is said out loud rather than saved broken (#438). - **A coding agent's conversation is an outline, and a way back into it.** The Info panel grows a **CONVERSATION** section: one row per turn, the prompt's first line as its label, a dot that says whether the turn is still running. Click a row and the pane scrolls so that turn's prompt is the top line — a long agent session in a terminal has never had a way back to "what did I ask an hour ago". It rides on the OSC 777 the hooks already send for the tab's status dot, so it costs a cut only when an agent event actually arrives, and it works wherever the agent runs — over ssh, in a container, in a remote workspace — rather than only where a transcript file happens to be readable. Reattaching to a pane rebuilds the outline from its own replayed history. A turn that began on the alt screen is listed but not clickable, because there is no scrollback behind it to return to. Agents whose hooks do not report prompt text (Codex, Copilot, Grok) are left out rather than drawn as a column of anonymous dots. - **The interface font is configurable** (#761). **Settings → Appearance → Typography** now carries an interface font family beside the terminal one, so the chrome can be set apart from — or matched to — what the panes are using. - **A coding agent's conversation gets an outline, and every turn is a jump target** (#703). The right panel lists the turns it saw go by; clicking one scrolls the pane back to where that turn started. A turn under a full-screen agent has nothing to land in, and the row now says so on hover rather than drawing a link that does nothing (#759). - **A remote workspace relinks its own dead panes** (#757). Panes whose SSH route died come back on their own when the link returns, and the tabs are named after what is actually running in them instead of keeping whatever the snapshot said. - **Saved SSH hosts are one click from the New Tab button** (#647), and an SSH pane names itself after its host, reaches the host form from wherever you are, and can test a connection before you commit to it (#566). - **Nushell panes get OSC 7 cwd tracking and OSC 133 prompt marks** (#637), so the sidebar follows a Nushell pane's directory and command marks work the way they do under the other supported shells. - **The shell's own line editor owns the prompt** (#633). tty7 stops second-guessing the line being edited and hands the row to zsh, fish, bash or Nushell, which is what makes their completion, history search and multi-line editing behave the way they do outside tty7. - **A tab whose cwd is not a repository groups by its folder** (#631) instead of falling into an unsorted pile at the bottom of the sidebar. - **The wheel-zoom modifier is configurable** (#676) — for anyone whose mouse or trackpad already spends Ctrl+wheel on something else. - **`tty7 server restart` replaces the server in place, keeping every session** (#669). The old stop-and-start is still there behind `--hard` for when the process really does have to go. - **Linux updates install in app** (#306, #652). A verified AppImage release is downloaded, checked and swapped in without leaving tty7, the way macOS and Windows already worked. - **An all-users Windows install updates through a single UAC prompt** (#562) rather than one per file it has to replace. - **A file path printed by a program opens in tty7**, resolved on the host the pane is actually on (#568) — a path from an SSH pane opens the remote file, not a local one that happens to share its name. - **The workspace switcher is a flat list with a create form**, and connecting to a machine syncs its workspaces at that moment rather than whenever the next poll came round (#616). - **SFTP opens remote text files in the built-in editor** instead of handing them to whatever the OS thought should have them. - **Hooks, resume and fork are wired for the CLI agents that support them** (#666), and Kimi Code CLI is detected and driven like the rest (#694). - **Four dark themes** (#663). - **Closing the last window retires tty7 to the tray** rather than quitting it, so the shells keep running and the next launch is instant (#639). - **A `tty7-server` build is published for macOS** (#605), so a Mac can host a remote workspace and not only connect to one. - **↑ and Ctrl+P recall the last command that matches what is already typed** (#768), instead of walking history from the end regardless of the prefix. ### Changed - **The Info panel's `agent` row is gone.** It said `Claude Code · working` beside a status dot — the same name and the same dot the tab and its sidebar row were already wearing, two panels away from neither of them. The CONVERSATION section below now says what that agent is doing in a form the row never could, and the dot stays where it was learned. - **A zsh or fish you gave your own arguments to is no longer injected into.** Custom arguments have always been the line where tty7 backs off — the bash, PowerShell and WSL setups checked for them — but the zsh and fish setups did not, so a `fish` started with your flags had `-C