mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-04 00:02:04 +00:00
5974 lines
368 KiB
Markdown
5974 lines
368 KiB
Markdown
# 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:<slug>`. "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
|
||
(``), 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/<drive>`. 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/<drive>/…`;
|
||
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.** <kbd>⌘ P</kbd>
|
||
(<kbd>Ctrl ⇧ P</kbd> elsewhere) opens one search over **All**, **Terminals**,
|
||
**Sessions**, **Hosts** and **Commands**, walked with <kbd>⇥</kbd> / <kbd>⇧
|
||
⇥</kbd>, 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, <kbd>⌘ P</kbd> <kbd>⏎</kbd> 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 <kbd>⏎</kbd>
|
||
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. <kbd>⌘ E</kbd> (<kbd>Ctrl E</kbd>
|
||
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, <kbd>⇥</kbd> and <kbd>⌃ R</kbd> —
|
||
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 <script>` appended to them
|
||
and a `zsh` had its `ZDOTDIR` swapped out from under its startup files. Both
|
||
now behave like the rest. **This turns shell integration off for a config
|
||
that sets `shell` or a `custom_shells` entry with `args`** — including the
|
||
`{"program": "fish", "args": ["-l"]}` the reference page used to print as an
|
||
example — so prompt marks, working directory reporting, command-finished
|
||
notifications, inline completion, <kbd>⌃ R</kbd> and per-pane history stop in
|
||
those panes. Drop the arguments to get them back, and the notice a pane
|
||
raises when integration never engaged now names this as a cause (#624, #629).
|
||
- **An SSH pane is named after its host.** A fresh SSH tab reads the host's
|
||
name, or its address when the host has none — every host imported from
|
||
`~/.ssh/config` arrives nameless, and a window full of them all read `tty7`
|
||
until the far shell happened to title itself. The name survives a title
|
||
reset and a dropped link (`prod-web — disconnected`), and a tab titled
|
||
`deploy@10.0.0.5:2222` is shown whole rather than cut down to `2222`: the
|
||
`user@host:` head is only a head when a path follows it (#438).
|
||
- **The host form's authentication row is a dropdown**, not a six-way
|
||
segmented control — it was the row that stacked first on a narrow page. And
|
||
the three proxy fields no longer read as independent: only the first one
|
||
filled is used, so the losing fields now say `Not used: Proxy command comes
|
||
first.` instead of leaving it to be discovered by connecting (#438).
|
||
|
||
- **A workspace reads its own name from the first frame, and stops changing
|
||
under you.** Every workspace a window creates is given a generated name —
|
||
`keen-marten` — and that is the name `tty7 ws ls` prints and `tty7 ws rename
|
||
keen-marten …` addresses. The window that created it was never told: a client
|
||
is left out of the deltas its own edits raise, and both create paths threw
|
||
away the name the machine sent back, so the chip showed the directory its
|
||
shells happened to start in. The GUI and the CLI gave two different answers to
|
||
"what is this workspace called", and the real name arrived later — at the
|
||
first daemon restart, rebuild or relaunch — looking like the workspace had
|
||
renamed itself. A window now learns the name along with the layout, so the
|
||
chip agrees with `ws ls` from the start. A workspace with no name, which is
|
||
what `tty7 new` leaves behind, still reads the directory it is working in, and
|
||
a name you chose yourself still wins (#604).
|
||
- **File links open in tty7 instead of leaving it.** A ⌘/Ctrl-clicked file path
|
||
now opens in the built-in editor, on the line and column the link named, and
|
||
the Files panel selects it and scrolls it into view; a directory link opens
|
||
the panel on that directory. **Settings → Terminal → Links → Open files with**
|
||
picks between the built-in editor, the OS file association and a command of
|
||
your own — anyone who had already set `link_file_command` keeps it.
|
||
- **A pane address may now be written without its `%`** — `tty7 pane ls
|
||
--json` prints bare ids, so `83` addresses the same pane as `%83`
|
||
everywhere an address is taken. One behaviour goes with it: a lone `tty7
|
||
send 83` used to type "83" into the caller's own pane and now reports that
|
||
it has nothing to send, the same as a lone `send %83` always did. It fails
|
||
loudly and never presses anything anywhere; to type a number as text, name
|
||
the pane too (`tty7 send %5 83 --enter`) (#538).
|
||
|
||
|
||
- **The seam between panes is lighter than the outline around a menu** (#771).
|
||
One hairline colour used to serve both, which made the sidebar, the right
|
||
panel and the document column read as boxes bolted together rather than one
|
||
surface. The two are derived the same way and floored differently now: a line
|
||
that is the only thing marking an edge keeps its weight, a line running
|
||
between two panes that already carry their own fills steps back.
|
||
|
||
- **The app's copy is shorter** (#663). Settings descriptions, prompts and
|
||
notifications lost the sentences that were restating their own titles.
|
||
|
||
- **`tty7 send` takes the bare pane id that `--json` prints** (#699), so an id
|
||
read out of one command can be pasted into the next without editing it.
|
||
|
||
- **The control dialect is v6** (#605). A daemon left behind by an older build
|
||
is detected at launch and offered a restart instead of serving a window with
|
||
no tabs.
|
||
|
||
### Fixed
|
||
|
||
- **A conversation row no longer offers a jump the pane cannot make.** Clicking
|
||
one under a full-screen agent did nothing at all: no scroll, no message,
|
||
nothing. The row had an anchor, so the panel drew it as a link — pointer,
|
||
hover fill and all — while the view refused the jump, because a pane on the
|
||
alternate screen has no scrollback to land in. Switching Claude Code into
|
||
`/tui fullscreen` mid-session is enough to produce it: the turns recorded
|
||
under the classic renderer keep their anchors, and every one of them goes
|
||
quiet at once. The two conditions now live in one predicate both sides read,
|
||
and a row that goes nowhere says why on hover rather than leaving grey text
|
||
to carry a meaning grey does not have. Jumping is still off while an agent
|
||
renders full-screen — there is genuinely nothing behind it — but the panel no
|
||
longer pretends otherwise.
|
||
|
||
- **In-app updates work again on macOS** (#708). Every "Update and Relaunch"
|
||
failed with `codesign did not report a designated requirement`, on every
|
||
build and both channels, with nothing a user could do but download the app by
|
||
hand. The updater compares the signing requirement of the staged app against
|
||
the installed one, and read `codesign -d -r-`'s answer off stderr — where
|
||
codesign puts only the `Executable=` header. The requirement is on stdout, so
|
||
the check could never match. Both streams are read now, and the parse is
|
||
split from the process call so a test can hold it against codesign itself
|
||
rather than against our belief about it.
|
||
|
||
- **A tree pull that has to be retried no longer ends with the window deleting
|
||
the tabs it was pulling.** A window told to rebuild itself from the machine —
|
||
a daemon back as a new process, a restart handoff that was refused, a remote
|
||
server restarted — abandoned that job the moment it had any tabs at all, and
|
||
in every one of those cases it does: the stale ones on screen are the whole
|
||
reason it was asked. So the rebuild silently did nothing, and worse, the
|
||
window went on claiming to speak for the workspace it had never read. Opening
|
||
one tab over it then diffed into "close every tab" and deleted those panes'
|
||
records while their shells were still running, with nothing left that could
|
||
reach them. The retry is now dropped only for a tab the user really did make
|
||
while the pull was out, and a window waiting on a rebuild adds to its machine
|
||
without pruning it until the pull lands (#579).
|
||
- **Installing an update on Windows shows a progress window.** The installer
|
||
ran `/VERYSILENT`, so from the app quitting to the new build coming up —
|
||
tens of seconds, longer under an antivirus scan — the screen held nothing
|
||
at all, and "clicked update, the app vanished" read as a crash. The
|
||
installer now runs `/SILENT`: still unattended, but Inno's own progress
|
||
window stays on screen for the gap (#600).
|
||
- **Orphan panes are visible in the GUI, and closable from it.** A shell
|
||
left running after its workspace went away — what an interrupted `tty7
|
||
run` leaves behind — showed up nowhere in the GUI; only the CLI's `tty7
|
||
pane ls --all` could see it, and only `pane close --orphans` could stop
|
||
it. The workspace switcher's local machine group now lists those
|
||
background panes with their owner and working directory, each with a
|
||
Close button (#596).
|
||
- **A launch that restores one of several windows says what it left behind.**
|
||
Quitting with several windows open and starting again restored only the
|
||
most recent one; the rest were marked detached — panes alive, nothing on
|
||
screen, the only trace a log line. The restored window now shows a
|
||
notification naming how many workspaces are still running in the
|
||
background and where to reopen them (#597).
|
||
- **`tty7 pane close %99` fails when no pane 99 exists.** The orphan path
|
||
hangs the pane up directly, and that kill is fire-and-forget — the daemon
|
||
never says whether it knew the pane — so a typo'd id printed
|
||
`{"closed":[99]}` and exited 0, telling a reaper script the leak it was
|
||
chasing was gone. Close now checks the id against the running-pane
|
||
registry first and reports the miss under `failed` with exit 1 (#588).
|
||
- **cd Here and Insert Path quote for the shell the pane runs.** Both used
|
||
to wrap a path with spaces in POSIX single quotes whatever the pane's
|
||
shell was, and cmd.exe — where a single quote is an ordinary character —
|
||
then split the path at its first space. The quote style now follows the
|
||
pane's shell: double quotes for cmd.exe, single quotes for PowerShell and
|
||
every POSIX shell (#593).
|
||
- **Remote path completion says what it's doing.** Tab-completing a path
|
||
on a remote workspace used to show nothing for the whole network
|
||
round-trip — a slow link read as a broken Tab key — and a listing that
|
||
failed ended in exactly the silence an empty directory ends in. A pill
|
||
over the pane's corner now says the listing is running, and a failed
|
||
listing reports its error there instead of vanishing (#585).
|
||
- **Seven hard-coded English strings moved into the language tables.** The
|
||
shell-integration notice, the pane titles a disconnected or exited pane
|
||
wears, the loopback forward's failure, the tray tooltip that lists running
|
||
agents, the cursor-shape choices, the command palette's empty-result hint
|
||
and the updater's install hint all used to render in English whatever the
|
||
UI language was; they now follow it, and the palette's hint no longer
|
||
suggests connecting over SSH in menus that have nothing to do with hosts
|
||
(#602).
|
||
- **A half-typed tab rename survives other tabs closing and the strip
|
||
reordering.** The rename box tracked its tab by index, so any unrelated
|
||
tab event forced it closed to keep the commit from landing on the wrong
|
||
tab — and even then, a reorder mid-rename left a window where the name
|
||
went to the tab that had taken the index over. The box now tracks its tab
|
||
by tree id: only closing the renaming tab itself ends the rename, and the
|
||
commit lands on the tab the box was opened on wherever it has moved
|
||
(#598).
|
||
- **A zoomed pane stays zoomed when you leave its tab and come back.** Zoom
|
||
was a window-level value that activating any tab cleared, so looking at
|
||
another tab and returning restored the split layout — while a zoom is a
|
||
tab's temporary view state, like its focused pane. It now rides with the
|
||
tab; the clears that genuinely reshape the layout (drag, split, close)
|
||
still stand, and a zoom whose pane exited while the tab was away does not
|
||
come back (#599).
|
||
- **Opening and closing the search bar no longer erases the grid
|
||
selection.** The selection that seeds the query is the thing being
|
||
searched for, yet opening the bar ran the same unconditional clear as
|
||
*changing* the query, and closing cleared it again — select text, press
|
||
Ctrl+F then Esc, and the selection was gone. The seeded selection is now
|
||
kept through the open, and closing keeps whatever selection the grid
|
||
holds; only an actual query change retires it, the discipline the output
|
||
rescan path already stated (#584).
|
||
- **Search highlights follow the text when the pane is resized.** A match
|
||
point is an absolute (line, column) against the width it was scanned at,
|
||
so narrowing a pane reflowed the text out from under every highlight until
|
||
new output happened to trigger a rescan — and a quiet local pane has none
|
||
coming. A column change now rescans immediately, with the output path's
|
||
discipline (the selection and scroll position are left alone); a
|
||
rows-only change reflows nothing and stays cheap (#586).
|
||
- **A mistyped "Start in" path is refused at save instead of silently
|
||
rerouting every new pane.** The custom path used to be stored unchecked,
|
||
and the daemon's picker then skipped it — not a directory — and started
|
||
each new shell in its own fallback directory, so "new shells don't start
|
||
in my project" read as a tty7 bug rather than a typo. Settings now marks a
|
||
non-existent directory in red and does not save it, and a hand-edited
|
||
`config.json` holding one gets a `log::warn!` naming the path at the
|
||
moment the fallback engages (#601).
|
||
- **`tty7 doctor` exits 1 when the server is unreachable.** Doctor is the
|
||
verb people run when something is not working, so an unreachable server is
|
||
*the* finding — not a row to exit 0 over while `tty7 doctor || alert`
|
||
never fires. The full table and JSON still go out, and stderr carries the
|
||
headline under `-q` (#592).
|
||
- **The `owner` field of `pane ls --all` is documented the same way in both
|
||
references** — the bundled skill reference still claimed the CLI stamps a
|
||
literal `"tty7-cli"` owner, the behaviour that was removed because an owner
|
||
names the workspace allowed to attach. Both now describe the workspace id,
|
||
or its absence while a pane is unfiled (#591).
|
||
- **A failed `tty7 wait` now says so on stderr even under `-q`.** Timeout and
|
||
"pane exited first" are structured exits, so they bypassed the anyhow path
|
||
that prints under quiet mode and left the exit code as the only evidence —
|
||
against the documented "errors still go to stderr". Both now print a
|
||
one-line headline to stderr, the discipline `pane close` already set (#590).
|
||
- **A timed-out `tty7 wait` now answers in the same JSON shape as a finished
|
||
one** — `matched`, `stale` and the agent session fields, plus
|
||
`"timed_out": true` — instead of a bare object missing the fields a
|
||
consumer's error branch was written against. The schema, including
|
||
`timed_out`, is now documented (#589).
|
||
- **Cancelling the amend confirmation no longer switches amend off.** The
|
||
toggle was cleared when Commit was pressed — before the "rewrite the last
|
||
commit?" prompt — so answering Cancel returned to a panel whose amend mode
|
||
had silently been dropped, and the next Commit created the brand-new commit
|
||
the user had just declined to risk. The toggle now switches off only when
|
||
the commit actually runs (#595).
|
||
- **The SCM panel's "discard all" confirmation no longer overstates what it
|
||
does.** The prompt asked to "discard every change in this repository" while
|
||
the operation has always left staged changes alone — it sweeps only unstaged
|
||
edits and untracked files. The prompt now says exactly that, in all three
|
||
languages (#594).
|
||
- **A local daemon that dies and comes back no longer leaves a window of dead
|
||
panes looking live** — from the client's side a killed daemon is
|
||
indistinguishable from one whose shells all exited at once, so the window
|
||
kept showing every pane with its last title, and the reconnect then pushed
|
||
that dead layout back up as the new daemon's truth. The control handshake's
|
||
instance id is now compared on every local reconnect — the same check the
|
||
remote path already made — and a changed instance rebuilds each local
|
||
window from the machine tree instead: tabs come back, and each pane is
|
||
restored from its scrollback snapshot with the "this is a new shell" banner
|
||
rather than left frozen mid-lie. The restart-server action uses the same
|
||
rebuild path (#553).
|
||
- **A file link in a remote pane no longer opens this machine's copy.** An
|
||
absolute path was checked against the local filesystem whatever the pane was
|
||
connected to, so `/etc/nginx/nginx.conf` on a server opened the one on your
|
||
laptop, silently. Paths are now resolved on the pane's own host, and a pane
|
||
running `ssh` typed into a local shell — where neither side can answer for
|
||
them — no longer offers file links at all. A file on another machine opens
|
||
in the built-in editor whatever **Open files with** says, since a local
|
||
`open` or editor command would land back on this machine's copy.
|
||
- **Relative paths resolve from where the work is, and from the repository
|
||
around it.** Detection followed the shell's kernel cwd, which an agent
|
||
working in a git worktree never updates, and only ever tried one directory —
|
||
so a path a build tool printed from the workspace root did not resolve from a
|
||
member directory. A path that matches nothing under either now says so
|
||
instead of the click doing nothing.
|
||
- **A broken config.json can no longer be silently replaced by defaults** —
|
||
a file that failed to parse was ignored with only a log line, and the next
|
||
write of any setting — dragging the sidebar divider, zooming the font with
|
||
Ctrl+=, saving anything in Settings — serialized the in-memory defaults
|
||
over it wholesale, turning one typo into the loss of every hand edit. A
|
||
load that fails to parse now keeps the file's contents beside it as
|
||
`config.json.corrupt` (the same quarantine `views.json` already had), and
|
||
the stand-in defaults carry a mark that makes `save` refuse to run, so
|
||
nothing writes until the file parses again. The hot-reload watcher tells
|
||
"broken" apart from "absent" and keeps the settings the app is running on
|
||
instead of swapping defaults in mid-session, and both the startup and the
|
||
reload path say what happened and where the copy is. A file that simply
|
||
cannot be *read* logs a warning now too — it used to fall to defaults with
|
||
no trace at all. (#537)
|
||
- **A mistyped `tty7 send` address no longer types itself into your own
|
||
pane** — `tty7 send %3x --key C-c` used to degrade the unparseable `%3x`
|
||
into text aimed at the pane the caller was sitting in, then deliver the
|
||
Ctrl-C there too, interrupting whatever was in front of them. A `%`
|
||
followed by a digit that still fails to parse is now the address error it
|
||
looks like, while text that merely starts with `%` — vim's `%s/foo/bar/`,
|
||
`%!sort` — keeps typing as given, as does anything unmarked that is not a
|
||
plain number (#538).
|
||
- **The `ws rm` docs now say the panes are hung up, not orphaned** — the CLI
|
||
reference claimed removing a workspace leaves its panes running as orphans
|
||
to be found with `pane ls --all`, when the code has hung them up since the
|
||
command exists; only a *failed* hang-up (which `ws rm` reports by pane id)
|
||
leaves orphans behind. The site reference, the bundled skill reference, and
|
||
`ws rm --help` all read the same way now (#539).
|
||
- **The Info panel's agent row reads one pane, not a splice of two** — the
|
||
name came from `tab.agent` (whichever leaf has an agent) while the status
|
||
came from `tab.agent_status` (the most urgent across the whole tab), so a
|
||
split tab running two agents could show one pane's name beside the other
|
||
pane's state. The row now takes both from the detail pane when it has an
|
||
agent, and otherwise from the tab's most urgent agent pane — so the row
|
||
still holds while focus sits on a plain shell, and its two halves always
|
||
describe the same pane (#543).
|
||
- **Windows paths in the Info panel shorten to their leaf again** — the cwd
|
||
row split on `/` only, so a backslash-spelled path (any agent-reported cwd,
|
||
a cmd pane, the shell-integration-off case) elided its *tail* and hid the
|
||
directory's own name. The split now takes the last of either separator,
|
||
and the `~` shortening — shared by the Info panel, the tab strip, and the
|
||
home picker — reads `USERPROFILE` as well as `HOME` and compares with
|
||
separators normalized and case folded, so a `C:/Users/…` pane shortens
|
||
under a `C:\Users\…` home (#544).
|
||
- **A hand-edited keybinding takes effect without a restart.** The config
|
||
watcher reloaded everything except the keymap, so a `keybindings` edit
|
||
showed up in the settings list off the live global while the key itself
|
||
stayed dead until the app restarted. The watcher now rebuilds the keymap
|
||
when the binding config actually changed — `(keybindings,
|
||
keybinding_preset, prefix)` — so the app's own `save()`, which a sidebar
|
||
drag or a palette open triggers, does not churn it. The rebuild also
|
||
replaces the map instead of appending to it, so rebinding no longer leaves
|
||
a retired copy of the whole table behind for every keystroke to walk. A
|
||
config.json that does not parse keeps the keys it was already dispatching,
|
||
since the reload that fails never reaches the rebuild (#548).
|
||
- **The font-size and line-height steppers no longer push a hand-set value
|
||
the wrong way** — the settings steppers and the `Ctrl+=`/`Ctrl+-` keys
|
||
clamped to a narrower range (font 6–48, line height 1.0–2.0) than the
|
||
config file allows (4–256, 0.5–4.0), so `font_size: 50` shrank to 48 on
|
||
"+" and wrote that back, permanently changing a value it only meant to
|
||
nudge. The steppers and `sanitize` now share one range, defined next to the
|
||
validation. The scrollback and notify-threshold preset rows got the matching
|
||
fix: a value between two buckets no longer lights up the nearest one
|
||
(`scrollback_limit: 5000` highlighted "10,000", and clicking that cell
|
||
silently overwrote it) — it shows a "Custom (5,000)" cell that names the
|
||
real value and is not a button (#550).
|
||
- **A refused in-place daemon restart no longer strands every pane it
|
||
promised to keep** — Restart Server clears the window before attempting the
|
||
handoff, and when the handoff failed the window simply stayed empty with an
|
||
error on the home page. The daemon was still running and serving those
|
||
panes, but every restore path is driven by the machine tree, and the next
|
||
sync of the emptied window diffed into "close every tab" against it —
|
||
deleting the pane records under shells that were still alive, or the whole
|
||
workspace at once if the user just closed the window, right after the
|
||
dialog had promised nothing would be interrupted. A failed restart now
|
||
drops the dead link and pulls the layout back from the tree, reattaching
|
||
the living panes; where the failure really did take the daemon away, the
|
||
missed pull leaves a rehydration debt instead, which is what stops the
|
||
empty window from being pushed up as the layout. (#554)
|
||
- **Settings' Shell Arguments field now splits like a command line, and quotes
|
||
on the way back.** The field split on raw whitespace, so `-c "echo hi"`
|
||
reached the new shell as four argv fragments with the quote characters still
|
||
attached, and the damage was invisible until the pane misbehaved. The round
|
||
trip was lossy in the other direction too: a perfectly legal
|
||
`"args": ["-c", "echo hi"]` in `config.json` refilled the field as
|
||
`-c echo hi` and re-committed as three argv the moment it lost focus, without
|
||
the user typing a thing. Quoted text is now one argument, the refill quotes
|
||
the arguments that need it, and a value whose quotes do not close is refused
|
||
with an explanation under the input rather than saved as fragments. A path
|
||
spelled with backslashes still means itself. (#551)
|
||
- **`tty7 send --enter` now presses Enter when there is nothing to type** —
|
||
`--enter` is shorthand for `--key enter`, but it was never counted as a key,
|
||
so `tty7 send %42 --enter` answered "needs TEXT … or a --key to press"
|
||
instead of running what pane 42 already had typed. It counts now, with an
|
||
address or without one (`tty7 send --enter` presses Enter in your own pane).
|
||
An *unmarked* id is deliberately left out: `tty7 send 83 --enter` reads as
|
||
much like typing "83" where you are sitting as like pressing Enter in pane
|
||
83, so it stays a loud error that names both spellings (`send %83 --enter`,
|
||
`send %PANE 83 --enter`) rather than quietly retargeting the keystroke
|
||
(#581).
|
||
- **A `~` in a path now belongs to the machine that path is on.** The Info
|
||
panel's cwd, the tab strip and sidebar titles, and the switcher's workspace
|
||
and tab rows all shortened against *this* machine's `$HOME` whoever the path
|
||
belonged to, so a server sitting in `/home/deploy/app` read as `~/app` on a
|
||
laptop that happens to log in as `deploy` and stayed spelled out on one that
|
||
does not — the `~` naming the wrong machine either way. Each of those rows
|
||
now measures a path against the home its own host reported when the link
|
||
came up, and a path on a machine nothing here has a link to — or one a pane's
|
||
shell has `ssh`'d away to — is shown in full rather than against a home that
|
||
is not its own. (#580)
|
||
- **Push at a detached HEAD no longer fails silently** — the sync tile claimed
|
||
"Publish Branch" (the one thing a detached HEAD cannot do) and swallowed the
|
||
click, and the key binding, palette and "Commit and Push" follow-up died in
|
||
the same guard just as quietly. The tile and the branch menu's Push item now
|
||
disable themselves with a tooltip that says why, every other path gets a
|
||
toast naming the reason, and an unborn branch — no commits to send yet —
|
||
gets its own answer instead of sharing the detached one's silence. (#545)
|
||
- **A refused commit says why, not always "Nothing to commit"** — committing
|
||
from the palette or the key binding with staged work but a blank message was
|
||
answered with "Nothing to commit", which sends the user staging files they
|
||
already staged; the toast now carries the plan's actual reason ("Write a
|
||
commit message first"), the same words the panel's own button shows on its
|
||
tooltip. (#546)
|
||
- **Terminal pop-up menus no longer leak clicks into the grid behind them** —
|
||
the completion menu and the reverse-search menu (both the floating panel and
|
||
the input-bar row) inserted no hitbox of their own, so a press on one fell
|
||
straight through to the terminal: it cleared whatever was selected and
|
||
dragged out a new selection, merely moving over a row underlined the text
|
||
beneath it, and a Ctrl+click opened the link the menu was covering. All
|
||
three occlude now, so a press on a menu stops at the menu. (#541)
|
||
- **A file link that fails to open says so** — clicking a file path whose
|
||
opener is missing, or whose `link_file_command` template expands to nothing,
|
||
used to fail into a logfile line and nothing else; the click now raises the
|
||
same kind of toast a failed image upload does, naming the path and the
|
||
error. (#542)
|
||
|
||
|
||
- **A restart no longer asks a second time about a server that is already
|
||
gone** (#770). The record behind the "Restart Server?" prompt could only ever
|
||
accumulate: the control link re-armed it on every failed reconnect, including
|
||
in the seconds spent reading the dialog, so restarting fixed the daemon and
|
||
not the record and the next window asked again. A probe that finds the daemon
|
||
ours now clears it, on both the handoff and the stop-and-spawn paths, and a
|
||
verdict about a daemon that has since been replaced is dropped rather than
|
||
written.
|
||
|
||
- **A split pane's cursor reflects which pane actually has focus** (#736) —
|
||
both cursors could render active at once, so neither said where typing would
|
||
land.
|
||
|
||
- **The agent unread badge reads live focus** (#758), instead of clearing on a
|
||
pane that was merely visible.
|
||
|
||
- **Clicking a sidebar row's change counts activates that row before opening
|
||
its diff** (#706, #729), so the diff that opens is the one whose numbers were
|
||
clicked.
|
||
|
||
- **A window arriving at a workspace no longer claims to speak for panes it has
|
||
not read** (#716, #728) — the state that let a fresh window prune tabs it
|
||
knew nothing about.
|
||
|
||
- **Every SSH channel tty7 abandons is closed before the server does it**
|
||
(#715, #727). Leaked channels accumulated over a long session until the
|
||
server's per-connection limit refused the next pane.
|
||
|
||
- **A restored tab keeps its name** (#725). The title was left out of the
|
||
snapshot, so a tab restored after a restart came back labelled by its shell.
|
||
|
||
- **An emoji that overflows its cell gets the room instead of being shaved
|
||
flat** (#707), and a regional-indicator pair is shaped as the one flag it is
|
||
rather than two letters (#686, #691).
|
||
|
||
- **An orphan pane's Close button stays on screen** (#712) — the one control
|
||
that could clear it was the first thing to scroll away.
|
||
|
||
- **A stalled remote link no longer blocks the UI thread** (#709). A server
|
||
that stopped answering froze the whole window rather than the one workspace
|
||
waiting on it.
|
||
|
||
- **Windows paths are quoted correctly, "Checkout to…" is wired up, and the
|
||
Spawn reply is bounded** (#705).
|
||
|
||
- **The input bar measures columns with `unicode-width`** (#704) instead of a
|
||
hand-rolled table that disagreed with the terminal beside it.
|
||
|
||
- **A window that draws its own title bar is not framed again by the
|
||
compositor** (#679, #683) on Linux.
|
||
|
||
- **Reveal and copy normalize path separators on Windows** (#680).
|
||
|
||
- **The PTY gets back the Ctrl chords tty7 was eating** (#684), and Ctrl+V
|
||
reaches a full-screen program on the alternate screen instead of pasting over
|
||
it (#677, #682).
|
||
|
||
- **A silent Attach fails instead of hanging, and a rebuild that cannot put the
|
||
tabs up holds them** (#673, #681) rather than dropping them on the floor.
|
||
|
||
- **A daemon that lost both its names is still found and reaped** (#671), and
|
||
one lingering after quit-and-stop stays findable rather than stranding its
|
||
shells (#655).
|
||
|
||
- **Hidden panes stop repainting the whole window** (#670) — a background tab
|
||
under a busy program cost the same frames as a visible one.
|
||
|
||
- **The code editor keeps scrolled-out text off the line numbers**, and stops
|
||
painting its gutter and current line in the stock syntax theme's colours
|
||
(#636).
|
||
|
||
- **Ctrl+W in the editor kills one path component at a time** (#658, #659).
|
||
|
||
- **The sidebar's tab rows and workspace head resolve to a width** (#662)
|
||
instead of collapsing at certain panel sizes.
|
||
|
||
- **A restored screen stays out of ConPTY's viewport on Windows, and Restart
|
||
Server no longer crashes the window** (#657).
|
||
|
||
- **The SCM sync tile stays on the branch row at any panel width** (#650), a
|
||
branchless push is answered, and a refused commit says why (#545, #546,
|
||
#576).
|
||
|
||
- **The SSH menu keeps a menu's shape** (#649), the password prompt's scrim
|
||
covers the whole window, and a failed reconnect names the machine (#645).
|
||
|
||
- **The in-app notification is restyled to tty7's own visual language** (#646).
|
||
|
||
- **A command that is over in a blink no longer flashes across the tab.**
|
||
|
||
- **Settings gets its scroll range back**, and the scrollbar is held off the
|
||
window corner.
|
||
|
||
- **A remote server stops cleanly on machines with no `/proc`**, and the install
|
||
shows on the strip (#627).
|
||
|
||
- **A pane seeded by a window is recorded in that window's own mirror** (#628),
|
||
and a workspace switch no longer rereads the app mid-update (#618).
|
||
|
||
- **A resize defers to the daemon's Size echo on remote routes too** (#632), so
|
||
a remote pane reflows once rather than twice.
|
||
|
||
- **Shell integration is not injected into a zsh or fish the user gave
|
||
arguments to** (#629) — the case where injecting silently changed what the
|
||
shell was asked to do.
|
||
|
||
- **The new-worktree prompt is a window-level modal** (#626).
|
||
|
||
- **A deleted remote workspace is scrubbed from the listing snapshots** (#622).
|
||
|
||
- **A server creation interrupted by an update is finished**, and the note that
|
||
answered it is retired.
|
||
|
||
- **A new workspace keeps the name it was given** (#619).
|
||
|
||
- **A dialect refusal has a way out instead of a retry loop** (#617).
|
||
|
||
- **A window is told the name the machine gave the workspace it made** (#604,
|
||
#613).
|
||
|
||
- **A Unix pwsh pane's title names the user, host and home** (#611).
|
||
|
||
- **The right panel's tab icons redraw, and the tree glyph sits on the tile
|
||
ladder** (#578); the plus is drawn on the same optical bound as the rest of
|
||
the set (#577).
|
||
|
||
- **The tab strip's status dot is ringed in white on dark themes.**
|
||
|
||
- **`tty7 send --enter` presses the key it is shorthand for** (#581, #606).
|
||
|
||
- **In-app updates check a Mach-O's signature by codesign's exit status**
|
||
(#696), and CI asserts every Mach-O in the macOS bundle is the architecture
|
||
it ships as (#687, #692).
|
||
|
||
- **Nine UX fixes across the diff overlay, layout, settings and pane spawn**
|
||
(#623), and nineteen more low-severity ones (#584–#602, #615).
|
||
|
||
## [26.8.3] - 2026-08-12
|
||
|
||
### Added
|
||
|
||
- **A full Source Control panel** — the half-finished Changes tab is now a
|
||
working-tree panel with a real index. Files are grouped into merge, staged,
|
||
changes and untracked, with a file appearing in two groups when its status
|
||
says so; the rows stage, unstage, discard and open a diff, and a commit can
|
||
be written, amended, pushed, pulled or synced from the panel itself. The file
|
||
tree carries git decorations beside every entry, folded once per refresh
|
||
rather than once per frame. A diff opens from three sources — working tree,
|
||
staged, or a commit — behind a unified/side-by-side toggle that names which
|
||
one it is showing. Under the file list sits a commit history drawn with
|
||
lanes, paged as you scroll, with lane colours derived from the theme's own
|
||
`ansi16` and walked to a contrast floor so they stay apart on every builtin.
|
||
Anything that can lose work asks first, and the data layer is the single
|
||
place that decides what counts as destructive. **The control protocol does
|
||
not change** — an unmodified remote server serves all of it, push, pull and
|
||
fetch included, because the deadline that would have cut a long network
|
||
operation short was only ever enforced client-side. Non-UTF-8 paths are a
|
||
documented read-only limitation: those rows carry a tooltip rather than
|
||
sending git a pathspec nobody can spell. (#424)
|
||
|
||
- **The interface has a font size of its own** — `ui_font_size` in
|
||
`config.json` scales the whole chrome, and every window's root sets it, so
|
||
sidebar, panels, menus and dialogs move together. It defaults to gpui's own
|
||
16, so an existing config renders exactly as before. The terminal grid is
|
||
unaffected: it stays absolute pixels off `font_size`, which means a display
|
||
that is not Retina can finally have readable chrome without touching the text
|
||
in the panes. The detail panel, which had carried a private run of pixel
|
||
sizes that put its primary text at the size everything else uses for
|
||
secondary text, is back on the same ladder as its neighbours, with mono a
|
||
notch under the sans it pairs with.
|
||
|
||
- **A scrollback scrollbar down the right edge of a pane** — a pane's scroll
|
||
position lives in rows of scrollback rather than pixels of laid-out content,
|
||
so it had no handle to give a scrollbar. It now implements one over the grid
|
||
and draws the same bar the sidebar and every list already use — same theme,
|
||
same fade-out. The bar never drives the terminal: it records the row it
|
||
wants and the next render applies it, clearing the sub-line remainder and
|
||
cancelling an in-flight smooth scroll on the way. Scrollback piling up at the
|
||
live edge is deliberately not reported, so a pane printing a build log does
|
||
not hold a thumb on screen for as long as the output runs; every other
|
||
change passes through, including the history shrinking. (#480, #432)
|
||
|
||
- **Zoom a pane's font with the platform modifier and the wheel** — holding
|
||
Cmd (Ctrl off macOS) and scrolling over a terminal resizes the font instead
|
||
of the scrollback, which is what you reach for when showing a pane to someone
|
||
else. A wheel detent is one step whatever the platform bills it as, and a
|
||
trackpad accumulates until the fingers have travelled three lines, so a flick
|
||
does not run the font end to end. Steps go out as the existing
|
||
increase/decrease actions, so the clamp and the saved setting stay in one
|
||
place.
|
||
|
||
- **The command palette previews a theme while you arrow through the list** —
|
||
picking a theme used to close the palette, so seeing what one looked like
|
||
meant reopening it and retyping the search for every candidate. The
|
||
highlighted row is now applied to the running window and the picker stays
|
||
open: Return keeps it, Escape or any other way of closing puts the previous
|
||
theme back. A preview only touches the in-memory config, so arrowing through
|
||
the list never writes `config.json`, and the picker opens on the theme
|
||
already in use so opening it changes nothing by itself.
|
||
|
||
- **The palette matches commands in every locale, not just the one on screen**
|
||
— the filter only ever saw the label it was rendering, so a window running in
|
||
Chinese answered "no matching commands" to `theme`. Each entry now carries
|
||
every locale's wording of its key, plus the stable command id, as hidden
|
||
search aliases: built once with the entry, never rendered, and scored just
|
||
below a hit on the visible label so what you can actually read still comes
|
||
first.
|
||
|
||
- **fish history joins the Ctrl+R menu and inline completion**, locally and on
|
||
remote hosts. `fish_history` looks like YAML and is not — fish escapes only
|
||
`\` and newline and quotes nothing, so a YAML reader silently drops every
|
||
record holding a `: ` or a leading `[` and truncates anything with a ` #`,
|
||
which is most conventional-commit messages. It is read with a line scanner
|
||
shaped like fish's own reader instead. Each history file fetched from a
|
||
remote host now carries the name it came from, so the far end's fish records
|
||
reach the fish reader rather than arriving as literal `- cmd:` rows.
|
||
Multiline entries are skipped rather than half-recalled. (#421)
|
||
|
||
- **The daemon upgrades in place instead of killing every shell** — picking up
|
||
a new build used to mean stopping the daemon, and stopping the daemon takes
|
||
every pane with it: the pty master is a descriptor that process holds, so its
|
||
death raises SIGHUP on the shell, the agent and the half-finished command.
|
||
The daemon now rewrites itself with `execve` on handoff — same pid, same
|
||
children, same descriptors, same locks — writing what it knows about each
|
||
pane into a blob and clearing `FD_CLOEXEC` on the ptys first. The restart
|
||
Settings offers for a quiet moment is still there for the cases handoff
|
||
cannot cover. (#449)
|
||
|
||
- **The tab sidebar elides labels tail-first and explains itself on hover** —
|
||
a path keeps its leaf rather than its root, anything that is not a path keeps
|
||
both edges, and a hover card spells out in full whatever the row had to hide:
|
||
a renamed tab's real name, the terminal title behind a `Shell 3` placeholder,
|
||
the remote host. The card is decided by comparing what the row rendered
|
||
against what it rendered it from, so it never opens on a row that hid
|
||
nothing. (#446)
|
||
|
||
- **The Info tab's rows do what they show** — the Session table used to render
|
||
every fact the same inert way, with two unlabelled actions in a strip four
|
||
rows below the path they acted on. A `changes` row is now the sidebar's green
|
||
and red `+N −M` and opens the same diff overlay; an agent row wears the
|
||
status dot its tab does; a port row hands over its address instead of leaving
|
||
it to be retyped; and what a row can do appears at the end of it on hover, in
|
||
the strip the Source Control rows already use. A port is only offered as
|
||
`localhost` when localhost actually reaches it — a server bound to
|
||
`172.17.0.1` is named by the address it really holds. (#531)
|
||
|
||
- **Closing a tab asks before it ends work that is still running** — ⌘W was the
|
||
one action that permanently ended a shell in an app whose headline claim is
|
||
that shells outlive it, and a mid-turn coding agent, a forty-minute build and
|
||
an idle prompt all died identically and silently. Reopen Closed Tab was never
|
||
an undo: it restores the layout with a *fresh* shell. Closing now asks once,
|
||
naming what it would end, with Keep as the default button; an idle prompt
|
||
still closes without a word. A pane counts as busy only when the shell is
|
||
actually reporting, so a terminal without shell integration is not held
|
||
hostage by a question it cannot answer. Close Other Tabs and Tabs to the
|
||
Right skip anything that would raise a question rather than stacking one
|
||
dialog per tab.
|
||
|
||
- **The opencode plugin reports its session id**, so a restarted pane can
|
||
resume the same session with `opencode --session <id>`. It also maps
|
||
`session.status` busy/idle for versions that no longer emit `session.idle`. A
|
||
task-tool subagent runs in a child session whose events are structurally
|
||
identical to the pane's own, so the bridge remembers child ids and lets their
|
||
events pass without touching the pane's session — otherwise the pane would
|
||
report and resume the subagent's session, and a subagent going idle would
|
||
call the pane done. (#481)
|
||
|
||
- **A documentation site** — the docs are now a Mintlify site rather than a
|
||
handful of Markdown files in the repository. (#478)
|
||
|
||
- **Drop files into the Files panel to copy them in** — the panel has always
|
||
been a drag *source*; it now takes a drop as well. Files dragged in from the
|
||
desktop land in the folder under the cursor: a folder row takes them itself,
|
||
a file row stands in for the folder holding it, and the space below the tree
|
||
belongs to the top of it. The landing is highlighted while the drag is in
|
||
flight. Folders come in whole, the executable bit survives the copy, and a
|
||
name already taken is asked about rather than silently replaced — a "no"
|
||
leaves every file in the drop untouched, and a "yes" copies beside what is
|
||
there and swaps the two only once the copy is whole, so a copy that fails
|
||
partway leaves the original where it was. Two dropped items of the same
|
||
name are one name and one file: the first keeps it and the second is
|
||
refused, rather than landing on top of it with both reported as copied. It
|
||
works over a remote workspace too, reading here and writing there, up to
|
||
the size one control frame can carry; past that the panel says to use
|
||
SFTP.
|
||
|
||
- **Rearrange splits by dragging a pane** — hovering a pane now floats a small
|
||
grip along its top edge; dragging it moves that pane elsewhere in the tab.
|
||
A drop on a pane's **side** goes in beside it: facing a neighbour in the same
|
||
row or column it joins that row and takes an equal share of it, and only
|
||
facing across the layout — where there is no row to join — does it split that
|
||
pane in half. A drop on its **middle** trades the two panes' places, and a
|
||
drop in the band past a pane's **outer side** — the one facing the window
|
||
rather than another pane — puts it beside everything else as a full-width or
|
||
full-height band, sized to an even share of what that side already holds — so
|
||
a pane in the middle of a 2×2 becomes a full-height *third* column in a
|
||
single drag rather than taking half the window. The landing is highlighted
|
||
while the drag is in flight, and is only offered when the drop would actually
|
||
change the layout. A pane dropped beside another is now reconciled with the
|
||
machine tree as one `PaneMove` instead of a close-and-rebuild; a drop that
|
||
lands beside a whole group of panes rather than beside a single one still
|
||
takes the rebuild, which is all `PaneMove` can name.
|
||
|
||
- **Native Windows backdrop materials** — Settings → Appearance now offers a
|
||
**Background material** picker on Windows (**Auto / Blur / Mica / Mica Alt /
|
||
Acrylic / Off**) that maps onto the OS backdrop APIs: Mica and Mica Alt via
|
||
`DWMWA_SYSTEMBACKDROP_TYPE`, Acrylic via the native
|
||
`DWMSBT_TRANSIENTWINDOW` material on Windows 11 22H2+ (classic acrylic
|
||
before that), and Blur via `ACCENT_ENABLE_ACRYLICBLURBEHIND`. The dropdown
|
||
only lists the presets the current Windows build supports, the file
|
||
sidebar and right detail panel follow the window opacity so the material
|
||
shows through the whole workspace, and the settings panel stays opaque.
|
||
macOS and Linux keep the existing blur toggle.
|
||
|
||
- **Panes come back showing what was on them after the background service dies
|
||
unexpectedly** — a crash, a `kill -9` or a reboot takes the shells with it
|
||
either way, but the screens no longer go with them. A capped tail of each
|
||
pane's output is kept at `<config>/scrollback/*.bin` (0600 on unix, behind
|
||
the config directory's ACL on Windows; 256 KiB per pane, written at most
|
||
every 30s and only for panes whose output moved), and a pane that reopens on
|
||
a dead predecessor's id is handed it. A planned restart already carried the
|
||
live ptys across untouched; this covers the deaths nothing gets to prepare
|
||
for. There is no switch: the moment anyone learns they wanted this is the
|
||
moment a service has already died, so it is on for everyone. The bytes are
|
||
dropped as soon as nothing can ask for them — closing a pane deletes its
|
||
file at once, a restore consumes it, and a periodic pass collects the rest.
|
||
A pane's shell is recorded alongside, so a git bash pane no longer comes
|
||
back as PowerShell.
|
||
|
||
- **`tty7 wait` can wait for a command, not just an agent** — a new `free`
|
||
state ends the wait when the pane's foreground command has exited and the
|
||
pane is back to its bare shell, which is what a `cargo test` running in a
|
||
pane has instead of an agent status. With `--changed` it means "something ran
|
||
and then finished", the shape you want on the line after a `send`. It costs a
|
||
second request per poll and so is only checked when you name it — and only
|
||
when none of the agent states you named answered first, so `waiting,done,free`
|
||
on a pane of unknown kind cannot lose you a `waiting`.
|
||
|
||
- **`tty7 send --key` presses keys instead of typing characters** — `C-c` to
|
||
stop a runaway build, `escape` to close a TUI, `up`/`down`/`enter` to answer
|
||
a permission prompt that takes no text. Repeatable for a sequence, composable
|
||
with `TEXT`, and delivered as separate events 200 ms apart so a raw-mode TUI
|
||
reads a sequence as a sequence rather than as a paste. An unknown key name is
|
||
a usage error raised before anything is written.
|
||
|
||
- **`tty7 pane close` takes several panes, and `--orphans` clears the lot** —
|
||
`pane ls --all` has been able to *show* the panes an interrupted `run` leaves
|
||
behind; there was no way to act on that except by reading ids off the table
|
||
one at a time. A pane that cannot be closed no longer abandons the rest of
|
||
the batch.
|
||
|
||
- **`tty7 doctor` reports where the agent status hooks stand** — it has claimed
|
||
to check hooks in its own `--help` for a while without doing so. Missing or
|
||
outdated hooks are why an agent can look frozen in `tty7 agents` and why
|
||
`tty7 wait` on it only ever times out, so the check belongs in the verb
|
||
people run when something is not working.
|
||
|
||
- **SSH probes the `~/.ssh` default identity keys** — a connection with no
|
||
identity file of its own used to offer the server nothing unless an agent
|
||
was running, which on Windows is the common case (the OpenSSH
|
||
Authentication Agent service is off by default), and then reported "no
|
||
public key was accepted" when no key had ever been sent. `id_ed25519`,
|
||
`id_ecdsa` and `id_rsa` now stand in for the identity list when the
|
||
connection names no key of its own, the way `IdentityFile`'s default works
|
||
in ssh_config — naming a key replaces them rather than adding to them. The
|
||
agent is asked before either, because every public key offered spends one
|
||
of the server's `MaxAuthTries` whether or not it is wanted, and the key the
|
||
user loaded into the agent is the one most likely to work. A discovered key
|
||
that is
|
||
encrypted is used only when its passphrase is already in the OS keychain:
|
||
russh has no offer-without-signing probe, so asking would spend a prompt on
|
||
a key the server may not even want. A key named in the profile still asks,
|
||
as before. The failure text now separates the two situations the old line
|
||
papered over — keys the server rejected are named, and a round that offered
|
||
nothing says where it looked. (#484)
|
||
|
||
- **A WSL pane gets its shell integration whatever the distro's shell is** —
|
||
the bootstrap that runs inside the distro dispatches on `$SHELL`, and until
|
||
now only bash had an arm of its own. A distro whose login shell is fish got
|
||
one in #422; a zsh distro fell through to the catch-all and opened as a
|
||
plain login shell, with no prompt marks and no working directory reported —
|
||
so the inline editor never armed, and neither did anything else downstream
|
||
of OSC 133. zsh now gets the same redirectors the native path writes,
|
||
carried in over `WSLENV` and pointed at with `ZDOTDIR`. The bootstrap checks
|
||
they are actually readable from inside the distro before trusting them: they
|
||
live on the Windows side and arrive over `/mnt`, which a distro with
|
||
automount off cannot see, and a `ZDOTDIR` aimed at nothing would start zsh
|
||
with none of the user's own startup files at all. (#135)
|
||
|
||
- **Put your own entries in the new-tab menu** — `custom_shells` in
|
||
`config.json` takes a list of `{label, program, args}`, and each one becomes a
|
||
row in the menu behind the sidebar's **+**, after the shells tty7 detected. A
|
||
distro, a container, a REPL, the same shell against a different profile: they
|
||
are launched exactly as written, since tty7 chose none of the command and has
|
||
no defaults to add to it — including its shell integration, so a custom entry
|
||
opens without prompt marks or working-directory tracking even where the
|
||
detected row beside it has both. `shell` still names the one command that
|
||
stands in for the platform default; these are the rest. An entry with no
|
||
`program` is
|
||
skipped, one with no `label` is named after what it runs, and one that
|
||
borrows a name already in the menu is marked `(Custom)` so the row telling
|
||
you what a plain new tab opens with stays the only one wearing it. (#443)
|
||
|
||
### Changed
|
||
|
||
- **A Windows install for all users now updates itself** — previously an
|
||
in-place update was refused there (replacing files under `C:\Program Files`
|
||
takes administrator rights, and a silent installer launched unelevated would
|
||
either install a second copy per-user or raise a bare UAC prompt from a
|
||
temporary directory). The update now runs as one announced UAC prompt that
|
||
covers both privileged stages, and the app itself never runs elevated: a
|
||
helper running as the signed-in user waits the install out and relaunches
|
||
tty7 with the user's own token. The update dialog says the prompt is coming
|
||
before the app quits, and "Install on Next Launch" is not offered — nobody
|
||
would be there to answer. Declining the prompt is not an error: the staged
|
||
package simply waits in Settings. Everything the elevated half needs crosses
|
||
as command-line arguments (an elevated child does not inherit the
|
||
environment), the package's checksum travels from the release server in the
|
||
GUI's memory rather than from a file the download could have rewritten, and
|
||
the privileged stages always run the *installed* updater — a medium-integrity
|
||
process cannot swap the binary that gets elevated. The staged copy lives in
|
||
an administrator-only `%ProgramData%` directory while the chain runs (#504).
|
||
Installations updated by an updater that predates the chain still fall back
|
||
to the manual download page, so the first release carrying this updates the
|
||
way it always did; the one after it updates itself.
|
||
|
||
- **Every confirmation answers the way the platform taught you it would** — the
|
||
action button is on the right and Cancel on the left, through one shared
|
||
helper. Answer 0 is drawn rightmost and takes Return, so the old
|
||
`&[Cancel, Delete]` put Delete exactly where a decade of macOS trains people
|
||
to click Cancel. Escape also cancels now, in all nineteen of them: gpui only
|
||
binds it on a button declared as the cancel, and every call site had been
|
||
passing plain strings.
|
||
|
||
- **A search match is washed in the theme's accent, at a strength the theme can
|
||
afford** — the wash was drawn from the terminal palette's selection colour at
|
||
a fixed 1.45:1 against the background, which is less than a hairline is worth
|
||
spread over a whole cell, on a grid that is already grey on grey. The tint is
|
||
now the accent, the one colour the terminal surface has nothing else in, and
|
||
its strength is derived per theme from that palette's own text-contrast
|
||
budget rather than from a constant that has to be safe for the worst
|
||
palette. The current match drops its caret-coloured outline, which existed
|
||
only because the fill could not say "this one" on its own. (#470)
|
||
|
||
- **The block cursor is drawn as reverse video** rather than as a 55% tint that
|
||
halved whatever contrast the caret had. On a wide character it inverts the
|
||
half holding the glyph.
|
||
|
||
- **An inactive pane dims by blending its terminal colours toward the window
|
||
background** instead of being faded as a whole, and the fade is now worn by
|
||
the terminal rather than by the pane — which is what keeps the grab grip
|
||
legible on exactly the panes `dim_inactive_panes` fades, the ones being
|
||
reached for. (#464)
|
||
|
||
- **The pane grip is three dots, the way Ghostty draws its own** — the bar a
|
||
pane was picked up by grew and recoloured under the pointer and appeared
|
||
whenever the pointer was anywhere in the pane at all: a mark on top of the
|
||
terminal wherever the mouse rested, and a target that moved while being
|
||
reached for. It is now three dots with 80×12 of reach around them and nothing
|
||
between the two states but ink, asked for by the pane's top fifth rather than
|
||
by the whole pane. The target is there for as long as the pane can be moved
|
||
and only the dots come and go, so a pointer going straight for the top of a
|
||
pane can press the grip on the frame it arrives; a 150 ms fade makes them
|
||
read as arriving rather than blinking. (#463)
|
||
|
||
- **A WSL distro list comes from the registry rather than the WSL service** —
|
||
`wsl -l -q` has to reach the service, and reaching the service is the part
|
||
that can be slow, behind a hardcoded 3 s timeout that made the listing
|
||
all-or-nothing. On a machine where one round trip takes 3.3 s the call timed
|
||
out every time and no distro was ever offered in the shell menu: not slow,
|
||
absent. `Lxss` is where `wsl.exe` registers them, nothing is listening on it,
|
||
and it cannot hang; `wsl -l -q` stays as the fallback for when the key will
|
||
not open at all. Distros are filtered by `State`, so a cancelled install or a
|
||
failed `--import` no longer arrives in the menu and opens a pane that dies of
|
||
a registration error.
|
||
|
||
- **Opening a WSL tab costs one probe instead of two** — every pane ran the
|
||
readiness probe from the client and then the daemon ran the identical probe
|
||
before opening the link: two rounds of five serial `wsl.exe` calls to learn
|
||
one fact, and the client's copy threw its answer away and kept only the
|
||
error. A distro also says once where its server is and is not asked again,
|
||
rather than re-proving `uname`, `$HOME`, a stat and a liveness check on every
|
||
pane. Measured on a distro already running and connected: 800 ms to open a
|
||
tab, down to 440 ms — and on the machine in #454, where a round trip takes
|
||
3.3 s, the duplicate alone was costing 15 s a tab.
|
||
|
||
- **`tty7 pane close --json` now reports `{"closed": [ids]}`** rather than a
|
||
single `{"closed": id}`, because the verb takes more than one pane. A batch
|
||
that could not close everything exits 1 with `{"closed": […], "failed": […]}`
|
||
and the complaint on stderr, so a retry knows what is left.
|
||
|
||
### Fixed
|
||
|
||
- **Ctrl+C works in a pane on Windows again** — the daemon was created with
|
||
`CREATE_NEW_PROCESS_GROUP`, which disables Ctrl+C for the whole new group,
|
||
and Windows hands that state down to every descendant. Every ConPTY shell a
|
||
pane spawned inherited it and so did everything those shells ran: the pane
|
||
wrote 0x03 and conhost turned it into a keypress, but the `CTRL_C_EVENT`
|
||
never came, so `go run` and `npm install` carried on regardless. Git Bash
|
||
looked fine only because MSYS synthesises SIGINT from the byte itself.
|
||
`DETACHED_PROCESS` already left the daemon without a console for a control
|
||
event to arrive on, so the group flag bought nothing to begin with; both
|
||
daemon spawn paths and the headless CLI server now share one constant
|
||
without it, and a pane clears any inherited ignore before it opens the pty,
|
||
so a tty7 launched from a shell that already had the bit set is covered too.
|
||
(#459, #451, #314)
|
||
|
||
- **Two tty7s on different config directories no longer erase each other's
|
||
workspaces** — the machine tree resolved from `$HOME` while everything else
|
||
an instance owns resolved from the config directory, so `--config-dir` moved
|
||
every part of an instance except the one that says which workspaces exist.
|
||
Two daemons, each holding its own lock and each certain it was the only
|
||
server on the machine, co-owned one `machine.json`; the tree is written
|
||
whole, so the second to flush replaced the first's workspaces with its own,
|
||
and the next daemon to start read the survivor's tree as the machine's. An
|
||
empty tree is indistinguishable from a machine that really has nothing on
|
||
it, so the GUI did what an empty tree means and forgot those workspaces for
|
||
good. A lock and the thing it protects have to be keyed alike. The daemon
|
||
adopts a legacy file on startup so the move itself loses nothing, and only
|
||
the instance running out of the config directory the machine resolves to on
|
||
its own is entitled to that adoption — otherwise a default install beside a
|
||
`--config-dir` one would rename the machine's tree into the wrong place.
|
||
(#462)
|
||
|
||
- **A remote window that lost its first tree pull no longer sits empty until
|
||
you restart** — a window opening onto a remote workspace is blank until the
|
||
machine's tree is pulled and its tabs rebuilt, and a failed pull recorded a
|
||
debt on the promise that the next sync would settle it. Nothing settled it:
|
||
the hook that repays the debt ran for the local host and nowhere else, so on
|
||
a remote machine only a full reconnect, an edit in the window, or restarting
|
||
the app ever cleared it — and an empty window has nothing in it to edit. The
|
||
window sat on the home page with every tab and every shell still alive on the
|
||
machine. Both routine ways to fail a pull with a healthy link are covered
|
||
now: a tree read that overruns its ten seconds, and a workspace create that
|
||
loses a race with the priming pass running the same create from the other
|
||
side of the same window opening — that one is no longer treated as a failure
|
||
at all, the tree is simply read again. The retry is backed off, the backoff
|
||
ends with the run of failures rather than with the next success, and a window
|
||
left open on a machine that is really gone stops logging at warn once the
|
||
backoff settles at its cap. (#472)
|
||
|
||
- **The macOS DMG builds again** — every release and nightly macOS job had been
|
||
dying in packaging with `hdiutil: create failed - No space left on device`,
|
||
after the build, the signing and the notarization had all succeeded. The
|
||
runner's disk was never full: the path in the error is the image being
|
||
created, so it was the volume that ran out. `hdiutil create -srcfolder` sizes
|
||
the image from the bytes it is about to copy and does not cover what the
|
||
filesystem spends carrying them, so a bundle that fits by measurement still
|
||
runs the volume dry partway through. The room is asked for explicitly now —
|
||
twice the content plus 64 MiB — and since the image is compressed on the way
|
||
out the slack is nearly free: 127 MiB of empty volume costs about 672 KiB in
|
||
the published DMG. This was a threshold rather than a cliff, which is why it
|
||
began without anyone touching packaging. (#477, #476)
|
||
|
||
- **The CLI and the GUI agree on what exists** — five places where a workspace,
|
||
a tab or an attachment was real on one side of the socket and invisible on
|
||
the other, all rooted in the GUI keeping its own list of which workspaces
|
||
exist and consulting the machine tree only for the ones already in it, so
|
||
anything another client created was unreachable by construction. The switcher
|
||
now lists workspaces the machine holds that this client has never opened, and
|
||
opening one keeps its id instead of claiming a fresh one; opening a workspace
|
||
hydrates from the machine's tabs rather than saving an empty session over
|
||
them; a workspace removed with `ws rm` stays removed instead of being written
|
||
back under the same id and haunting the switcher until a restart; `tty7 ls`
|
||
can name the host holding a workspace; and `tab ls` / `ws tree` fall back
|
||
through name, agent, cwd leaf and process name rather than showing a blank.
|
||
(#423)
|
||
|
||
- **A pane the CLI created can be attached to** — a pane's owner names the
|
||
workspace allowed to attach to it, and the CLI wrote a literal `tty7-cli`
|
||
there for every pane it made. A window opening on a CLI-built workspace found
|
||
none of them attachable: it spawned a fresh shell per tab, orphaned the live
|
||
ones, and — because the tree still carried each pane's agent session — greeted
|
||
the user with a failing `claude --resume <id>` in every one of them. Both
|
||
spawn paths pass the workspace id now, and restore treats an unparseable
|
||
owner as no claim at all, so panes stamped by an older CLI attach instead of
|
||
stranding. `pane ls --all`'s OWNER column shows a dash when it agrees with
|
||
WS, so what is left in it is exactly what is worth reading. (#425)
|
||
|
||
- **A WSL pane starts when the distro's default shell is fish** — `wsl.exe --`
|
||
hands the command line to that default shell, which parsed the POSIX
|
||
bootstrap before `sh` could receive it, so the pane never started at all.
|
||
Every invocation carrying an argv goes out under `--exec` now. Those panes
|
||
then came up with no shell integration, because the bootstrap only had a bash
|
||
arm: no OSC 133 and no OSC 7, so no prompt marks, no exit status, no cwd, and
|
||
`tty7 wait` and busy/idle status dead in the pane. There is a fish arm now,
|
||
POSIX-quoted since `sh` parses the script rather than the user's own shell.
|
||
(#422)
|
||
|
||
- **A remembered WSL server path repairs itself** — the note saying where a
|
||
distro's server lives had no working way to be wrong: its only repair fired
|
||
when spawning `wsl.exe` failed, and `wsl.exe` starts perfectly happily with a
|
||
server path that no longer exists inside the distro. The exec failure arrives
|
||
later as an EOF on the bridge, so a distro that was reinstalled or had its
|
||
bin directory cleaned out failed every WSL tab from then on with no way out
|
||
but restarting tty7. A bridge that closed without ever sending a byte never
|
||
ran, and after one of those the next pane proves the distro again. The note
|
||
is also read under the install lock now, so a window restoring panes can no
|
||
longer spawn the binary that a server update is in the middle of replacing.
|
||
|
||
- **A half-read registry is no longer passed off as the distro list** — the
|
||
walk ended on any non-zero return and reported what it had, but only one of
|
||
those returns means "that was all of them"; the rest mean it stopped early,
|
||
and a failure at the very first index came back as an authoritative "there
|
||
are no distros". The sweep behind the shell menu keeps its last good list
|
||
only when the probe declines to answer, so that empty answer erased the
|
||
user's distros for the length of the TTL, with no error anywhere. The walk
|
||
says nothing now unless it reached the end.
|
||
|
||
- **The Source Control panel is on the same font scale as the rest of the
|
||
window** — the panel was cut from a branch that copied the right panel's size
|
||
ramp hours before the interface font scale landed on main and moved that ramp
|
||
onto rems. The two sides touched different files, so the merge had nothing to
|
||
conflict over and the panel shipped a step small and deaf to `ui_font_size`
|
||
entirely — its primary text at the size its neighbours use for secondary
|
||
text, and a file row mixing a rem-sized status letter with a pixel-sized
|
||
path. Every size a reader reads is a rem now; pixels stay on what type sits
|
||
inside, and the row pitches and section heights grow with the type they hold.
|
||
The commit body's fold also folds: `line_clamp` limits the wrapped lines
|
||
within one logical line, and a commit message is the one string this panel
|
||
draws that is always hard-wrapped, so a clamp of four over a thirty-line body
|
||
laid out all thirty and Show more was a label with nothing behind it. It
|
||
clips a height now. (#557)
|
||
|
||
- **A search highlight stays on its text while the pane is printing** — a match
|
||
point is a line of the grid it was scanned from, so every line that scrolled
|
||
off the top left each highlight washing the row its text had just left. The
|
||
stored points now slide with the text on every wakeup. The rescan behind them
|
||
was purely debounced, so a pane that never pauses — an agent streaming a
|
||
reply — never got one at all: the count froze and the highlights sat on text
|
||
that had since been rewritten in place. Output can now only push the deadline
|
||
out so far. (#559)
|
||
|
||
- **A machine's name gets its own line in the switcher** — a group header drew
|
||
the machine name and its `user@host:port` on one row, and the endpoint was a
|
||
fixed-width item that claimed its space first, so a machine whose name ran
|
||
long read as a stub: `y...`, `GAEM...`. Both the header and the "other
|
||
machines" rows use the two-line shape the workspace rows under them already
|
||
use, with the name backed by the full row width and the endpoint and link
|
||
status underneath. Rows with neither stay single-line. (#560)
|
||
|
||
- **The stray guide rail under a remote machine is gone** — expanding one in
|
||
the switcher drew a 1px vertical line left of the workspace avatars that
|
||
local groups never drew, so it read as a stray mark rather than an indent
|
||
guide, and it stopped short of the last row's midpoint leaving a dangling
|
||
stub. Remote rows indent exactly like the SSH host rows below them now.
|
||
(#536)
|
||
|
||
- **The workspace discs in the switcher are legible** — the monogram disc was
|
||
drawn at 0.55 opacity on every row except the current workspace, and the
|
||
initial inside it is already the foreground at 0.65, so the letter landed at
|
||
about 0.36 and went illegible on exactly the rows the panel exists to let you
|
||
choose between. The liveness dot is painted on the wrapper rather than inside
|
||
the disc, so it never dimmed with it — a washed-out grey blob with a
|
||
full-strength green dot stuck to its corner, which is what made the column
|
||
look dirty rather than quiet. The dimming is gone; the current workspace was
|
||
already saying so three louder ways. (#483)
|
||
|
||
- **Overlay scrollbars fade out again** — macOS reports that scrollbars should
|
||
not auto-hide for anyone with a mouse plugged in, and that was turned into
|
||
"always show" for every list in the app. The preference is about legacy
|
||
scrollbars, which take a gutter out of the layout; tty7's are overlay bars
|
||
painted on top of the content, so "always" parked an opaque bar over the
|
||
switcher's tab column for as long as the panel stayed open, with nothing to
|
||
fade it. (#471)
|
||
|
||
- **A tab label is cut on grapheme clusters, not on chars** — the sidebar's
|
||
elision measures against real glyph widths but sliced by `char`, so it could
|
||
satisfy every width check and still hand back a torn cluster: a
|
||
zero-width joiner with nothing in front of it, a variation selector landing
|
||
on the ellipsis, half a flag rendering as a bare letter. Scanning budgets
|
||
from 30px to 200px over emoji fixtures, 48 widths produced output no font can
|
||
render as intended. A tab title carrying an emoji is not exotic — plenty of
|
||
TUIs and coding agents put one there. Widths are unchanged, since clusters
|
||
are measured the same way chars were. (#450)
|
||
|
||
- **A multiline history entry draws on one menu row** — a command composed in
|
||
the inline editor keeps its newlines when it goes into the in-session
|
||
history, and gpui breaks text on `\n` whatever the white-space setting says,
|
||
so one such entry painted a dozen lines inside a one-line row and covered the
|
||
whole reverse-search menu. Line breaks are folded to a visible `↵` when
|
||
drawing — one char for one char, so the fuzzy matcher's highlight positions
|
||
still line up — and the stored entry is untouched, so recalling and running
|
||
it are unchanged. (#436)
|
||
|
||
- **One Restart server button on the Settings page** — the in-place-update
|
||
notice carried its own while the Server section right below it carried an
|
||
identical one. The notice moved into that section, so the single button that
|
||
ends every running pane is the only one on the page. (#452)
|
||
|
||
- **Several machines with outdated servers ask one question at a time** —
|
||
reconnecting to them raised one native modal per machine in a single pass,
|
||
stacked on each other, each about a machine the one in front of it did not
|
||
name. The queue is walked one question at a time now, and if the window goes
|
||
away with a question open the rest go back on the queue instead of being
|
||
swallowed.
|
||
|
||
- **Gestures that did nothing at all** — a pass over the whole app, most of it
|
||
found by driving the UI rather than by reading the code:
|
||
|
||
- **Escape in a confirmation dialog** cancels, in all nineteen of them.
|
||
- **⌘P then Return** now runs the first row. The list started with nothing
|
||
selected and only picked a row when a *query* changed, so nothing showed
|
||
what Return was aimed at.
|
||
- **The palette's `→ edit` badge** is `⌘E`. Neither advertised gesture could
|
||
fire — `→` is eaten by the query field and `⌘↵` by Enter Full Screen — so a
|
||
saved SSH profile could not be opened for editing from the palette at all.
|
||
- **⌘G past the last match** wrapped to match 1 and left it behind the
|
||
floating search bar, reading "1/3" with nothing visible. The bar's rows
|
||
count as off screen now.
|
||
- **`ls | gre` + Tab** completes `grep` rather than filenames. The
|
||
command-position test was "everything before the cursor is whitespace",
|
||
true only at the very start of the line; over SSH it also spent a round
|
||
trip listing the remote cwd to do it. Same for `a && b` and `a; b`.
|
||
- **Typing or pasting while scrolled back** snaps the view to the prompt, so
|
||
your own keystrokes are not landing off screen.
|
||
- **Typing into a filled text box** — rename, prompts — replaces the text
|
||
instead of prepending to it. Every one of them opens with its contents
|
||
selected.
|
||
- **Double-clicking a file over SFTP** opens it; only folders used to
|
||
respond.
|
||
- **Tab inside the palette** stays in the palette, and in the switcher
|
||
crosses to the tab column, instead of escaping to the next focusable
|
||
button.
|
||
- **Reveal in Finder on a remote workspace** is hidden rather than handing
|
||
the local file manager a path on another machine. Copy Path stays.
|
||
|
||
- **Things that failed silently now say so** — theme writes, theme
|
||
duplication, opening the themes folder, a server restart, a session restore
|
||
that came back short, an unparseable theme file, an SFTP overwrite, a search
|
||
count that hit its ceiling, a file-tree search that hit its ceiling, a delete
|
||
that did not go through, SSH auth with nothing to try, and an unknown action
|
||
in a keymap each either did nothing visible or blamed something else. All of
|
||
them now report what happened and, where there is one, what to do. The
|
||
sidebar's diff count no longer disagrees with the diff it opens, a
|
||
filtered-out SFTP directory is no longer called empty, and the theme picker
|
||
says when its filter matched nothing.
|
||
|
||
- **A directory with no children says which kind of nothing it is** — the
|
||
listing error was discarded, so a folder the OS refused rendered
|
||
byte-identically to one that really is empty and to one still being read. An
|
||
expanded directory now draws Reading…, Empty, Only hidden files, or Could not
|
||
be read; arrow-key navigation skips those placeholders because they are not
|
||
files. The tree column and the tab sidebar get the same treatment when a
|
||
filter drops everything.
|
||
|
||
- **Layout and hit targets** — the settings pages, the diff overlay, the
|
||
Markdown preview, the SFTP transfers list and the switcher's columns all
|
||
scrolled with no scrollbar and now have the one the rest of the app has. The
|
||
split divider takes an 8px grab, the same as the panel edges. Icon-only
|
||
buttons across the chrome get a 24×24 target without changing how they look,
|
||
and their tooltips print the chord. The active tab and the New Tab button
|
||
stay on the tab strip instead of scrolling out of reach. The switcher card
|
||
fits inside the window it floats over, and the window itself has a minimum
|
||
size. Below about 1000px the SSH host form stacks each control onto its own
|
||
line — rows measure the width they will actually get — rather than squeezing
|
||
the label column toward a letter per line and pushing Connect and the 2FA
|
||
option off screen. Nothing moves above the breakpoint.
|
||
|
||
- **Contrast, colour and theming** — button labels, panel headings, tooltips,
|
||
menus, the command line and the splitters are drawn in the active preset's
|
||
own colours instead of ignoring it. Sidebar text, carets, hairlines and the
|
||
semantic inks that bypassed the contrast machinery are floored, and a
|
||
hairline is worth the same in every theme. Three faded states had two values
|
||
each, the app had two transition curves, and the theme card rounded unlike
|
||
the panel it sits in: one value, one duration and curve, one radius.
|
||
|
||
- **An agent's state is not carried by hue alone** — Working, Needs input and
|
||
Done were a hue on a nine-pixel dot with no shape, label or tooltip
|
||
anywhere, and the pair that decides whether you go and look is amber versus
|
||
green, which is the pair red-green colour vision separates worst. Needs input
|
||
draws as a ring rather than a filled dot, the tab avatar has a tooltip naming
|
||
the agent and its state in all three locales, and a brand disc that lands
|
||
within 1.25:1 of the window fill — Codex and Grok both ship a pure black one,
|
||
which dissolves on a dark theme and leaves a white glyph floating — gets a
|
||
hairline.
|
||
|
||
- **The palette stops showing recent commands twice** — the zero-query list
|
||
pushed a Recent section and then pushed every group containing those same
|
||
commands, so the five rows you use most were the five rows shown twice.
|
||
Promoting to Recent moves a command now rather than cloning it, and a group
|
||
left empty drops its header with it. Ranking also threw frecency away the
|
||
moment a character was typed, so the command someone runs every day stopped
|
||
floating exactly when they started reaching for it; a bounded bonus rides
|
||
along with the match score — enough to separate two comparable matches, not
|
||
enough to let a well-worn command outrank a plainly better one.
|
||
|
||
- **SFTP transfers stop writing over files without a word** — downloading the
|
||
same file twice overwrote the first copy and uploading into the folder on
|
||
screen overwrote whatever was already there, neither with any warning and
|
||
neither recoverable. A download lands as `name (2).ext` the way a browser
|
||
does, still in one click; an upload asks once, naming what it would replace.
|
||
The upload check reads the listing already on screen rather than making a
|
||
round trip to ask.
|
||
|
||
- **Escape no longer throws away an unsaved SSH connection** — editing a
|
||
profile and pressing Escape closed Settings and discarded the form. Whether
|
||
the form was dirty was already known — it is what enables Save — but was only
|
||
used to grey out a button. Escape, the close button and ⌘, ask now, with Keep
|
||
Editing as the default. The nineteen rows of SSH Advanced also carry six
|
||
quiet headings — Authentication, Proxies, Algorithms, Connection, Session,
|
||
Security — of two to four rows each; no row moved, the existing order already
|
||
grouped this way and never said so.
|
||
|
||
- **Settings search marks what it counted** and scrolls the page to the hit,
|
||
including when the hit is a section header. Background images, Compression
|
||
and four other settings that were missing from the index are in it, and a
|
||
query that matches nothing on the current page no longer greys out a page you
|
||
are simply reading.
|
||
|
||
- **Copy, i18n and discoverability** — every failure opens with "Could not", as
|
||
most already did; every popup menu is in the Title Case the rest use; the
|
||
background server has one name everywhere; settings labels are sentence case;
|
||
and no Markdown backticks are printed at the user. The terminal's right-click
|
||
menu was the last surface still hardcoded English in a three-locale app, and
|
||
it also used a fourth name for actions the menu bar, palette and Keybindings
|
||
already agree on. Chinese quotes names the way Chinese quotes them, Japanese
|
||
descriptions end the way the other eighty do, the 16 ANSI colours are named
|
||
rather than numbered, and the remote Files search box re-translates on a
|
||
language switch. The i18n parity test was a hand-maintained 900-line copy of
|
||
the key list and now sweeps a generated one, with a documented allow-list for
|
||
the keys that stay English — and the untranslated-string guard actually
|
||
guards. Completion specs got git's missing descriptions filled in, cargo's
|
||
`--color` described like the other 41, and a rustup flag that described
|
||
itself fixed; a candidate says when its description was cut. The Keybindings
|
||
page is grouped by command group, SSH is reachable from the menu bar, "Enter
|
||
Full Screen" no longer appears twice, ⌘W names the command it is about to
|
||
end, and the View menu teaches the right key for Zoom Pane.
|
||
|
||
- **Switching workspaces no longer wipes the tab titles of the one you left** —
|
||
in the switcher, the workspace the window was showing had properly named tabs
|
||
while every other one read "Claude Code", over and over, for as many tabs as
|
||
it had. The two columns come from two sources: a window names its own tabs
|
||
from its live terminals' OSC titles, and every workspace it does not own it
|
||
reads out of the machine tree — which recorded each pane's foreground
|
||
*process* name and never the terminal's title, so the naming fell through to
|
||
the next best thing it had, the agent. Switching a workspace happens in place
|
||
and drops the terminals the window was reading, so the workspace you just
|
||
left went from named tabs to a column of identical rows. The daemon now
|
||
records the title a pane sets (OSC 0 and 2, capped at 256 characters) in the
|
||
tree beside its cwd, and the switcher shows it, put through the same
|
||
abbreviation the tab strip uses so a shell's `user@host:~/dir` reads `…/dir`
|
||
in both places. In a split, the pane running an agent names the tab rather
|
||
than whichever shell happens to be first. `tty7 tab ls` and workspaces listed
|
||
on a remote machine were reading the same missing field, and are named the
|
||
same way now.
|
||
|
||
- **A WSL distro that cannot reach `/mnt` keeps its own `.bashrc`** — the
|
||
bootstrap handed bash a `--rcfile` living on the Windows side without
|
||
checking the distro could read it. bash ignores an unreadable `--rcfile`
|
||
without a word, and tty7 starts it non-login precisely so that `--rcfile` is
|
||
honoured at all — so a distro with automount off, or a root moved by
|
||
`wsl.conf`, opened a shell that had read no startup file the user wrote: not
|
||
tty7's, and not their `.bashrc` or `.bash_profile` either. The arm now checks
|
||
first and falls back to a plain login shell, which reads the same startup
|
||
files a pane without shell integration reads and only gives up the
|
||
integration.
|
||
|
||
- **An idle window stops re-reading the repository on Linux** — with a
|
||
repository open, tty7 ran `git status -uall` about two and a half times a
|
||
second, forever, with nothing on screen changing and nobody touching the
|
||
machine. The filesystem watch was feeding itself: Linux reports opening a
|
||
file as a watch event, every read of `.git` is an open, and reading `.git`
|
||
is exactly how the watch's own events were answered — so each answer
|
||
scheduled the next question. Reads are no longer mistaken for changes;
|
||
a write finishing still is. macOS and Windows were never affected, which is
|
||
why this survived so long.
|
||
|
||
- **An alias put back in an `Include`d ssh config file is found again** — the
|
||
check behind a parked remote workspace watched `~/.ssh/config` for changes
|
||
and nothing else, but `Include` is common and editing an included file
|
||
leaves the including file's timestamp exactly where it was. So re-adding a
|
||
`Host` block where it used to live changed nothing: the workspaces on that
|
||
alias stayed parked, with no retry and no error, saying a new profile would
|
||
find them again — which is what the user had just done. Every file the parse
|
||
reads is watched now, and the root config even when it could not be read, so
|
||
one appearing later is noticed too.
|
||
|
||
- **A rejected stored credential asks again instead of failing forever** — a key
|
||
passphrase saved with "remember" was written to the keychain before the daemon
|
||
had tried it, and a wrong one then ended every later connection at "could not
|
||
decrypt identity file" with no prompt at all: the key was unusable and nothing
|
||
in the app could clear the entry. Keyboard-interactive had the matching hole,
|
||
replaying a stale saved password on every reconnect, because OpenSSH ends a
|
||
rejected request with a plain failure rather than another question. The daemon
|
||
now says which secret was refused, both paths ask again, and an answer given
|
||
without "remember" forgets the stale entry — the self-heal the password path
|
||
has always had. Deleting a host profile also stops stranding its key
|
||
passphrases in the keychain, and a prompt raised for a remote workspace now
|
||
carries the real user, host and port instead of filing everything under port
|
||
22.
|
||
|
||
- **A host key of a new algorithm is no longer reported as a compromise** — a
|
||
server that grew an ed25519 key beside the ssh-rsa one it had always had was
|
||
met with the full man-in-the-middle sheet: fingerprint diff, type `yes` to
|
||
override. Worse, negotiation always led with ed25519 rather than with what was
|
||
on file, so a host known only by an ssh-rsa key raised that alarm on every
|
||
single connection. The algorithms already in `known_hosts` are now offered
|
||
first, the way OpenSSH's own ordering does it, so the question usually never
|
||
comes up; when it does it is the ordinary "a key you have not seen" confirm,
|
||
naming the algorithm the host is already known by. A key that contradicts one
|
||
on file still raises the alarm, and overriding it now removes the superseded
|
||
line instead of leaving the rejected key trusted for good.
|
||
|
||
- **The "Override" button on a changed host key overrides** — it submitted the
|
||
same refusal the Abort button sends whenever the field did not read exactly
|
||
`yes`, so clicking it, or typing a stray character first, closed the sheet and
|
||
quietly abandoned the connection. It is now disabled until the confirmation is
|
||
typed, says so once there is something in the field, and Enter no longer
|
||
abandons the connection either.
|
||
|
||
- **One account of whether a machine is connected** — the window's memory of a
|
||
manual attempt used to outrank the supervisor that actually holds the links,
|
||
in every place the two were consulted. Taking a workspace back from another
|
||
client flipped the status to Attached and re-enabled input without ever
|
||
sending the attach, so keystrokes went into a socket the far end had given
|
||
away; a link brought up from the switcher never attached anything at all; a
|
||
failed connect owned the status strip of every remote window, including
|
||
windows on other machines, until it was dismissed by hand; and a machine being
|
||
retried in the background was drawn as one nobody had ever connected to, its
|
||
workspaces folded out of the switcher, with the reason for the last failure on
|
||
screen for a quarter of a second before being overwritten. The supervisor is
|
||
now the only account: the reclaim is really sent and undone if it is refused,
|
||
a stale failure is retired the moment the link comes up, and a retry says
|
||
which attempt it is on and what went wrong last time.
|
||
|
||
- **A failed SFTP poll no longer reads as an empty transfer list** — a poll that
|
||
could not reach the daemon returned no jobs, which is the same answer as
|
||
having none: the transfer tray disappeared and every upload the panel was
|
||
waiting on was counted as landed. Over a link that is down that was the
|
||
permanent answer, not a blink. A failed poll now keeps the transfers it cannot
|
||
see, settles nothing, and says so in the tray.
|
||
|
||
- **A port forward whose loop has exited stops reporting Listening** — the
|
||
status was written once when the forward was set up and never again, so a
|
||
forward whose accept loop had already gone reported itself as healthy for as
|
||
long as the pane stayed open — which is indefinitely, because a pane is
|
||
deliberately not closed when its SSH connection dies. The loop now records why
|
||
it left, and the panel shows it.
|
||
|
||
- **A file-tree search that failed says so** — an unreachable host, or a search
|
||
the host refused, drew "Nothing matches" — indistinguishable from a search
|
||
that ran and found nothing, and every bit as convincing. It now says the
|
||
search failed. Creating or renaming a file through the tree also stops
|
||
reporting a bare `Permission denied (os error 13)` and names the file and the
|
||
operation, the way deleting one already did.
|
||
|
||
- **A port-forward edit cannot lose the rule it replaces** — saving an edit
|
||
removed the old rule before adding the new one, so an edit onto a port already
|
||
in use ended with the working forward gone and nothing in its place. The old
|
||
rule is now put back when the new one cannot start, and the form stays open
|
||
with the reason. Add and Save also stop doing nothing in silence: an
|
||
unparseable bind port, a missing target host or a target port of 0 used to be
|
||
a button that did not respond, and now disables itself and says which field is
|
||
wrong. A forward request that never reaches the session no longer empties the
|
||
panel either.
|
||
|
||
- **A half-filled SSH profile is refused rather than saved** — the form saved
|
||
whatever was in it. An empty host became a blank, unidentifiable row in the
|
||
host list and reached the socket layer as a DNS error about a name nobody had
|
||
typed, because Connect had no gate at all. A mistyped jump host was silently
|
||
dropped and saved as a direct connection. `proxy.example.com` with no port,
|
||
and `proxy.example.com:88O` with a bad one, both saved a proxy on port 0 and
|
||
failed much later, somewhere else. Each complaint now prints under its own
|
||
field and disables Save and Connect: the host is required (a name is not, and
|
||
a blank port still means 22), an unknown or self-referential jump host is
|
||
named out loud, and a proxy address with no port takes the scheme's default —
|
||
1080 or 8080 — rather than none at all. A proxy already saved on port 0 is
|
||
read as no proxy.
|
||
|
||
- **An ssh config import says what it did** — the button was silent three ways
|
||
over: a missing or unreadable file did nothing, a file naming no importable
|
||
hosts did nothing, and a clean import of six hosts also did nothing visible.
|
||
It now reports each of those, with counts of the hosts added, updated and
|
||
already current, and names the options tty7 has no field for — `IdentityAgent`,
|
||
`CertificateFile`, `IdentitiesOnly` and the rest — grouped with the hosts that
|
||
set them, so a host that behaves differently under tty7 than under `ssh` has
|
||
something to point at.
|
||
|
||
- **"Forget Password" asks first, and says who else it signs out** — one click
|
||
deleted the keychain entry, irreversibly and without a word. The entry belongs
|
||
to the endpoint rather than the profile, so forgetting from one host also
|
||
signed out every other profile reaching the same `user@host:port` — a direct
|
||
connection and one through a jump host, say — and that only turned up as an
|
||
unexpected password prompt on a host nobody had touched. It now confirms, and
|
||
when the endpoint is shared the dialog says how many others go with it.
|
||
|
||
- **The Files tree follows an agent into a git worktree** — the pane's cwd row,
|
||
the tab sidebar and the source control panel all track where a coding agent
|
||
is actually working, because an agent reports its own directory over the hook
|
||
stream. The file tree was the one panel still reading the foreground
|
||
process's cwd, and an agent that moves into a worktree never `chdir`s — so
|
||
`claude` entering `.claude/worktrees/feature` left the tree rooted in the
|
||
main checkout, disagreeing with the path printed directly above it.
|
||
|
||
- **`tty7 wait` no longer calls a busy shell `idle`** — a pane with nothing
|
||
reporting agent status was reported as `idle`, so `tty7 wait %3 --until idle`
|
||
returned success immediately, `matched: true`, about a pane that was midway
|
||
through a build. Those panes now report `no-agent`, which is both true and
|
||
the signal to use `--until free` instead; a wait that times out there says so.
|
||
|
||
- **Deleting an SSH profile no longer strands its remote workspaces** — the
|
||
switcher kept every entry that had connected through the profile, labelled
|
||
with a bare internal id, retrying a route that could never work again. The
|
||
confirmation now names the profile's endpoint and counts the entries going
|
||
with it, and deleting forgets those bookmarks along with the profile and its
|
||
keychain credentials — *forgets*, not deletes: the sessions on the remote
|
||
machine are untouched, and connecting to it again under a new profile brings
|
||
its workspaces back from the machine's own list. An entry holding a live or
|
||
in-flight connection is left alone, and parks once the link drops: it stops
|
||
retrying, keeps the name of the route that made it rather than the id, and
|
||
its switcher row offers *Remove entry* next to a note that a new profile
|
||
finds the session again.
|
||
|
||
- **Opening a folder from Explorer no longer costs you your layout** — "Open in
|
||
tty7", "Open tty7 here", and `tty7 <PATH>` restore the last window's tabs and
|
||
splits before opening the folder as one more tab in it. A launch naming a
|
||
directory used to skip the restore outright whenever no window was already
|
||
up — whatever "Restore last layout" said — so the folder arrived as a lone
|
||
blank terminal and the previous tabs were left behind, still running on the
|
||
server but reachable only through the workspace switcher. With a window
|
||
already up the same menu entry had always just added a tab, which is the
|
||
behavior both shapes of launch now share. A path still declines to follow the
|
||
layout onto a remote machine and starts a fresh local workspace there, since
|
||
the directory it names is a path on this computer.
|
||
|
||
- **An SFTP upload no longer sits in the browser under its temporary name** —
|
||
an upload is written as `<name>.tty7-upload-<hex>` and renamed into place at
|
||
the end, and the browser listed the directory the moment the transfer
|
||
started, catching exactly that name. Nothing listed again afterwards, so a
|
||
finished upload read as a file with a hash glued to its name until the
|
||
directory was navigated by hand. The premature listing is gone and the
|
||
directory is listed once the upload stops running — finished, failed or
|
||
cancelled.
|
||
|
||
- **Drag cursors on Windows** — the grip on a pane's top edge, and a sidebar
|
||
group being dragged, now change the pointer on Windows too. Win32 ships no
|
||
open- or closed-hand cursor and gpui answers both with the plain arrow, so
|
||
the grip read as ordinary background and a drag in flight gave the pointer
|
||
nothing to say; both now use the pointing hand there, and hovering the grip
|
||
no longer hands the cursor back to the arrow the moment the drag it
|
||
advertised begins.
|
||
|
||
- **A failed update install says so instead of asking again** — the GUI hands
|
||
the install to the `tty7-updater` helper and quits, so a failure inside the
|
||
helper used to reach only `update.log`: the old version relaunched with no
|
||
word about it, and because the prompt state had already been cleared, the
|
||
next check offered the very same version again — and again. The helper now
|
||
records the terminal outcome of every attempt (`update-outcome.json` next
|
||
to `update.json`) — before relaunching the previous app, so the relaunched
|
||
GUI finds it already on disk — and the GUI folds it into the update state at
|
||
startup: Settings shows the failure with the installer's own reason until
|
||
dismissed, and the version asks again only after the same three-day
|
||
reminder "Later" uses rather than on the next launch, so one failed install
|
||
neither nags nor quietly retires the version. The same channel carries the
|
||
success case, so a failure an earlier attempt recorded is retired by the
|
||
update that did land. As part of this the helper takes the config directory
|
||
as a `--config-dir` argument rather than through the environment, which an
|
||
elevated child process would not inherit (#540).
|
||
|
||
## [26.8.2] - 2026-08-09
|
||
|
||
### Added
|
||
|
||
- **Update tty7 without leaving the app** — the launch check and
|
||
**Settings → About → Check Now** now offer **Update and Relaunch** instead of
|
||
sending the user to GitHub Releases. A dedicated `tty7-updater` helper
|
||
verifies the release checksum, the bundle version, and — on macOS — the
|
||
code-signing requirement before replacing the installation, and restores the
|
||
previous copy if the relaunch fails. The update is fetched and verified while
|
||
the prompt is up, so installing it is a restart; declining defers it rather
|
||
than retiring it.
|
||
|
||
On macOS the new GUI reuses a running local server when its wire protocol is
|
||
compatible, preserving shells; an incompatible server keeps its shells too and
|
||
raises the existing explicit keep-or-restart prompt. Windows cannot replace a
|
||
running daemon's image, so its install path stops the service and the dialog
|
||
says so. Layouts that cannot be updated in place — notably an all-users
|
||
`C:\Program Files` install, where running Setup as the signed-in user would
|
||
either install a second copy beside the real one or put a bare UAC prompt in
|
||
front of a user whose GUI just vanished — keep the release-page fallback.
|
||
(by @ayamir in #309, by @ArnoChenFx in #330)
|
||
|
||
- **Stable and Nightly are separate update channels** — the channel is a
|
||
property of the installation rather than something inferred from how version
|
||
numbers sort, so a Nightly build follows Nightly instead of being walked back
|
||
onto Stable by an update it never asked for. Stable reads `/releases/latest`,
|
||
which excludes prereleases; Nightly reads `/releases/tags/nightly`. Neither
|
||
feed can hand the other an update, so an installation only changes channel
|
||
when the user changes it in Settings.
|
||
|
||
The nightly release cannot state its version in its tag — `nightly` is
|
||
force-moved every night, so `tag_name` is the literal string — so it now
|
||
publishes `nightly.json` beside the packages, falling back to parsing asset
|
||
filenames for builds that predate the manifest. Prereleases are ordered by
|
||
every numeric identifier in the stamp, and the stamp goes to the minute so two
|
||
builds in one day are distinguishable; a stable release still outranks every
|
||
dated build of its core version, which is how switching back to Stable
|
||
graduates rather than downgrades. Switching channel invalidates what the old
|
||
feed produced: the staged package, the deferred prompt, and any transfer still
|
||
in flight. Skipping a version is retired along with its state and its Settings
|
||
row. (#386)
|
||
|
||
- **A proxy for tty7's own network traffic** — update checks, release downloads
|
||
and remote-server installs now resolve an HTTP/SOCKS proxy from, in order, a
|
||
new `http_proxy` config field, the platform system proxy (Windows registry,
|
||
macOS `SCDynamicStore`), and `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY`.
|
||
Programs running in a pane are deliberately unaffected — they inherit their
|
||
proxy from their own environment, as in any other terminal.
|
||
(by @shihuaidexianyu in #367, reported in #365)
|
||
|
||
- **`tty7 wait` — the primitive an agent team was missing** — block until a
|
||
pane's agent needs input, finishes its turn, or dies:
|
||
`tty7 wait %3 --until waiting,done --changed --timeout 600`. The daemon
|
||
already tracked, per pane, which coding agent runs there and its status
|
||
(idle / working / **waiting on the user** / done, plus the agent's native
|
||
session id), but the only consumer was the GUI's status dots. This hands
|
||
the same view to programs: one agent can start a worker in another pane,
|
||
sleep until its peer blocks on a permission prompt, answer it, and read
|
||
the result — which a tmux-based agent team has to fake by screen-scraping
|
||
`capture-pane` on a timer.
|
||
|
||
The status the server keeps is a *level*, not an event: `done` stands
|
||
until the next turn begins, `waiting` until the agent moves again. So
|
||
`--changed` is not optional dressing — it ignores the state the pane was
|
||
already in when the wait began, which is what every round after the first
|
||
needs. Without it, a wait issued right after a `send` answers with the
|
||
*previous* turn's state before the worker has even read the input; the
|
||
JSON's `stale` flag says when that happened. Exit codes are made for
|
||
scripts: `0` matched, `124` timed out (the `timeout(1)` convention), `1`
|
||
with `"status": "exit"` when the worker died before it could get there.
|
||
The reply carries the agent's message and native session id, so a wake-up
|
||
is directly actionable.
|
||
|
||
Polling `AgentStates` rather than subscribing to events, on purpose: a
|
||
stateless question composes into scripts (`tty7 wait %3 && tty7 capture %3
|
||
--plain`), survives a server restart mid-wait, and needs no cursor. The
|
||
cost is one aggregate request per tick — the same one `tty7 agents` makes
|
||
once. (by @yetone in #248)
|
||
|
||
- **An agent-facing skill for the CLI** — the repo carries
|
||
[`skills/tty7`](skills/tty7/SKILL.md), which teaches a coding agent the
|
||
verbs above: work out which pane it is sitting in, split one, send a task
|
||
into another, capture what came back, run a command in a real PTY and pass
|
||
its exit code through. You install it yourself, with
|
||
`npx skills add l0ng-ai/tty7` — tty7 writes nothing into `~/.claude` for
|
||
it, and there is no switch in Settings that does.
|
||
|
||
A skill rather than a global instruction, on purpose: an earlier cut
|
||
appended this guidance to `~/.claude/CLAUDE.md`, which taxed every
|
||
session's context window and, worse, encouraged *every* agent to go drive
|
||
its neighbours. As a skill, only its one-line description rides in context
|
||
until something explicitly reaches for it.
|
||
|
||
- **Smooth scrolling for wheel mice** — a notch now eases into place over a
|
||
handful of frames instead of jumping the whole distance at once.
|
||
**Settings → Terminal → Scrolling → Smooth scrolling**, on by default.
|
||
|
||
Sub-line scroll positions have been in place for a while: the view keeps a
|
||
fractional remainder and paints the grid shifted by it, so the scrollback
|
||
need not land on a line boundary. But the position was a function of the
|
||
*event*, not of *time* — a notch arrived and the whole thing was applied at
|
||
once. Whether that looked smooth came down entirely to how fine-grained the
|
||
platform's deltas were. A macOS trackpad reports pixels, so it did. A wheel
|
||
on Windows reports whole lines (gpui multiplies the notch by the system's
|
||
scroll-lines setting, three by default), the fraction came out zero every
|
||
time, and the view jumped three lines per notch — the sub-line machinery was
|
||
present and never engaged. That is the "no smooth scroll on Windows" report.
|
||
|
||
macOS was no better, just differently broken: it reports a wheel mouse as
|
||
*pixels* too — `hasPreciseScrollingDeltas` is set — so the fraction was never
|
||
zero, but one detent arrives as a single ~103px event, which at a 21px line
|
||
height is a five-line jump applied in one go. Worse than Windows, and
|
||
invisible to any check based on the delta's type.
|
||
|
||
A wheel detent is one discrete pulse; no amount of arithmetic on the delta
|
||
recovers a continuous gesture from it. So the fix is to make position a
|
||
function of time: a detent adds to a remaining distance, and each frame
|
||
consumes a share of what is left. Roughly 120ms to land, exponential, so most
|
||
of the travel happens immediately and the tail is invisible — under a pixel
|
||
of remainder is snapped rather than approached, since every frame of it costs
|
||
a repaint.
|
||
|
||
Trackpads keep the direct path; putting an animation between the fingers and
|
||
the grid would only add lag. What separates the two is **phase**, not delta
|
||
type and not delta size — measured on this machine, a trackpad flick moves up
|
||
to ~3 lines in one event while an inched wheel moves ~0.6, so size overlaps
|
||
badly, but only a device that can gesture ever reports `Started`/`Ended`, and
|
||
a wheel is `Moved` forever on every platform. The gesture is then held open
|
||
on a 150ms idle timer rather than closed on `Ended`, because lifting the
|
||
fingers is not the end of the stream: the momentum tail keeps delivering
|
||
`Moved` events *larger* than the gesture itself, and animating those would
|
||
smooth what the system is already smoothing.
|
||
|
||
Two more things stay direct. A jump under a line reads as continuous already
|
||
— inching a wheel one detent at a time lands there — and mouse reporting and
|
||
alternate-scroll forward whole lines to the application, which cannot be
|
||
spread over frames at all.
|
||
|
||
The distance in flight is relative, not an absolute target, so output
|
||
arriving mid-scroll shifts the grid without dragging the animation somewhere
|
||
else. Everything that moves the view on its own — jumping to a prompt,
|
||
dragging a selection past the edge, clearing the scrollback, the keyboard and
|
||
mouse-reporting paths — cancels what is in flight first. (#382)
|
||
|
||
- **The GUI speaks English, Simplified Chinese and Japanese** — every string in
|
||
the interface now goes through a locale table, selected by **Settings →
|
||
General → Language**. The menu bar, command palette, switcher, settings,
|
||
SFTP panel, file tree, diff overlay, toasts, SSH prompts and confirmations are
|
||
all translated, with plural- and select-aware helpers where a count or a
|
||
gender-free choice is involved. The `zh` and `ja` key tables are exhaustive,
|
||
so adding an `L10nKey` fails the build until it is translated rather than
|
||
silently rendering English.
|
||
|
||
Two things stay in English on purpose. A theme's name is data, not chrome —
|
||
it is written into the theme YAML and matched back by suffix, so translating
|
||
it forked "Nord" into a Chinese name that then stacked a second suffix on the
|
||
next fork and stayed Chinese after switching the GUI back. And the words
|
||
Chinese developers read and speak in English anyway — hook, agent, worktree,
|
||
diff, fork, shell, pane, server — are left alone; translating them lost more
|
||
than it gained. Language names in the picker stay endonyms (English /
|
||
简体中文 / 日本語) in every locale.
|
||
(by @shihuaidexianyu in #303, by @cwatanab in #372)
|
||
|
||
- **Oh My Pi (`omp`) is a first-class agent** — the eighteenth CLI tty7
|
||
recognizes in a pane, with the full set: brand avatar, status dot, session
|
||
resume (`omp --resume <id>`), and **Fork Session** (`omp --fork <id>`).
|
||
**Settings → Agents** installs its hooks like the others.
|
||
|
||
Oh My Pi is a fork of Pi, but it is its own binary with its own
|
||
`~/.omp` config directory, so it gets its own entry rather than sharing
|
||
Pi's — a pane running `omp` was previously not detected at all, and
|
||
aliasing it onto Pi would have written the bridge to `~/.pi` and offered
|
||
`pi --session <id>` to a binary that spells it `--resume`. The status
|
||
bridge is the Pi extension the fork inherited (same default-exported
|
||
factory, same four lifecycle events), so one template now serves both,
|
||
landing at `~/.omp/agent/extensions/tty7/index.ts`.
|
||
(#405, reported by @Hanser0521 in #376)
|
||
|
||
- **The switcher is two columns, and Ctrl+Tab raises it** — it listed
|
||
workspaces only, so reaching a tab inside one meant opening the workspace
|
||
first. Now workspaces sit on the left and the tabs of whichever one the cursor
|
||
is on sit to the right, and Ctrl+Tab raises the panel as a most-recently-used
|
||
tab switcher that commits when the modifier comes up, the way IDEA does it.
|
||
|
||
Picking a workspace or a tab switches this window in place; a second window is
|
||
something you ask for, with the platform modifier or **Open in New Window**,
|
||
rather than what happens by default. New workspaces also get a codename
|
||
("amber-yak") instead of inheriting whatever directory their first shell
|
||
started in. (#380)
|
||
|
||
- **Every shell tty7 can find is in the new-terminal menu, plus one you name** —
|
||
the menu lists the shells detected on this machine (Nushell included, on every
|
||
platform) and a configurable custom entry with its own arguments, which
|
||
survive across shell-inventory rescans. (by @ArnoChenFx in #311)
|
||
|
||
- **Windows Explorer context menus** — an optional **Open in tty7** verb for a
|
||
directory background and for a folder, registered from the installer rather
|
||
than from Settings: writing shell verbs is an install-time decision, which is
|
||
where VS Code and Git for Windows put theirs. A task checkbox drives new
|
||
`--register-explorer-menu` / `--unregister-explorer-menu` flags, so the key
|
||
layout stays in `core::explorer_context_menu` instead of being copied into the
|
||
`.iss`. The uninstaller unregisters unconditionally, so an install that
|
||
registered once and was later upgraded without the box ticked does not leave
|
||
keys pointing at a deleted exe. (by @ArnoChenFx in #310, #350)
|
||
|
||
- **`tty7` opens directories in new tabs** — the CLI accepts one or more
|
||
directories and opens a tab per directory in the running GUI, restoring a
|
||
window if none is open. Paths that do not round-trip through UTF-8 are
|
||
rejected rather than opened somewhere else. (by @ArnoChenFx in #308)
|
||
|
||
- **A bell that both flashes and rings** — `BellMode` gained a `Both` variant
|
||
alongside `None`, `Visual` and `Audible`, exposed as a fourth option under
|
||
**Settings → Terminal → Bell**. Existing `none` / `visual` / `audible` config
|
||
values are unchanged. (by @shihuaidexianyu in #357)
|
||
|
||
- **Desktop notifications you can click** — a notification now carries the pane
|
||
it came from, and clicking it reveals that pane's window, tab and split.
|
||
Windows shows a WinRT toast with an `Activated` handler, macOS uses
|
||
mac-notification-sys' click response, and both route through the existing tray
|
||
dispatch channel; Linux keeps the plain notify-rust path. Titles gained
|
||
context — an agent name or the machine, then the workspace — and bodies name
|
||
the command or agent alongside the duration, all of it translated. Notification
|
||
text is sanitized on every path: it comes off the terminal, and a stray
|
||
control byte used to make the Windows toast XML fail to parse and lose the
|
||
notification outright. (by @shihuaidexianyu in #373)
|
||
|
||
- **The bright half of the ANSI palette is rescued when a theme makes it
|
||
illegible** — bright colors that fall under the contrast floor on the theme
|
||
background are lifted to it, behind a setting so a theme that means it can opt
|
||
out. (by @ArnoChenFx in #413)
|
||
|
||
### Changed
|
||
|
||
- **The About page is about the app again** — it had grown three sections that
|
||
change system state and that nobody looks for under "About": a PATH install, a
|
||
registry write, and a daemon restart. The `tty7` CLI toggle moves to
|
||
**Agents**, which already describes tty7 ↔ agent integration in the other
|
||
direction (hooks reporting session status). The Windows Explorer context menu
|
||
moves to the installer. Server restart stays — it is about the app itself.
|
||
|
||
What is left is trimmed to what it is for: the marketing paragraph goes, the
|
||
update and server explanations drop from multi-sentence accounts of internals
|
||
to one line each, the tech credits move to the bottom, and the update toggle
|
||
renders with `settings_row` like every other switch. Eight strings the page
|
||
had skipped are now localized. (#350, #358)
|
||
|
||
- **The close-confirmation setting is gone** — closing a window with live panes
|
||
always confirms. (#383)
|
||
|
||
- **The Outline right panel is gone** — with all of its wiring: the tab, the
|
||
action, the palette command, the keymap arm and four i18n keys. An existing
|
||
config value of `outline` falls back to the default Info tab, and a user
|
||
keybinding naming `ShowRightPanelOutline` degrades to a logged warning rather
|
||
than breaking the keymap. (#375, requested by @shihuaidexianyu in #374)
|
||
|
||
- **Chinese terminology, corrected throughout** — the worst one collided two
|
||
concepts: a git worktree was translated as 工作区, the same word tty7 uses for
|
||
a workspace, so "Remove Worktree" read as "delete this workspace" inside a
|
||
destructive confirmation. Worktree is 工作树 now and workspace keeps 工作区 to
|
||
itself. Also: Forget password means "clear the stored password", not password
|
||
recovery; the SSH auth mode **Agent** was 代理, the same word as the proxy
|
||
fields beside it; an SSH profile is a saved host (主机配置), not a file on
|
||
disk; scrollback was 回滚, which means rollback — the opposite direction; and
|
||
tty7's own server is now written `server`, the way `shell`, `pane` and `agent`
|
||
already were, while 服务器 stays where it means the SSH host being connected
|
||
to. (#334, #350, #417)
|
||
|
||
- **Kitty graphics frames cost one copy less** — a re-transmitting sender like
|
||
terminal-browser sends a fresh full-window frame per rendered frame, ~26 MiB
|
||
of RGBA at Retina resolution, and the client copied that buffer twice on the
|
||
way to the atlas. The decode now consumes the frame `Vec` the reader already
|
||
owns off the socket and drains the header off the front, and the uncompressed
|
||
fast path moves the pixel buffer out so the R↔B swap happens in place. On a
|
||
3216×2160 frame the decode-and-normalize step drops from ~1.78 ms to ~0.89 ms
|
||
— about 53 ms/s at 60fps. (by @ayamir in #388)
|
||
|
||
- **Powerline separators are drawn with more curve segments**, so the
|
||
half-circle caps read as circles at large font sizes.
|
||
(by @cwatanab in #335, by @ArnoChenFx in #341)
|
||
|
||
- **macOS declares its privacy intents** — tty7 shipped no
|
||
`NS*UsageDescription` keys, so macOS denied a pane's child process access to a
|
||
protected folder (Containers, Mail, Messages, Calendar) with a repeated prompt
|
||
instead of a single clear grant. The bundle now declares the folder, volume
|
||
and device usage strings its peers declare, kitty-style, and the Full Disk
|
||
Access manual grant is documented in the feature notes. No entitlement is
|
||
added: the hardened-runtime automation check runs against the process actually
|
||
sending an Apple event, which is the pane's child carrying its own signature,
|
||
so granting one would only have widened what injected code could reach.
|
||
(by @Gabyran in #323)
|
||
|
||
### Fixed
|
||
|
||
- **The ConPTY resize fix now actually engages** — the one-line opt-in that
|
||
switches Windows panes onto conhost's resize model was lost from the
|
||
working tree between verification and commit, and nothing caught it: the
|
||
field defaults to off in the vendored `alacritty_terminal`, so the build,
|
||
the test suite, and CI all stayed green while the previous nightly
|
||
shipped with the semantics half of the fix disabled and the maximize
|
||
garbling intact. The flag is back, and a regression test now pins the
|
||
wiring itself so the flag can't silently drop again. (#419)
|
||
|
||
- **Maximizing a Windows pane no longer shreds it** — two separate resize
|
||
disagreements stacked up here, and the visible one was the second.
|
||
|
||
ConPTY emits no repaint after a resize; conhost silently re-anchors its
|
||
own layout and keeps painting with absolute cursor addresses computed
|
||
against it. Measured against a live ConPTY: growing the window keeps
|
||
rows and cursor pinned and opens blank rows at the bottom — nothing is
|
||
restored from scrollback — and shrinking scrolls the last *written* row
|
||
(PSReadLine parks a continuation hint below the prompt) to the new
|
||
bottom. The grid resized the alacritty way instead: growing pulled
|
||
scrollback back into view and pushed the cursor down by that many rows.
|
||
After a maximize the two layouts disagreed by exactly the pulled row
|
||
count, so every later absolute-CUP paint — PSReadLine redraws the prompt
|
||
that way per keystroke — landed mid-screen inside the old output. The
|
||
vendored `alacritty_terminal` now has a `conpty_resize` mode that
|
||
mirrors conhost's model for the primary screen (the alternate screen
|
||
keeps stock behavior; its application repaints itself), and every pane
|
||
on Windows opts in — a shell, `wsl.exe`, and `ssh.exe` are all presented
|
||
through conhost alike. The cost is Windows-Terminal-standard behavior:
|
||
maximizing no longer reveals extra scrollback below the fold.
|
||
|
||
Separately, a resize during a burst of output reflowed the grid ahead of
|
||
the backlog: the daemon can hold up to 16 MiB of queued old-width bytes,
|
||
and the client parsed them into the already-resized grid. The daemon now
|
||
echoes a `Size` frame to the controller at the exact stream position
|
||
where the PTY changed geometry — the same geometry-tagging the reattach
|
||
replay has always used — and the client defers its reflow to that
|
||
marker, so every queued byte parses into the grid it was rendered for.
|
||
Observers, which already received live `Size` frames but only applied
|
||
them after a replay snapshot, follow the controller's resizes at the
|
||
right moment too. The client keeps the reflow-at-request-time path
|
||
against an older daemon that never echoes (new `resize-echo` feature
|
||
probe), and on remote routes, where only the local daemon's features are
|
||
known — a current remote daemon's echo lands there as a harmless
|
||
same-size no-op, and probing per connection is the follow-up that
|
||
extends deferral to remote panes. (#415)
|
||
|
||
- **The whole window no longer hangs after a remote workspace reconnects** — on
|
||
Windows, `shutdown()` does not wake a thread parked in a blocking `read()` on
|
||
the same socket the way it does on unix, and pane teardown shut the writer
|
||
down and then *joined* the reader, counting on that wake-up. When the peer
|
||
stayed silent — a routed pane whose SSH leg went zombie: nothing arrives, no
|
||
FIN ever comes — the join blocked its caller, which was the UI thread. That is
|
||
the "not responding" freeze right after a reconnect.
|
||
|
||
Teardown now sets a per-reader quit flag and abandons the thread instead of
|
||
joining it. The reader's read always times out within 500 ms so a parked
|
||
reader notices the flag promptly, and it checks the flag once per frame, so a
|
||
retired reader processes nothing at all — without that, a buffered `Exited`
|
||
arriving down an abandoned link could close the pane that had just adopted its
|
||
replacement. The reader teardown had no Windows test coverage at all before
|
||
this; the new tests block for over three seconds against the old code. (#407)
|
||
|
||
- **A Windows update that reported success and left the old build** — the
|
||
updater stopped the daemon and started the Inno installer the moment the
|
||
daemon's endpoint disappeared, but the endpoint going away is not the same
|
||
event as the images being released. The ConPTY hosts (`OpenConsole.exe`) are
|
||
the daemon's children, not the shells', so the per-pane kill never reached
|
||
them, and the daemon's `exit(0)` skipped every destructor that would have
|
||
closed them: they kept the installed `OpenConsole.exe` open for seconds after
|
||
`--stop-daemon` returned. Silent Setup then hit the lock, took the suppressed
|
||
dialog's default (Abort), and the recovery relaunched the old build. A daemon
|
||
that died without cleaning up made it permanent — its orphaned hosts survive
|
||
indefinitely, which is the `DeleteFile` code 5 users hit even after "closing
|
||
everything".
|
||
|
||
Both shapes were reproduced in isolation first: with a pane open,
|
||
`--stop-daemon` returned about a second in while `OpenConsole.exe` stayed
|
||
locked for another 1.4 s; after killing the daemon outright, the orphaned host
|
||
held the lock forever. The shutdown now finishes what it starts at every layer
|
||
that can be the last one standing — the daemon reaps its descendants and waits
|
||
for them while the endpoint is still up, `stop()` waits for the recorded pid to
|
||
actually exit rather than merely stop listening, and
|
||
`--stop-daemon --update-install-dir` also terminates anything still running
|
||
from the installation directory and only returns once the `.exe`/`.dll` images
|
||
there open for writing, naming the holdouts if they never do. The updater runs
|
||
that clearing itself before invoking Setup, so a directory that cannot be
|
||
cleared fails with a cause in `update.log` instead of Inno's bare
|
||
"DeleteFile failed; code 5".
|
||
|
||
Three smaller holes closed with it: the macOS updater waits for its parent by
|
||
watching `getppid()` reparent to launchd rather than polling `kill(pid, 0)`,
|
||
which a recycled pid could satisfy forever; a Windows update guard makes
|
||
`ensure_running` refuse to spawn a daemon mid-install, so a `tty7` CLI call
|
||
cannot relock the images Setup is replacing; and a Windows portable backup
|
||
carries an incomplete marker from the first file move until the replacement
|
||
lands, so an interrupted update is reported at the next launch instead of
|
||
leaving a silently mixed installation. (#403)
|
||
|
||
- **A newly installed command works in the next pane** — Windows hands every
|
||
process a private copy of the environment block at `CreateProcess` time and
|
||
never updates it, so a daemon that had been up since before an installer
|
||
edited `HKCU\Environment` gave a brand-new pane its startup `PATH` and the
|
||
freshly installed command was unresolvable until tty7 restarted. Windows
|
||
Terminal launched from Explorer found it, because Explorer rebuilds its own
|
||
block when it sees `WM_SETTINGCHANGE`.
|
||
|
||
Rather than chase broadcast messages, the daemon re-reads the two hives
|
||
Windows itself composes an environment from — machine
|
||
`Session Manager\Environment` and `HKCU\Environment` — at the moment a pane is
|
||
spawned. The merge is a pure function over (machine, user, process, configured
|
||
overrides), so the semantics are unit-testable without a registry: names are
|
||
keyed case-insensitively so `Path` and `PATH` collapse into one variable;
|
||
`PATH` and `PSModulePath` are *combined*, machine first, which is what Windows
|
||
does and what keeps a per-user install from shadowing the system half;
|
||
`REG_EXPAND_SZ` values are expanded against the merged map with a depth bound;
|
||
the machine hive's `USERNAME=SYSTEM` is dropped as Windows drops it; and
|
||
directories the daemon's own `PATH` held that neither hive lists stay
|
||
reachable, appended behind the registry entries — freshening `PATH` should
|
||
only ever add resolvable commands. (#349, reported by @yhzhu99 in #333)
|
||
|
||
- **Windows panes can answer a background-color query** — the in-box conhost
|
||
swallows a pane process's OSC 11 query, so it never reached tty7's emulator
|
||
and no reply was written back: applications that pick a light or dark UI from
|
||
the terminal background rendered a dark UI under a light theme. tty7 already
|
||
answers OSC 10/11/12 from the live theme, so nothing was missing but a
|
||
pseudoconsole that forwards the question. Microsoft ships one as a
|
||
redistributable and `portable-pty` already prefers a sideloaded `conpty.dll`
|
||
over kernel32's, so this is packaging rather than code: the pair goes beside
|
||
`tty7-app.exe`.
|
||
|
||
Measured on Windows 11 26200, same binary, only the pair added beside it: with
|
||
the in-box host the client times out with no reply; with the bundled ConPTY a
|
||
real pane reads back `rgb:efef/f1f1/f5f5` under `catppuccin_latte`, which is
|
||
that preset's exact background. The two files are one supported unit, so the
|
||
release verifier fails a package carrying only one, a mismatched pair, or a
|
||
DLL without its MIT notice, and `build.rs` stages them beside cargo's output
|
||
so a development build does not quietly run on the in-box host.
|
||
(#360, reported by @yhzhu99 in #345)
|
||
|
||
- **A second cursor no longer blinks in the wrong place on Windows** —
|
||
conhost's VT renderer brackets every frame it paints with `?25l` … `?25h` so
|
||
the cursor does not flicker across the repaint, and it moves the cursor
|
||
explicitly just before the show only on the frames where it painted the
|
||
cursor. On the others the show commits wherever the last erase or write left
|
||
it, and the cursor blinks there until conhost's next frame moves it back. tty7
|
||
repaints when a batch of pty output lands, so it draws that cursor for a frame
|
||
— and a TUI that repaints on a spinner produces one every tick.
|
||
|
||
Measured on Windows 11 26200 from a raw ConPTY capture of a Codex session at
|
||
110×30: 295 ms of cursor-visible dwell across 42 frames parked at the end of
|
||
the status line, each stray corrected 7–15 ms later by the following frame.
|
||
A scanner now pairs each hide with its show and marks the show as *parked*
|
||
when the run in between moved the cursor around to paint but did not end on a
|
||
move — nothing chose the cell it is about to appear on — and restores the cell
|
||
the cursor stood on when it went invisible, which is where the correcting
|
||
frame would have put it anyway. A hide and a show more than 100 ms apart are
|
||
an application keeping the cursor off for the length of some work, not a
|
||
renderer bracketing one frame, and are left alone. This is inert where the
|
||
bundled ConPTY redistributable is in play; it earns its keep on hosts without
|
||
it, notably a remote Windows server. (#362)
|
||
|
||
- **Windows toasts are tty7's, not PowerShell's** — notifications showed the
|
||
PowerShell icon and name because tty7 had no AUMID of its own registered. It
|
||
now brands the process and refreshes the single per-user `tty7.lnk` Inno's
|
||
default install owns — but only when that shortcut is not already ours, never
|
||
from a cargo build directory, and never as a second per-user copy beside an
|
||
all-users install, all three of which the first cut got wrong on a real
|
||
machine. The shell indexes a new `.lnk` asynchronously and `Toast::show()`
|
||
reports success while dropping the toast for an AUMID it has not seen yet, so
|
||
toasts keep the PowerShell identity for half a minute after tty7 writes one:
|
||
ugly beats invisible. (by @shihuaidexianyu in #340, reported in #339)
|
||
|
||
- **Windows panes are told whether the window is light or dark** — the daemon
|
||
needs to know when it spawns a pane, because ConPTY drops the child's OSC 11
|
||
query before tty7's emulator can answer it. It was reading that from a dead
|
||
`Config::theme` field, which meant the GUI had to rewrite the user's
|
||
`config.json` every time the effective preset changed sides. The hint moves to
|
||
its own `appearance.json` in the data dir: derived state, written by the
|
||
process that paints the window and read by the one that has to describe it.
|
||
Absent, unreadable and unparsable all read as light — what the default preset
|
||
is — so a daemon that starts before the GUI has ever applied a theme describes
|
||
the default window instead of guessing. (by @yhzhu99 in #332)
|
||
|
||
- **Scoop shims work again when tty7 is launched from a hardened Windows
|
||
shell broker** — some brokers enforce `ProcessRedirectionTrustPolicy` on
|
||
what they start. The daemon inherited it, every ConPTY shell under the
|
||
daemon inherited it in turn, and PowerShell could then no longer traverse
|
||
a user-created junction — which is exactly what Scoop's `current` links
|
||
are. `oh-my-posh` and `fzf` died with `Shim: Could not determine if target
|
||
is a GUI app`. Windows Terminal was unaffected because its process tree
|
||
never picked the policy up in the first place.
|
||
|
||
The policy cannot be relaxed once it is on, so the fix is to not inherit
|
||
it: when tty7 detects the enforcing bit, it creates the daemon with
|
||
`PROC_THREAD_ATTRIBUTE_PARENT_PROCESS` naming the interactive desktop
|
||
shell, which supplies the ordinary desktop token, device map, and
|
||
mitigation policy. Everything else about the spawn is unchanged, and the
|
||
ordinary path still runs whenever the policy is absent — or whenever the
|
||
desktop shell cannot be borrowed, in which case tty7 logs a warning and
|
||
starts degraded rather than not starting at all. Note the daemon's token on
|
||
the clean-parent path derives from Explorer, so an elevated tty7 starts a
|
||
medium-integrity daemon. (by @ArnoChenFx in #292)
|
||
|
||
- **A pane's last line of output no longer loses the race with its exit on
|
||
Windows** — the process-exit monitor could observe a short-lived shell
|
||
exiting before the ConPTY reader had delivered its final frame, so
|
||
`Exited` reached clients ahead of the output that preceded it. The
|
||
monitor now releases the pseudoconsole and lets the reader, which reports
|
||
only after it has forwarded everything up to EOF, announce the death. A
|
||
bounded window behind it still covers the case where EOF never arrives —
|
||
a grandchild holding the ConPTY output pipe open keeps the shell's own
|
||
exit from ever closing it. (by @ArnoChenFx in #292)
|
||
|
||
- **A resumed agent keeps the flags it was launched with, on Windows** — on
|
||
macOS and Linux the daemon reads the foreground process's real argv, so
|
||
`resume_command` can replay a launch flag like
|
||
`--dangerously-skip-permissions`. The Windows path detects the agent from the
|
||
OSC 133;C command capture but threw the command line away, stamping an empty
|
||
argv, so every resumed agent came back bare. The captured command is now
|
||
tokenized (case preserved, quotes trimmed, the PowerShell call operator
|
||
dropped) and stamped as the pane's launch argv; fork commands share that
|
||
source and are fixed by the same change. (#406)
|
||
|
||
- **Codex hook commands work under PowerShell on Windows** — the installed hook
|
||
command is written as a PATH-resolvable bare name rather than a path
|
||
PowerShell would not run. (by @shihuaidexianyu in #298)
|
||
|
||
- **Switching workspaces no longer destroys live sessions** — it rebuilt every
|
||
pane it was asked to restore, and a window that rebuilt nothing then deleted
|
||
the workspace outright, tree and store both. Three separate guesses, each one
|
||
authorizing an irreversible act.
|
||
|
||
`session_from_tree` erased a pane's id when the tree said `live: false`. That
|
||
flag is a cached observation from another process, reloaded as false on every
|
||
server start, so a quiet pane read as dead while its shell was running: the
|
||
restore had nothing to attach to and spawned a fresh shell over it. Two
|
||
servers could also start against one config dir — `run_with` decided another
|
||
server was dead by failing to connect once, then unlinked its socket and bound
|
||
its own, and the loser kept `control.sock` with an empty pane registry, so
|
||
`MachineGet` reported every pane dead and nothing logged an error. And
|
||
`finish_hydration` marked a window informed before the rebuild and without
|
||
looking at the result, while `tabs_from_session` drops any tab whose panes all
|
||
fail to start — which is every tab when the pane socket is unreachable —
|
||
leaving a window that was empty and authoritative at once, so the next switch
|
||
deleted a workspace with ten live tabs.
|
||
|
||
Each is now settled by whoever holds the truth: attaching decides whether a
|
||
pane is there, an advisory lock decides which process is the server, and a
|
||
deletion needs the machine's own mirror to agree that the workspace is empty.
|
||
A corrupt `views.json` is quarantined the way `machine.json` already was,
|
||
rather than logged and overwritten by the next save. (#410)
|
||
|
||
- **Tab in a WSL pane completes paths again** — the pane's filesystem is
|
||
foreign, so the completion engine had no cwd to list and handed every Tab back
|
||
to bash. A WSL pane's POSIX cwd (OSC 7) is now translated to the distro's
|
||
`\\wsl$` share, which this process can read like any directory, while
|
||
everything that would consult *this* machine stays switched off: no `PATH`
|
||
binaries in the command position, no generator scripts, and `~` is left to the
|
||
shell, since it names the distro's home. Absolute words stay inside the share,
|
||
so `ls /etc<Tab>` lists the distro's `/etc` and not `C:\etc`. A `wsl.exe` pane
|
||
spawned without `--distribution` resolves the default distro from the registry
|
||
rather than carrying an empty placeholder.
|
||
|
||
The follow-up fixed the gate that had made this unreachable for the case it
|
||
was written for: a pane in a WSL *workspace* is served by the daemon inside
|
||
the distro, so its host id is never `LOCAL`, and an early locality check threw
|
||
those panes away. The check belongs only to the `wsl.exe`-in-a-local-workspace
|
||
branch — a remote host's WSL pane must not read a same-named share here —
|
||
while a WSL workspace is this computer's distro by construction. (#408, #418)
|
||
|
||
- **A WSL machine can restart and replace its server** — the machine menu
|
||
offered "restart server" only when the target was SSH, so a distro's row had
|
||
just "new workspace" and "disconnect", and the router refused the action for
|
||
anything else even though `restart_wsl_daemon` had been sitting unused since it
|
||
was written. A distro's server is installed and launched from this computer
|
||
exactly like an SSH one; only the transport differs. Both actions now have WSL
|
||
arms, so the mismatch dialog's **Update Server** is no longer a dead end
|
||
there. A `--stdio` workspace still has nothing to restart: it is whatever
|
||
program the user named. (#417)
|
||
|
||
- **The mismatch dialog replaces the server binary instead of just restarting
|
||
it** — the old **Restart Server** button restarted the same incompatible
|
||
daemon, leaving the user on the same mismatch afterwards. It now replaces the
|
||
binary and then restarts, and says so: **Update Server**.
|
||
(by @shihuaidexianyu in #352, reported in #351)
|
||
|
||
- **A version-mismatched remote server is described as old, not as
|
||
not-tty7** — a handshake refused on the control dialect was shown with the
|
||
protocol layer's own wording: "java answered, but not as a tty7 server:
|
||
control peer (build …) speaks control v4, this build speaks v5". The far end
|
||
*is* tty7; it is a build on the other side of a dialect bump, and the reader
|
||
cannot act on the dialect numbers either way. The message now states which
|
||
side is behind, and the button names the action. The failure that followed was
|
||
also invisible: the switcher paints a failed `connect` in preference to stored
|
||
host errors, and restarting or replacing a server cleared only the latter, so
|
||
whatever went wrong during the install was covered by the complaint that
|
||
started it. (#384)
|
||
|
||
- **Remote server errors appear under the machine they came from** — restart and
|
||
replace failures were reported through a global modal, which mixed errors from
|
||
different machines together and blocked the UI. They are now stored per host
|
||
and surfaced in that host's switcher group, which expands to show them, with a
|
||
Dismiss button scoped to its own host and a clear on the next connect so a
|
||
later success does not leave a stale failure on screen. A failure raised where
|
||
there is no switcher to put it in — the window menu's restart command, or a
|
||
mismatch hit mid-connect — still falls back to the modal.
|
||
(by @shihuaidexianyu in #354)
|
||
|
||
- **An SSH remote install uses the bundled server before reaching for the
|
||
network** — it only ever checked the release download path and ignored the
|
||
server binary already shipped beside the Windows executable, which WSL was
|
||
using. The bundled binary is now auto-discovered and the GitHub release
|
||
download is the fallback for when no matching local asset exists. The explicit
|
||
path keeps its strict no-fallback behavior, and WSL remains bundled-only.
|
||
(by @shihuaidexianyu in #344)
|
||
|
||
- **A server that is only a control dialect behind is noticed at launch** — the
|
||
pane protocol and the control dialect are versioned apart and the launch check
|
||
only compared the first, so a server from before the v4-to-v5 control bump
|
||
answered the pane handshake with this build's own number and was waved through
|
||
as ours while every machine-tree call was refused: the window opened with no
|
||
tabs and the only trace was a log line the default config does not write
|
||
anywhere. The control socket is asked too now, and the restart prompt says
|
||
which version disagrees and picks its wording by which side is ahead, rather
|
||
than calling a newer server old. A window that still opens empty says why —
|
||
once per window, latched, since a failed pull is retried from every
|
||
fifteen-second sync. (#387)
|
||
|
||
- **"Keep Shells" is no longer offered beside a mismatched server** — both
|
||
handshakes compare their version for equality and hang up on anything else, so
|
||
a server whose number disagrees cannot be talked round, and that button
|
||
offered a state that does not work: panes still spawn while every machine-tree
|
||
call is refused, which is how a window opens with no tabs and saves none of
|
||
the ones you make. The choice is now restart or quit. Quit goes first, because
|
||
it destroys nothing — the server and every shell under it keep running — and
|
||
because this prompt arrives unasked at launch, the moment a stray Return is
|
||
likeliest. A prompt dismissed without an answer re-arms instead of falling
|
||
through. (#389)
|
||
|
||
- **Pasting a screenshot into a remote pane works** — the clipboard-image paste
|
||
that stages a file and hands the agent its path was built off macOS only,
|
||
because a local macOS agent reads the system clipboard itself when it sees
|
||
Ctrl+V and that carries the image at full fidelity. The reasoning stops at the
|
||
pane boundary: an agent in an SSH pane or a remote workspace reads the
|
||
clipboard of the host *it* runs on, which never holds this machine's
|
||
screenshot, so the paste did nothing at all. Remote panes now stage and upload
|
||
on every platform, while a local macOS pane keeps its direct path untouched.
|
||
macOS screenshots arrive as TIFF, which agent vision rejects the same way it
|
||
rejects a Windows BMP, so those transcode to PNG on the way out.
|
||
|
||
Staging is hardened on the remote side. `/tmp/tty7-clipboard-<user>` is a
|
||
predictable name in a world-writable directory: any local account on the
|
||
remote host could pre-create it, and the `Mkdir` result was discarded, so tty7
|
||
would have uploaded into a directory someone else owned — readable by them,
|
||
and swappable for another image before the pane's agent opened the path.
|
||
Images now stage in `$HOME/.cache/tty7/clipboard` resolved from the session's
|
||
own `realpath .`, and the directory is verified before anything goes into it:
|
||
a symlink is refused outright, a `chmod 0700` the daemon watched succeed is
|
||
the ownership proof, and a following `stat` must report exactly `0700`. Any
|
||
doubt is a hard failure that falls back to pasting the local path. The whole
|
||
path also moves off the UI thread, which is what lets a failed upload notify
|
||
once, naming the host and the reason, instead of leaving a dangling remote
|
||
path in the line and saying nothing.
|
||
(by @shihuaidexianyu in #338, reported in #337; #399)
|
||
|
||
- **A WSL pane is handed a path it can actually open** — the same paste staged
|
||
its file on the Windows side and pasted the Windows name for it, so an agent in
|
||
WSL got `C:\Users\…\paste-1.png` and found nothing there. A WSL pane shares
|
||
this machine's disk but not its path syntax, so there is nothing to upload and
|
||
only a name to rewrite: the paste now carries the automount view,
|
||
`/mnt/c/Users/…`. A path with no mapping — a UNC temp directory — keeps the
|
||
Windows name, which at least says where the file went. (#399)
|
||
|
||
- **Arrow keys reach ncurses applications in the form they asked for** — Arrow,
|
||
Home and End were always sent as their CSI form, no matter what the foreground
|
||
program had requested. Programs that turn on DECCKM via `smkx` — which is
|
||
every ncurses full-screen app — expect the SS3 form, because that is what
|
||
`xterm-256color` spells `kcuu1` and friends as, and ncurses matches terminfo
|
||
byte for byte. htop was the report: ncurses failed to match `\E[A`, handed the
|
||
bytes to htop one at a time, and htop binds `[` to "lower priority", so every
|
||
Up or Down bumped the selected process's nice value instead of moving the
|
||
selection. The same breakage hit ncdu, mc, dialog, menuconfig and nmtui;
|
||
shells were unaffected because readline and zle bind both forms.
|
||
|
||
Named keys now also carry their modifiers the way terminfo declares them
|
||
(`kLFT=\E[1;2D`, `kUP5=\E[1;5A`, `kDC3=\E[3;3~`) instead of dropping
|
||
Shift/Ctrl entirely and prefixing Alt with a bare ESC. Cmd stays out of the
|
||
modifier parameter — xterm has no encoding for it.
|
||
(#366, reported by @hak0 in #361)
|
||
|
||
- **A right-click goes to the application when mouse reporting is on** — a TUI
|
||
that turns mouse reporting on (vim with `set mouse=a`, lazygit, tmux) draws its
|
||
own right-button menus, and tty7 was delivering one right-click to both
|
||
consumers: the press was forwarded to the application *and* tty7's own context
|
||
menu popped over the top of it. One pure predicate now decides, called from
|
||
both sides, so one click can only ever feed one consumer; Shift stays the
|
||
escape hatch that reaches tty7, the same override it already provides for
|
||
selection and for the wheel. (#347)
|
||
|
||
- **`$SHELL` names the shell the pane actually runs** — a pane configured to run
|
||
fish still advertised the login shell, because the pane environment injected
|
||
`TERM`, the `TTY7_*` markers and `TERM_PROGRAM` but never touched `SHELL`, so
|
||
the pane inherited the GUI session's login-time snapshot. Everything that
|
||
spawns "the user's shell" read that: tmux's `default-shell` started zsh inside
|
||
a fish pane, and so did `sudo -s`, an editor's shell escape, and any coding
|
||
agent picking a quoting dialect from `$SHELL`. The failure is silent — fish
|
||
rejects the bash line, the agent's sentinel file never appears, and the
|
||
rejected text stays in the line editor to concatenate onto the next send.
|
||
|
||
`SHELL` is now injected alongside the other markers, set to the absolute path
|
||
of the program the pane is about to exec, read off argv rather than off the
|
||
shell tty7 resolved — an argv-replacing integration injection and the
|
||
parent-shell override both rewrite argv. Only an absolute path is ever
|
||
written: a configured command may be bare so `PATH` decides which install
|
||
wins, so a bare name is resolved against the `PATH` the pane will inherit and
|
||
skipped when that finds nothing. An explicit `SHELL` in the user's env block
|
||
still wins. Windows is deliberately left out — neither cmd nor PowerShell reads
|
||
`SHELL`, and the POSIX emulations that do want a POSIX path.
|
||
(#348, reported by @nohzafk in #342)
|
||
|
||
- **The login shell is read from passwd, not from a stale `$SHELL`** — `$SHELL`
|
||
is a snapshot the session inherits at login, so `chsh` never moves it: a GUI
|
||
launch kept reporting the shell that was current when the user logged in, and
|
||
the window's shell menu marked the wrong entry "default" for that whole
|
||
stretch. The passwd entry is read instead, via the reentrant `getpwuid_r`,
|
||
with `$SHELL` as the fallback. The same change fixes who wins a name in the
|
||
menu: `$PATH` is probed before `/etc/shells`, so on a machine with a Homebrew
|
||
bash the menu's "bash" is the binary typing `bash` would reach rather than
|
||
macOS's 3.2 from 2007. (#278)
|
||
|
||
- **A forwarding `.bash_profile` no longer sources `.bashrc` twice** — the
|
||
rcfile tty7 hands bash replays the login-shell startup chain, but then sourced
|
||
`~/.bashrc` unconditionally afterwards. A login shell never does that on its
|
||
own: `~/.bashrc` arrives only because the profile that won the chain forwarded
|
||
to it, which is how nearly every `~/.bash_profile` is written. The result was
|
||
the user's whole `~/.bashrc` running twice per pane — banners printed twice,
|
||
completions sourced twice, appends to `PROMPT_COMMAND` stacking up.
|
||
`~/.bashrc` joins the same first-match-wins chain, which keeps the fallback
|
||
for a `$HOME` with no profile at all. (#279)
|
||
|
||
- **The macOS locale fallback sets `LANG`, not `LC_CTYPE`** — when a pane
|
||
inherits no locale at all, the usual case for a GUI-launched process on macOS,
|
||
tty7 derives an installed UTF-8 locale and injects it — but as `LC_CTYPE`,
|
||
which backs only character handling. Collation, time and numbers stayed at C,
|
||
and bash warned `setlocale: LC_COLLATE: cannot change locale ()` once per
|
||
category on every launch; zsh and fish swallowed the failure and merely looked
|
||
fine while being just as half-configured. `LANG` backs every category and
|
||
still loses to any `LC_*` the user's own rc files set afterwards, which is
|
||
what a fallback should do. (#280)
|
||
|
||
- **Three defects in the inline completion menu** — a candidate was inserted
|
||
verbatim, so a directory named `My Documents` completed to `cd My Documents/`,
|
||
which the shell resplits into two arguments; candidates are escaped now, with
|
||
a leading `~/` left alone since that prefix is the user's own text and escaping
|
||
it would stop the home expansion it was typed for. The same escape decides
|
||
whether a common-prefix step is safe to write — the prefix shared by
|
||
`My Documents` and `My Music` is `My `, and writing it raw both broke the line
|
||
and closed the menu on the next keystroke — so a prefix that needs escaping
|
||
steps through the candidates instead. A menu fed only by generators stayed
|
||
armed forever when nothing matched, swallowing every later Tab instead of
|
||
handing the line to the shell; sessions now count their generators and the
|
||
last one to answer closes a menu that still has nothing in it. And command
|
||
completion scanned *this* machine's `PATH` in a remote pane, so
|
||
`system_prof<Tab>` over SSH to Linux offered macOS's `system_profiler` —
|
||
builtins are true on any POSIX shell and still go out, the `PATH` scan is now
|
||
local-only. (#276)
|
||
|
||
- **macOS editing shortcuts reach a foreground TUI** — Cmd+Backspace and the
|
||
rest of the line-editing chord set are forwarded to the application instead of
|
||
being swallowed. (by @loscoy in #304)
|
||
|
||
- **Ctrl-E accepts a ghost suggestion on the first try** — it accepts a visible
|
||
inline history suggestion while keeping end-of-line behavior when none is
|
||
shown. (by @vkingw in #329, reported in #315)
|
||
|
||
- **No stray `^U` after interrupting an agent** — typeahead captured across an
|
||
alt-screen boundary was replayed into the line editor after the interrupt.
|
||
(by @shihuaidexianyu in #312, reported by @fuchen in #305)
|
||
|
||
- **The editor is restored after an interrupted tab handoff.** (#290)
|
||
|
||
- **ANSI dim text and text decorations render** — underline, double underline,
|
||
curly, dotted, dashed, overline and strikethrough, and SGR 2 dim.
|
||
(#288, #289)
|
||
|
||
- **Powerline separators have no seam between them** — adjacent separators left
|
||
a hairline of background between the two glyphs. A cover quad closes it, and
|
||
is skipped when the glyph is dim: the cover and the anti-aliased path overlap
|
||
on the closing edge's device pixel, which is a no-op for an opaque foreground
|
||
but pushes a dim cell's alpha to 0.884 and tints the neighboring cell's
|
||
background. (by @ArnoChenFx in #336)
|
||
|
||
- **Tokens that bypassed the contrast machinery now have a floor** — sidebar
|
||
text, the caret and hairlines were flat blends with no floor, sitting one line
|
||
away from tokens that are bisected to hit a target exactly, and semantic inks
|
||
were floored against the window background but painted on popovers and sidebar
|
||
rows, which sit a step toward the foreground. `sidebar_fg` is floored at 4.5:1
|
||
on the sidebar fill it is actually painted on — four builtins landed at
|
||
3.35–3.92:1 — the caret is conditioned to 3:1, where the default Light theme
|
||
shipped an orange caret on pure white at 2.07:1, `border` keeps its blend but
|
||
gains a 1.5:1 floor so a divider is worth the same in every theme, and
|
||
semantic inks clear their floor on background, sidebar and popover alike.
|
||
Themes that already cleared a floor are untouched. (#400)
|
||
|
||
- **Smooth scroll steps are aligned with presented frames**, and a queued scroll
|
||
animation wakes the window. (by @ArnoChenFx in #414)
|
||
|
||
- **Kitty graphics frames cannot pile up faster than they decode** — the
|
||
unbounded decode channel becomes a bounded latest-frame inbox, so a
|
||
full-window sender cannot queue frames faster than they are consumed, and
|
||
superseded frames are discarded before being decoded. Only images that reached
|
||
the sprite atlas are retired, and remaining atlas entries are evicted when a
|
||
pane closes, which keeps hidden terminal-browser tabs and repeated pane
|
||
lifecycles from retaining one decoded frame per repaint.
|
||
|
||
- **The terminal's own context menu and search bar are translated** — the
|
||
context menu was the last surface still speaking hardcoded English in a
|
||
three-locale app, 14 literals of it, and the search bar had three more with
|
||
its Previous / Next / Close buttons icon-only and untooltipped. It also spoke
|
||
a fourth vocabulary for one action: "Maximize Pane" for what the menu bar, the
|
||
palette and Keybindings all call "Zoom Pane", and "Close Pane" for what is
|
||
"Close Pane / Tab" everywhere else — which is the accurate name, since the
|
||
action closes the tab when the pane is the last one. (#401)
|
||
|
||
- **A new tab lands in its repo group on the first frame** instead of appearing
|
||
ungrouped for one paint. (by @ArnoChenFx in #411)
|
||
|
||
- **Settings hover highlights respond immediately**, with a stable row identity
|
||
so the highlight does not jump between rows. (by @ArnoChenFx in #313)
|
||
|
||
- **Overlay scrollbars are softer.** (#293)
|
||
|
||
- **`tty7 send` submits its Enter outside a paste burst**, so an agent does not
|
||
read the newline as part of pasted text. (#322)
|
||
|
||
- **Closing a tab from the CLI terminates its panes.** (#319)
|
||
|
||
- **`tty7` diagnoses an unavailable agent hook** instead of failing silently.
|
||
(#321)
|
||
|
||
- **tty7 no longer panics on launch under Wayland on a slow VM** — the
|
||
xdg-desktop-portal event source notified windows of the initial color-scheme
|
||
and button-layout replies while still holding `client.borrow_mut()`, and those
|
||
callbacks re-enter GPUI and reach the same `RefCell`. Whether it fires depends
|
||
on whether the portal reply beats window creation, so a VMware Ubuntu guest
|
||
lost that race every time. Fixed in the gpui fork for both the Wayland and X11
|
||
clients by collecting the window pointers and dropping the borrow before
|
||
notifying.
|
||
|
||
- **The Windows package verifier can read the Inno payload again** — the bundle
|
||
script deleted the staging directory as its last act, and the verifier reads
|
||
that directory to check what lands in `{app}`, since a compiled `setup.exe`
|
||
cannot be read back without innoextract. Every Windows build had failed the
|
||
check since it arrived, taking nightly red for two nights, and a stable
|
||
release would have failed the same way. Both workflows already expected the
|
||
directory to survive — their upload steps name it among the intermediates the
|
||
asset globs deliberately skip — so the removal goes rather than the check.
|
||
(#368)
|
||
|
||
## [26.8.1] - 2026-08-01
|
||
|
||
### Added
|
||
|
||
- **A remote workspace is a window that is one machine, not a pane that
|
||
happens to be SSH'd somewhere** — pick a host from **Home → Connect to
|
||
Host** (or the workspace switcher) and tty7 opens a window bound to that
|
||
machine: its own tab and pane tree, file browser and editor, repo grouping,
|
||
branch line, Changes panel, diff overlay and worktree list, all answered by
|
||
*that* machine rather than read off the client's disk. Before this, reaching
|
||
a remote box meant an SSH pane from the command palette — a shell and
|
||
nothing else. Repo grouping, the diff overlay, the right-panel Changes list,
|
||
worktrees: all of it read the client's local disk, so none of it existed
|
||
over SSH. The file tree was SFTP browsing, a separate, lesser thing from the
|
||
local editor. And the moment you closed the lid or lost the network, the
|
||
agent running in that pane died with the connection — there was nothing on
|
||
the far side to keep it alive.
|
||
|
||
A remote workspace fixes all four at once by putting the same code on both
|
||
ends. `tty7-server` — a new headless binary built on `tty7-core`, the crate
|
||
the GUI's own daemon logic was extracted into — runs on the far machine and
|
||
serves the identical `LocalHost` implementation the GUI uses for your own
|
||
disk, over one control connection. Because both sides run the *same* code
|
||
rather than a client-only reimplementation, a remote workspace has the exact
|
||
feature set a local one does, not a cut-down version of it. Sessions live in
|
||
that server, not in the window: closing the window detaches rather than
|
||
kills, panes and agent conversations keep running on the far side, and
|
||
reconnecting — from this laptop or another one — lands back where you left
|
||
it.
|
||
|
||
Local and remote workspaces are two different features, not one with a flag:
|
||
a window is never a mix of an SSH pane and a real workspace, and that split
|
||
is enforced rather than left to convention. The install itself is one-time
|
||
and unprivileged — the server is a static binary pushed over the same
|
||
connection, no `sudo`, confirmed once per machine. This is the foundation
|
||
the WSL-machines, incremental git-streaming and per-machine `⌃R` history
|
||
entries below build on: none of them had anywhere to attach to before this
|
||
landed. (#235)
|
||
|
||
- **`tty7` is now a CLI, not just the app you launch by clicking an icon** —
|
||
a new scriptable command-line client, built for coding agents to drive
|
||
panes without a window in the way. The GUI binary is renamed `tty7-app`
|
||
(bundle id, icons and display name unchanged); `tty7`, the name you type,
|
||
is now the CLI. Both are clients of the same `tty7-server` a remote
|
||
workspace runs, talking the same control and pane protocols the window
|
||
does — an agent reaching through the CLI can do exactly what the CLI
|
||
exposes, no more, no less.
|
||
|
||
| | |
|
||
|---|---|
|
||
| Hot path | `ls`, `run -- <cmd>` (real exit code, `--keep` keeps the pane around), `new`, `send`, `capture`, `split`, `procs` |
|
||
| Addressing | `ws` / `tab` (`@N`) / `pane` (`%N`) / `machine` / `server` |
|
||
| Introspection | `agents`, `events` (NDJSON), `status`, `doctor` |
|
||
| Global | `-m <machine>` routes over the server's existing SSH/WSL links; `--json` everywhere; implicit context from `TTY7_PANE` / `TTY7_WS` / `TTY7_SOCKET`, injected into every spawned shell |
|
||
|
||
The server's control dialect grew several additive verbs to carry this: a
|
||
read-only `Observe` for watching a pane without owning it, budgeted per
|
||
observer; pane exit codes, which previously were always reported `None`
|
||
and now actually carry the code; and aggregate `AgentStates` / `Routes` /
|
||
`Status` queries. Writing into a pane now hands off cleanly instead of
|
||
fighting the previous controller for it — a displaced client gets a clean
|
||
EOF and its stale input is dropped, closing a long-standing hazard where
|
||
two writers on one pane could leave it looking frozen. `tty7_core::client`
|
||
exposes the `ControlClient`/`PaneClient` pair the CLI is built on as a
|
||
public library, with zero behavior change to the GUI's own connection code.
|
||
|
||
There is deliberately no interactive `attach` verb — agents drive panes
|
||
with `run` / `send` / `capture` / `events` instead of taking one over
|
||
interactively. An `attach` verb was designed and partway built during this
|
||
work and then dropped before it ever reached a release, so this is design
|
||
history, not a behavior change for anyone — the CLI itself is new in this
|
||
release, so there was no previous `tty7 attach` to remove out from under
|
||
existing scripts. The protocol-level `Attach` call the GUI's own panes use
|
||
is untouched.
|
||
|
||
Every unimplemented verb (`ws stop`, `machine connect`/`disconnect`, bare
|
||
`tty7` launching the GUI) says so in `--help` rather than only failing at
|
||
runtime. Along the way, the GUI's own copy was reworded for the same two
|
||
concepts the CLI now names — "server" for the background process, "shell"
|
||
for what runs in a pane — replacing inconsistent uses of "daemon" and
|
||
"session" in the tray, palette and confirmation dialogs (no behavior
|
||
change, wording only). (#274)
|
||
|
||
- **The CLI ships in every install and lands on PATH automatically** —
|
||
previously it was built by every release and then discarded: the bundle
|
||
scripts copied only `tty7-app`, and the upload glob never reached the CLI
|
||
binary. It now ships inside the macOS `.app`, the Linux tarball and
|
||
AppImage, and the Windows installer and zip, and a fresh launch puts it
|
||
where a shell can find it without you doing anything.
|
||
|
||
The install is split into two halves on purpose. The **environment half**
|
||
prepends the CLI's directory to the GUI process's own PATH before the
|
||
server is spawned, so every pane — which inherits its environment from the
|
||
server, which inherits it from the GUI — can run `tty7` immediately. It
|
||
writes nothing to disk and cannot fail. The **on-disk half** covers typing
|
||
`tty7` in some other terminal, and is allowed to fail safely: on Unix it
|
||
symlinks into a fixed, ordered list of candidate directories
|
||
(`/opt/homebrew/bin`, `/usr/local/bin`, `~/.local/bin`, `~/bin`,
|
||
`~/.cargo/bin`) rather than scanning PATH for the first writable entry —
|
||
version-manager shim directories (pyenv, rbenv, asdf, mise) sit at the
|
||
front of many people's PATH and are writable, and a binary dropped there
|
||
survives only until that tool next rehashes and silently deletes it. On
|
||
Windows it appends the app's directory to `HKCU\Environment` through the
|
||
registry API rather than `setx`, which truncates and corrupts anything
|
||
over 1024 characters, and reads/writes the value as UTF-16 end to end so a
|
||
PATH entry Rust can't represent as a `String` survives untouched.
|
||
|
||
It is deliberately conservative about what it touches: only a symlink (or,
|
||
on the AppImage, a copy) this installer itself created is ever replaced —
|
||
marked as ours with a small marker file so a user who moves from the
|
||
AppImage to the tarball still gets recognized and re-linked rather than
|
||
mistaken for someone else's binary — and a real file or a symlink aimed
|
||
elsewhere is left alone. An occupied candidate directory doesn't end the
|
||
scan, and every platform now reports whether the install actually wins the
|
||
lookup (`InstalledShadowed` names whichever `tty7` beats it on PATH).
|
||
Debug builds and cargo build trees only get the environment half, so a
|
||
`cargo run` can't repoint your real `tty7` at a binary the next `cargo
|
||
clean` deletes. The Windows uninstaller removes the PATH entry it added;
|
||
on Unix there is no uninstall hook, so the symlink is left dangling and
|
||
that limitation is stated in the module docs rather than left to be
|
||
discovered. The whole thing turns off from Settings → About, or
|
||
`install_cli_on_path: false` in `config.json`. (#277)
|
||
|
||
- **Pi is a first-class agent, not a fallback one** — Pi panes drew the generic
|
||
robot glyph every unbranded agent shares, so a Pi tab was indistinguishable
|
||
from an Aider or Qwen one in the sidebar, the tab chip and the tray menu. They
|
||
now carry their own avatar on the existing sky accent, status dot unchanged.
|
||
The mark is Pi's own, from pi.dev, rescaled to tty7's 24x24 icon grid — its
|
||
`prefers-color-scheme` stylesheet dropped, since these avatars are tinted by
|
||
the app. Restoring a Pi pane also resumes its conversation now: the tty7
|
||
extension reports Pi's session id, and the resume command is
|
||
`pi --session <id>` (Pi's `--resume` is a boolean that only opens the
|
||
interactive picker), with `--session` / `--session-id` / `--fork` /
|
||
`--resume` / `-r` / `--continue` / `-c` stripped off the replayed launch
|
||
flags so the restored id wins. A pane launched with `--no-session` is not
|
||
resumed at all — that pane never wrote a session to disk, and reopening one
|
||
would override the choice to keep it ephemeral. (#240)
|
||
|
||
- **Fork an agent session** — branch a live agent conversation into a second,
|
||
independent one, so a risky direction can be tried without losing the thread
|
||
that got you there. tty7 shells the agent's *own* fork command rather than
|
||
touching its transcript files: `codex fork <id>`, `claude --resume <id>
|
||
--fork-session`, `opencode --session <id> --fork`, `grok --resume <id>
|
||
--fork-session` — every one checked against the installed CLI's own help.
|
||
Agents with no fork tty7 could verify simply don't offer the action, rather
|
||
than getting a row that can only produce a usage error. The command carries
|
||
the pane's original launch flags exactly as session restore does, and sheds
|
||
the stale session-targeting ones so a fork of a fork can't branch twice or
|
||
replay an old id as a prompt.
|
||
|
||
Where the fork lands follows where you asked from: right-click a **pane** and
|
||
it asks for a split placement (Right / Left / Down / Up), since a pane-level
|
||
ask is a spatial one; right-click a **tab** or a sidebar row and it opens in a
|
||
new tab, with no placement question. The command palette and the File menu
|
||
carry the new-tab form only — a placement question means nothing when the ask
|
||
wasn't a spatial one — while all five, the four directions included, are
|
||
bindable in Settings → Keybindings.
|
||
|
||
A fork needs the session id the agent's hooks report, so the row disables
|
||
itself — rather than disappearing — until one arrives, and a remote pane can't
|
||
fork at all (the command would run against the *local* agent). Forking while a
|
||
turn is in flight is allowed but says so: agents fork from the persisted
|
||
transcript, so the turn you're watching won't be in the copy. The parent is
|
||
never modified either way. (#241)
|
||
|
||
- **Your WSL distributions are machines you can open a workspace on** — the
|
||
transport for them has been there since remote workspaces landed
|
||
(`wsl.exe -d <distro> -- tty7-server --stdio`: no SSH, no address, no
|
||
credential, no host key), but nothing offered one, so nothing could reach it.
|
||
Every installed distribution now appears in the workspace switcher beside your
|
||
saved SSH hosts, and opening one is the whole setup — there is nothing to
|
||
configure, because there is nothing that *could* be configured. The row is
|
||
named exactly what `wsl -d` calls the distro, since that string is also how
|
||
the machine is keyed (`wsl:<distro>`), and searching the switcher for `wsl`
|
||
finds all of them.
|
||
|
||
A distribution is served the Linux `tty7-server` this client shipped with
|
||
rather than one downloaded from a release, so the first connect writes it into
|
||
`~/.local/share/tty7/bin` inside the distro — with the same one-time
|
||
confirmation any other machine gets, and no `sudo` anywhere. A build with no
|
||
bundled server (any `cargo build`, and any platform that isn't Windows) says
|
||
so and names the directories it looked in.
|
||
|
||
Two things had to be fixed for this to work at all, both invisible until
|
||
something could actually select a distribution. WSL kills what an interop
|
||
session started the moment its `wsl.exe` exits, and `setsid` does not make the
|
||
new daemon safe instantly — so the launch now holds its invocation open until
|
||
the daemon answers, instead of reporting "started but nothing was answering on
|
||
the control socket" over a correctly installed binary. And a pane's connection now asks
|
||
for the remote's *pane* socket: the flag that says so was added by the SSH
|
||
path and by the local `--stdio` one, but never by WSL, so the workspace would
|
||
connect and the window would open with a pane that could not reach the machine.
|
||
|
||
Ports need no forwarding (WSL shares `localhost`, so ⌘-clicking a dev server's
|
||
URL just works), and files move over the same `Host` calls every remote
|
||
workspace uses, or through `\\wsl$` directly. (#253)
|
||
|
||
- **Copy Session ID** — the agent's native session id on the clipboard, beside
|
||
*Copy Working Directory* in the tab / sidebar context menu, the palette and
|
||
the File menu. Codex has no copy-or-duplicate subcommand — forking *is* how
|
||
you duplicate a conversation there — so copying the id is what "copy the
|
||
session" means: paste it into `codex resume`, a bug report, or another tool.
|
||
(#241)
|
||
|
||
- **Remote panes read big git output incrementally** — `Host` grew a streaming
|
||
companion to its buffered `git`, implemented on both the local host and the
|
||
remote wire protocol, so a read whose size scales with the work tree no
|
||
longer has to exist in memory all at once on either side. The buffered call
|
||
stays for the many reads that answer in bytes. A stream that goes silent for
|
||
two minutes while the link stays up ends with a timeout rather than parking
|
||
its reader forever — the wait is between chunks, not on the whole read, so a
|
||
slow-but-alive `git diff` still runs to completion. And the queue *between*
|
||
the two ends is bounded as well, not just the reads at either end: a peer that
|
||
pushes faster than this side can parse is cut off at 32 MiB of arrears with an
|
||
error, rather than quietly reassembling the whole diff in a channel. (#247)
|
||
|
||
- **Sidebar diff preview is optional** — clicking a sidebar row's `+N −N`
|
||
working-tree counts opens the diff overlay, which is the point of them for
|
||
most people but not for everyone. Settings → Window & Tabs → *Open diff
|
||
preview from sidebar counts* turns the click off (`sidebar_diff_preview` in
|
||
`config.json`, on by default). Off, the branch and the counts stay exactly
|
||
where they are and read exactly the same; they simply stop being their own
|
||
click target, so the press falls through to ordinary tab activation like any
|
||
other part of the row. (#247)
|
||
|
||
- **Kitty graphics protocol, with a shared-memory fast path** — TUIs that
|
||
draw images now render inline in a pane. The daemon intercepts kitty
|
||
graphics escape sequences (`ESC _G…`) in the pane reader before they reach
|
||
scrollback or the client's VT parser — a zero-copy sniff on ordinary
|
||
output, allocating only once a chunk actually carries a graphics command —
|
||
and answers `a=q` capability queries directly on the PTY so probing
|
||
senders see support. Images and deletes travel to the client as compact
|
||
binary frames interleaved with normal output in stream order, so an image
|
||
lands at the cell the sender drew it at; decoding (inflate, BGRA swap,
|
||
atlas placement) runs off the render thread with newest-frame-per-id
|
||
coalescing, so a full frame from a graphics-heavy TUI can't stall PTY
|
||
output or scrolling. For a local pane, file and POSIX shared-memory
|
||
transfers are honored directly — the daemon reads the object and hands the
|
||
client raw pixels, skipping the zlib inflate the compressed-inline path
|
||
would otherwise force. A remote (SSH) pane keeps refusing shm/file and
|
||
rides the compressed-inline path over the tunnel instead. (by @ayamir in
|
||
#272)
|
||
|
||
### Changed
|
||
|
||
- **Windows and Linux stop taking keys the shell needs** — `secondary` means
|
||
Cmd on macOS and Ctrl everywhere else, and the default keymap was carried over
|
||
from macOS unchanged. That put window actions straight on top of terminal
|
||
control codes: Ctrl+D could not send EOF (it split the pane), Ctrl+[ could not
|
||
send ESC (it cycled panes), and Ctrl+W, Ctrl+K, Ctrl+P, Ctrl+J, Ctrl+T,
|
||
Ctrl+Q and Ctrl+S were all spoken for. These are window-level bindings with no
|
||
context, so they matched before the terminal ever saw the key — the code that
|
||
sends EOF was there, just unreachable.
|
||
|
||
Off macOS the rule is now that `ctrl-<letter>`, `ctrl-[`, `ctrl-]`, `ctrl-\`
|
||
and `ctrl-space` belong to the terminal, and window actions live on
|
||
`ctrl-shift-*` — the convention GNOME Terminal, Konsole, Windows Terminal and
|
||
WezTerm already share. A test enforces it, so the next binding added cannot
|
||
quietly reintroduce the problem.
|
||
|
||
| action | was | now |
|
||
|---|---|---|
|
||
| Focus previous / next pane | Ctrl+[ / Ctrl+] | Ctrl+Shift+[ / Ctrl+Shift+] |
|
||
| Split right / down | Ctrl+D / Ctrl+Shift+D | Ctrl+Shift+D / Ctrl+Alt+Shift+D |
|
||
| Close tab | Ctrl+W | Ctrl+Shift+W |
|
||
| New tab | Ctrl+T | Ctrl+Shift+T |
|
||
| Reopen closed tab | Ctrl+Shift+T | Alt+Shift+T |
|
||
| Clear scrollback | Ctrl+K | Ctrl+Shift+K |
|
||
| Command palette | Ctrl+P | Ctrl+Shift+P |
|
||
| Toggle right panel | Ctrl+J | Ctrl+Shift+J |
|
||
| Toggle left panel | unbound | Ctrl+Shift+B |
|
||
| Quit | Ctrl+Q | Ctrl+Shift+Q |
|
||
| Fullscreen | Ctrl+Enter | F11 |
|
||
| Activate tab 1-9 | Ctrl+1-9 | Alt+1-9 |
|
||
| Focus pane by direction | Ctrl+Alt+arrow | Alt+arrow |
|
||
|
||
Ctrl+S keeps saving in the code panel but now falls through to the terminal
|
||
when the editor does not have focus, so a shell still receives XOFF.
|
||
Ctrl+C, Ctrl+V and Ctrl+X are unchanged — Ctrl+C still copies only when there
|
||
is a selection and sends SIGINT otherwise — and **Shift+Insert** now pastes.
|
||
With Alt+←/→ focusing panes (Windows Terminal's default), word-by-word
|
||
movement in the prompt editor lives on Ctrl+←/→ — the Windows/Linux text
|
||
convention, which already worked — with Ctrl+Shift+←/→ and Alt+Shift+←/→
|
||
both selecting by word. macOS bindings are untouched. Anything you rebound
|
||
yourself still wins; only the defaults moved. (#270)
|
||
|
||
- **Ctrl+Shift+C and Ctrl+Shift+V copy and paste, like every GUI terminal** —
|
||
the chord GNOME Terminal, Konsole, Windows Terminal and WezTerm all teach.
|
||
Ctrl+C/Ctrl+V still work as before; Shift+Insert stays a second paste chord,
|
||
and moving Paste to a key of your own retires both defaults together. Copy
|
||
and Paste now sit in the default keymap table rather than being installed
|
||
behind the scenes, so Settings → Keybindings lists them, records new chords
|
||
for them, and warns when another binding would collide — previously
|
||
Shift+Insert was invisible there and could not be reassigned. (#271)
|
||
|
||
- **The machine that runs your panes now owns their layout** — the workspace,
|
||
tab and pane tree has moved out of the app and into the background service, so
|
||
one machine has one tree that every client of it reads: the window on it, a
|
||
laptop connected to it across the world, and (next) the session CLI. Clients
|
||
send named edits ("split this pane", "rename that tab") and receive the
|
||
incremental changes other clients make, which is what lets two windows on one
|
||
machine both land their work instead of the last one to save winning. A pane's
|
||
working directory, its coding agent and whether it is still running are now
|
||
observed by the service that owns the PTY rather than remembered by whichever
|
||
client last wrote a file — so after a service restart every pane is *known*
|
||
dead and revives into its recorded directory with its agent conversation
|
||
resumed, with no guessing about which saved ids survived.
|
||
|
||
Two consequences worth knowing before you upgrade:
|
||
|
||
- **Saved layouts do not carry over.** The tree is a new file
|
||
(`~/.local/share/tty7/machine.json`) and the old `session.json` is not read;
|
||
the upgrade also replaces the background service, which ends the panes it was
|
||
holding. The first launch after upgrading comes up on a fresh workspace, and
|
||
tabs from before it are not recoverable. `views.json` (window geometry and
|
||
which workspaces you had open) replaces `session.json` for the client's own
|
||
half; the old file is left on disk, unread.
|
||
- **Windows keeps its panes but not its layout, for now.** The tree is served
|
||
over the same control channel remote machines use, and that channel is
|
||
Unix-socket-only today, so on Windows tabs do not come back across a restart.
|
||
Panes, splits, agents and shell integration are unaffected within a session.
|
||
(#260)
|
||
|
||
- **The prompt editor's soft newline is now a rebindable action** — `Shift+Enter`
|
||
and `Alt+Enter` have inserted a literal newline into the command editor since
|
||
the multi-line prompt editor landed, but the chords were hardcoded in the key
|
||
handler: there was no `InsertNewline` to name in `keybindings`, and no way to
|
||
move the gesture to a chord of your own. The behaviour is unchanged out of the
|
||
box — both chords still insert, plain `Enter` still submits the whole buffer —
|
||
but it now runs through an `InsertNewline` action, so it appears in Settings →
|
||
Keybindings and can be rebound like anything else, and rebinding it retires
|
||
both defaults. Only the prompt editor answers it; with a full-screen program on
|
||
the pane the chord reaches the application exactly as before. `⌘⏎` fullscreen
|
||
and `⌘⇧⏎` pane zoom are untouched.
|
||
|
||
Two smaller behaviour changes come with it, both aligning on what other
|
||
terminals do. With a completion menu open, the newline chords now insert a
|
||
newline and close the menu instead of accepting the highlighted candidate —
|
||
plain `Enter` remains the key that accepts it. And `Shift+Alt+Enter`, which
|
||
the old modifier test caught by accident, now submits like any other `Enter`:
|
||
keybindings match modifiers exactly, and no terminal treats that three-key
|
||
chord as a newline. (#246)
|
||
|
||
- **Every header in the window moves it now** — grabbing the window by a header
|
||
is a property of the whole app rather than a per-surface feature, so you never
|
||
have to learn which rows happen to be draggable. The detail panel's section
|
||
title joins the caption rows that already were, wherever the panel draws one —
|
||
every tab off macOS, where that row is also the panel's tab switcher, and the
|
||
remote Files header on macOS. In horizontal-tab mode the strip also keeps a
|
||
bare 80px slice of caption for grabbing: its spacer was a flexible one with no
|
||
minimum, so it collapsed to exactly 0px once the tab chips saturated the row —
|
||
around 7-8 tabs on a 1440px window — leaving nothing to grab but three 6px
|
||
gaps and a hairline above and below the chips. The chip row's fixed-chrome
|
||
reserve is corrected to match: it was a flat 100px, sized when the corner held
|
||
a 30px "+" and a 30px "⋯", and was never raised when the workspace chip
|
||
absorbed the "⋯" menu in #169/#188 — so the row's width budget was ~20px of a
|
||
lie. It now measures the group it is reserving for, ~121px of fixed chrome plus
|
||
the 80px grab handle, so chips reach their minimum width and truncate a tab or
|
||
two sooner. (#252)
|
||
|
||
- **Large working-tree diffs no longer stall the window** — the diff overlay
|
||
had five costs that all scaled with the size of the tree rather than with
|
||
what it could show. Four were named by issue #239's source-level analysis;
|
||
the fifth, the untracked list, turned up while fixing those. Measured on a
|
||
300-file, 90 000-line, 4.5 MB working-tree diff:
|
||
|
||
- The full `git diff HEAD` was read into one `String` before parsing began.
|
||
It is now read incrementally through a new streaming call on `Host`, so
|
||
what is held at once is one 64 KiB read buffer plus the line being
|
||
reassembled rather than the whole diff — and that line is itself capped at
|
||
1 MiB, because a line is only complete at its newline and a minified bundle
|
||
is one line of many megabytes. Measured on this repository's own `git log -p
|
||
-n 400` (8 269 409 bytes of real git output, release build): 8.3 MB resident
|
||
→ 64 KiB, and *faster* end to end — 829 ms buffered (817 ms read + 12 ms
|
||
parse) → 557 ms streamed, because parsing now overlaps with git producing
|
||
output instead of waiting for all of it.
|
||
- The parsed snapshot was deep-cloned onto every tab's overlay *on the UI
|
||
update path*. It is now shared behind an `Arc`: 1.99 ms of main-thread
|
||
copying per holder → 10 ns (426 µs → 10 ns even at the new retention
|
||
budget).
|
||
- The per-file 2000-line cap bounded one pathological file but not the sum,
|
||
so two hundred ordinary files could retain 90 000 `DiffLine`s. A repo-wide
|
||
budget now caps retained lines and files-with-hunks — 90 000 lines / 6.2 MiB
|
||
of line text → 20 000 lines / 1.2 MiB — while `+N −N` keeps counting the
|
||
whole diff, so the numbers stay exact and the overlay doesn't re-probe in a
|
||
loop chasing a total it can no longer reach.
|
||
- The untracked list escaped all of the above: `git ls-files --others` reports
|
||
every path not yet ignored, and one un-ignored `node_modules` reached the
|
||
overlay as tens of thousands of rows without touching the diff at all. It is
|
||
now streamed, capped where it is retained and again where it is rendered —
|
||
while the count shown stays the true total, so a capped list never reads as
|
||
files having vanished. It deliberately does *not* drive the collapse-
|
||
everything rule below: folding file bodies shut removes no untracked rows,
|
||
so a tree with an un-ignored dependency directory and three edited files
|
||
would have hidden the three cheap things and kept the expensive one.
|
||
- The 400-line auto-collapse rule was per-file, so sixty medium files all
|
||
opened at once (thousands of side-by-side rows, none of them individually
|
||
large). Past a repo-wide total the overlay now opens with every file
|
||
collapsed — zero rows built — leads with a summary saying the diff is too
|
||
large to render efficiently, and points at expanding individual files or
|
||
`git diff` in the terminal. That total counts the context lines git prints
|
||
around every hunk, which is what is actually rendered, so it sits several
|
||
times above the `+N −N` figure at which it would otherwise fire on an
|
||
ordinary afternoon's work.
|
||
|
||
- **One `git diff` per repository, not two** — the Changes panel and the diff
|
||
overlay each ran their own full-diff probe and kept their own snapshot of the
|
||
same repository. They now share one probe and one snapshot, so opening both
|
||
costs one shell-out and one parse, and opening the overlay while the panel is
|
||
already showing that repo paints immediately instead of re-probing. (#247)
|
||
|
||
### Fixed
|
||
|
||
- **A remote workspace no longer loses or crosses its own layout on reopen
|
||
or restart** — two related failure modes in the same area, closed
|
||
together. Reopening a remote workspace, or coming back to one whose
|
||
`tty7-server` had been replaced, could land on permanently `disconnected`
|
||
panes with the coding-agent conversations gone: there was no way to tell a
|
||
genuinely restarted server (which needs a rebuild from the saved layout
|
||
and fresh shells) from a link that had merely blinked (which just needs
|
||
re-attaching). A new `ControlHelloOk.instance` identifies the server
|
||
*process* so a reconnect can tell the two apart — an absent instance reads
|
||
as unknown, never as a restart — attach failures now fall back to
|
||
spawning instead of stranding the pane in a link-down state, and "End
|
||
Sessions" clears the recorded pane ids so reopening spawns fresh shells
|
||
with `--resume` rather than trying to re-attach to panes that are already
|
||
dead.
|
||
|
||
Separately, a restart could materialize one *local* workspace's tabs
|
||
inside a different workspace's window and auto-resume every one of their
|
||
agents a second time — real, duplicate `claude --resume` calls against
|
||
conversations already running elsewhere. The root corruption that seeded
|
||
a workspace's records with another one's panes is still unattributed, but
|
||
every way it could propagate or amplify is now closed: `Spawn` carries
|
||
its owning workspace so restore refuses to attach a pane owned by another
|
||
one; pane ids are bound to the daemon instance that issued them, so an id
|
||
recorded against a dead process can't attach to an unrelated shell after a
|
||
reboot (agent resume still happens on this path, since the pane really is
|
||
gone); deduping two claims on one pane id now strips the loser's agent
|
||
session id and launch flags along with the id, so a corrupted record can't
|
||
double-resume an agent the winner already runs; and saving a session now
|
||
logs an error naming both workspace ids if a window is ever caught holding
|
||
a pane it doesn't own, so a recurrence is caught rather than silently
|
||
re-amplified. All wire and schema changes are backward and forward
|
||
compatible. (#257)
|
||
|
||
- **A captured pane reads like text, not a stream of raw escape codes, and a
|
||
broken pipe ends quietly** — `tty7 capture --plain` replays a pane's bytes
|
||
through the same `alacritty_terminal` grid the window renders panes with,
|
||
instead of leaving a script to regex-strip escape sequences itself.
|
||
Stripping alone gets the easy 90% and invents the rest: whether 200
|
||
characters with no trailing newline in a 249-column pane is one logical
|
||
line or a soft wrap is a fact about the terminal grid, not about the
|
||
bytes, and the same is true of a `\r`-overwritten progress bar, a
|
||
redrawing shell prompt, and cursor-addressed text. `--plain` now replays
|
||
each streamed segment at the width the daemon actually recorded it with —
|
||
panes on different machines are different widths, and a hardcoded
|
||
assumption would rewrap both wrong — so a captured pane matches what the
|
||
window would have shown. Raw `capture` (no `--plain`) is unchanged: still
|
||
the exact bytes the daemon stored.
|
||
|
||
Separately, every CLI verb that outputs to a pipe now ends the way `cat`
|
||
does when the reader hangs up — silently, with the shell's own SIGPIPE
|
||
exit code — instead of a Rust panic and a backtrace on stderr, which most
|
||
verbs produced on something as ordinary as `tty7 ls | head -1`. Rust
|
||
disables `SIGPIPE` by default, so a doomed write surfaces as an
|
||
`io::Error` and `println!` turns that into a panic; the fix restores the
|
||
default disposition on Unix so the kernel handles it directly, and
|
||
recognizes the Windows equivalent (`ERROR_NO_DATA`) at every stdout write
|
||
site since Windows has no signal to hand back to. (#283)
|
||
|
||
- **Restarting the server no longer strands the window on the home page** —
|
||
after confirming "Restart Server?", the window's post-restart resync could
|
||
go out on the control link to the server that had just been killed: the
|
||
connected flag is an `AtomicBool` the reader thread only flips on EOF, so
|
||
right after `restart()` returns the link often still read as up, the
|
||
layout pull failed against a dead socket, and the failure was logged and
|
||
discarded rather than retried — leaving the window sitting on an empty
|
||
home page with no hint anything was owed to it. A failed hydration is now
|
||
recorded as debt and retried on the next sync, and a window that hasn't
|
||
yet recovered its layout no longer pushes its own (empty) state back to
|
||
the server in the meantime — previously that could diff into "close every
|
||
tab" and erase the layout on disk for a window that was only ever waiting
|
||
its turn. (#282)
|
||
|
||
- **Tab no longer clears your command line when a remote pane's link is
|
||
down** — `Tab` reaches the prompt editor through a `SendTab` action that
|
||
bypassed the same-input guard ordinary typing goes through, so on a
|
||
reconnecting remote workspace a completion with no candidates handed the
|
||
line off to a dead link anyway: the command vanished, the editor stood
|
||
down for the rest of the prompt cycle, and every keystroke after it —
|
||
Enter included — was swallowed by the very guard the Tab had skipped. The
|
||
guard now covers the Tab path too, so the line stays put and typing
|
||
resumes once the link is back; a late-arriving SFTP completion result is
|
||
also dropped, with the reason logged, rather than acting on a line the
|
||
editor no longer owns. A local pane, which has no notion of a down link,
|
||
is unaffected. (#273)
|
||
|
||
- **The IME candidate window follows the actual caret, not a parked
|
||
cursor** — typing Chinese in a Kimi CLI pane on Windows stranded the
|
||
candidate list at the input box's bottom-right corner instead of
|
||
following it. Two independent causes stacked: gpui never answered
|
||
`WM_IME_REQUEST` / `IMR_QUERYCHARPOSITION`, which is how Windows 11's
|
||
default Microsoft Pinyin IME asks for caret position — it ignores the
|
||
older `CANDIDATEFORM` call tty7 was already making once per composition —
|
||
so an unanswered query fell back to the IME's own default spot; and Kimi
|
||
CLI itself hides the hardware cursor and draws its own caret as a
|
||
reverse-video cell that never gets parked at the logical input point,
|
||
landing the only real anchor tty7 had at the right edge of the input box's
|
||
border. The fix answers the position query from the input handler and
|
||
re-anchors on every composition update instead of sampling once, and when
|
||
the hardware cursor is hidden and its row holds exactly one caret-sized
|
||
(≤2 cell) inverse run, that run is now read as the IME anchor. Apps that
|
||
show the real cursor (pwsh, vim, current Claude Code) are unaffected; the
|
||
heuristic only applies with the cursor hidden, where the previous anchor
|
||
was arbitrary anyway. (#284)
|
||
|
||
- **A link that wraps across rows opens the whole URL, not just the clicked
|
||
line** — ⌘-click and ⌘-hover resolved a URL only from the physical grid
|
||
row under the pointer, so a soft-wrapped URL opened its first line only
|
||
and 404'd. OSC 8 hyperlinks now resolve across a soft wrap as one
|
||
contiguous run, using the link's own declared URI rather than
|
||
reconstructing it from the clicked row; bare URLs and file paths resolve
|
||
through the existing soft-wrap stitching, plus a new opt-in mode that also
|
||
bridges a program's own *hard* newline when the row is full to the right
|
||
edge and the last character continues the link into the next row's first
|
||
column — so an ordinary short line is never glued to the next paragraph.
|
||
Double-click word/smart-select is unaffected; only link click and hover
|
||
bridge hard wraps. (by @ayamir in #258)
|
||
|
||
- **A workspace title stays tied to the workspace** — foreground process and
|
||
agent names no longer replace the workspace title in the sidebar or its
|
||
switcher button. An explicit workspace name wins; otherwise the title is
|
||
derived from the workspace's repo/cwd, with `Untitled` as the final fallback.
|
||
|
||
- **Fullwidth CJK punctuation no longer overlaps, and prompt-mark scanning
|
||
is faster** — wide glyphs are now shaped independently instead of being
|
||
batched together, which was producing the overlap; OSC prompt-mark
|
||
scanning moves from a byte-by-byte scan to SIMD-backed `memchr`; and Thai
|
||
and Lao SARA AM combining-mark clustering is reconciled with upstream.
|
||
(by @ChihGodlee in #250)
|
||
|
||
- **tty7 gets a taskbar icon and identity on Linux** — the window now sets
|
||
`app_id: "tty7"` on every platform, which X11 uses as `WM_CLASS` and
|
||
Wayland matches against the packaged `tty7.desktop` entry, so taskbars and
|
||
window switchers can resolve the app's identity and icon where they
|
||
previously couldn't. X11 additionally needs the icon on the window itself
|
||
(`_NET_WM_ICON` ships raw pixels per window rather than reading the
|
||
desktop entry), so `assets/app-icon.png` is decoded once and downscaled to
|
||
256px — the most a taskbar uses — and attached via `WindowOptions::icon`.
|
||
macOS and Windows already got their icons through the bundle and the
|
||
embedded `.ico` respectively and are unaffected. (by @zerolover in #254)
|
||
|
||
- **Installing a remote server keys off the wire dialect it actually
|
||
speaks, not its version string** — two builds between releases can share a
|
||
version string but not a dialect, which previously let a client adopt an
|
||
incompatible remote server and fail permanently in the handshake instead
|
||
of reinstalling. (#264)
|
||
|
||
- **Listing WSL distributions no longer hangs the shell menu** — a
|
||
starting-up, updating, or wedged `wsl.exe` is now given a bounded wait
|
||
before the probe reports no distributions, matching what an unreachable
|
||
WSL already produced instead of holding the whole menu open.
|
||
|
||
- **⌃R on a remote machine searches that machine's history** — tty7 owns ⌃R at
|
||
the prompt and shows its own fuzzy menu, but the store behind it had no notion
|
||
of *where* a command had run. Every pane read one file, so ssh'ing to a server
|
||
and reaching for ⌃R offered the commands you had typed on your laptop —
|
||
worse than offering nothing, since the answers look plausible until you run
|
||
one. History is now kept per machine: the local store stays where it was, and
|
||
each remote gets its own, keyed by the target you connected to. On a remote
|
||
workspace tty7 also reads the far end's own `~/.zsh_history` and
|
||
`~/.bash_history` through the same channel it already uses for git and file
|
||
listings, so the first ⌃R on a freshly connected box has something in it
|
||
rather than starting empty. Switching a pane between machines swaps the store
|
||
under it, and the local history file is untouched by the upgrade. (#270)
|
||
|
||
- **A Windows clipboard pastes like every other clipboard** — text copied on
|
||
Windows carries `\r\n`, and a bracketed paste forwarded it byte for byte. vim
|
||
counts CR and LF as two line breaks, so pasting a block of code into it left a
|
||
blank line under every line — bad enough to make tty7 unusable for editing.
|
||
Bracketed pastes now fold `\r\n` down to a single `\n`, which is exactly what
|
||
the same paste already produced on Linux and macOS. The non-bracketed path is
|
||
untouched: with no paste mode to distinguish text from typing, a line break
|
||
still has to arrive as the CR the Return key sends. (#270)
|
||
|
||
- **Closing every window before quitting no longer loses your place** — launch
|
||
only ever restored a workspace that still had a window at quit, so closing them
|
||
one by one and relaunching came up on the empty home page, with no hint that
|
||
four workspaces were sitting there. Closing a window here is a *detach*: its
|
||
panes keep running in the daemon, which makes that workspace every bit as much
|
||
"where you left off" as one that still had a window on screen. Launch now falls
|
||
back to the workspace closed last, and the only launch that comes up on a fresh
|
||
workspace is a genuine first run. Deleting a workspace still means deleting it —
|
||
that is the one gesture that drops it from the file. (#267)
|
||
|
||
- **An unsubscribed directory watch stops delivering immediately** — dropping a
|
||
local watch handle now closes its delivery channel rather than only asking the
|
||
OS backend to stand down. Tearing that backend down is not instantaneous, and
|
||
on Windows a `ReadDirectoryChangesW` completion can fire *during* teardown and
|
||
reach a consumer that has already unsubscribed. Batches already queued stay
|
||
readable, which is the one thing a consumer racing its own drop may
|
||
legitimately still see. (#247)
|
||
|
||
- **Rounded UI controls no longer square off their corners**
|
||
([#236](https://github.com/l0ng-ai/tty7/issues/236)) — the cursor-shape
|
||
toggles (Block / Bar / Underline) are the clearest case: the selected
|
||
segment's fill filled the whole corner of the track it caps, with the track's
|
||
own anti-aliased border arc floating *inside* that square. The controls were
|
||
relying on `overflow_hidden` to shape those fills to the track's rounding, and
|
||
it cannot: gpui's overflow mask is an axis-aligned rectangle with no corner
|
||
radii, tested per fragment as a hard discard, so it can only ever cut a square
|
||
and never anti-aliases the cut. A container's own corners come from the quad
|
||
shader's distance field instead, which is why a plain card looked smooth while
|
||
anything with a filled child in the corner did not. Every such fill now
|
||
carries its own radius, inset one border-width so it nests inside the border
|
||
rather than bulging past it: the segmented controls, the −/value/+ steppers'
|
||
hover fills, the theme picker's flush previews, and the diff overlay's card
|
||
headers and closing rows. Most visible at a device pixel ratio of 1, where the
|
||
hard clip edge is a whole physical pixel wide. (#244)
|
||
|
||
- **Dragging the window by a title bar worked only rarely with a trackpad** —
|
||
five rows that stand in for the caption (the tab rail's top zone, the settings
|
||
page's top strip, the detail panel's top zone, and the code and diff overlays'
|
||
headers) armed their window-drag with a flag that was rebuilt on every render.
|
||
Any repaint between the press and the first drag event threw the arm away, and
|
||
the press *itself* schedules one — these rows carry a double-click, which makes
|
||
gpui refresh the window on mouse-down — so the drag only survived if the first
|
||
move beat the next vsync: 16ms at 60Hz, 8ms on ProMotion. A mouse press nudges
|
||
the pointer and often won that race; a trackpad press is a finger pushing down
|
||
without translating, and almost never did. The terminal's cursor blink disarmed
|
||
it on its own even without a press. The arm now lives in element state, which
|
||
survives frames — where gpui-component's own `TitleBar` has always kept it,
|
||
which is why the ordinary caption strip was never affected. (#252)
|
||
|
||
- **An idle window stays idle while a file in the Files panel is being written**
|
||
— the tree watches its roots plus every directory you have expanded, so what
|
||
it hears about is a change in a directory it is *displaying*, and a file's
|
||
contents being rewritten reports exactly as loudly as a file appearing. A
|
||
formatter rewriting in place, an editor saving on every keystroke, a build
|
||
dropping its log next to the sources: each of those repainted the whole
|
||
window, twice over, for as long as it went on. A window with nothing new to
|
||
draw never reached render idle, which is what issue #243 reports in its title.
|
||
A batch now repaints only when the re-read comes back different from what is
|
||
already on screen, and that re-read is issued from the watcher callback rather
|
||
than by asking for a paint in order to get one. A closed Files panel does no
|
||
work at all: the change is recorded and picked up on reopening. A panel showing
|
||
search hits is the same case — the hits are their own walk, so no listing is on
|
||
screen to refresh — and the change is picked up when you clear the search box.
|
||
Real changes still arrive exactly as fast.
|
||
|
||
Measured headlessly by counting window draws, before and after: five rewrites
|
||
of a file in a displayed directory cost 10 frames and now cost 0.
|
||
|
||
`.gitignore` edits keep their whole-cache refresh, now taken only when the file
|
||
can actually govern a directory the tree holds. That one is a correctness guard
|
||
on the most expensive branch here rather than a measurable saving — the watch
|
||
is non-recursive, so a `.gitignore` has to sit directly in a displayed
|
||
directory to arrive in the first place.
|
||
|
||
The other half of that report — the Files panel flickering — was fixed
|
||
independently by "keep file-tree listings on screen while they refresh", which
|
||
landed first and is the mechanism the panel now uses. This is only the frames.
|
||
Note the redraw behaviour is platform-independent and is what that number is
|
||
about; the reporter's ~15% CPU figure, and whether their flicker also
|
||
involved something in the Wayland presentation path, were not reproducible on
|
||
macOS and are not claimed to be confirmed. (#249)
|
||
|
||
- **Return no longer confirms the file tree's delete prompt** — it was the only
|
||
destructive prompt in tty7 with the destructive action first, and on macOS
|
||
(NSAlert) and Windows (TaskDialog) the first button is the Return-key
|
||
default, so pressing Return deleted — recursive folder deletion included. The
|
||
buttons now put the safe option first, matching every other destructive
|
||
prompt, with Escape still cancelling. Linux uses gpui's click-only fallback
|
||
dialog, so the swap only reorders the buttons there. (#255)
|
||
|
||
- **"Finder" is no longer named on Linux and Windows** — the file-tree context
|
||
menu and the SFTP job list's reveal tooltip hardcoded Finder-flavoured
|
||
labels on every platform; only the Info row's button was conditional. All
|
||
three sites now share that one conditional, so the action reads "Reveal in
|
||
Finder" on macOS and "Open Folder" everywhere else. On macOS the SFTP
|
||
tooltip's "Show in Finder" becomes "Reveal in Finder" too, retiring a third
|
||
name for the same action. (#255)
|
||
|
||
- **Grok Build turns up in settings search** — the agent renders a Settings →
|
||
Agents row but had no search-index entry, so searching could never surface
|
||
it. The other five agent entries had drifted from what their rows actually
|
||
say ("Claude Code hooks" for a row titled "Claude Code"), as had "Option
|
||
acts as Meta" from the rendered "Option (⌥) acts as Meta"; the index titles
|
||
now match the rows, the mechanism words (hooks / plugin / extension) stay
|
||
behind as search keywords, and a test derives the Agents index from the
|
||
agent list so a future agent can't ship unsearchable. (#255)
|
||
|
||
## [26.7.6] - 2026-07-28
|
||
|
||
### Added
|
||
|
||
- **Readline parity at the prompt** — keys that worked in a raw terminal stopped
|
||
working the moment shell integration engaged, because the local command editor
|
||
owned the keyboard at the prompt and swallowed every chord it didn't
|
||
recognise. `Ctrl-P` / `Ctrl-N` now walk history exactly like ↑ / ↓ (completion
|
||
picker and reverse search included); `Ctrl-Y` yanks back whatever `Ctrl-W` /
|
||
`Ctrl-U` / `Ctrl-K` / `Alt-D` last cut, from a one-slot kill ring of the
|
||
editor's own — zle's ring holds text this editor never cut, so borrowing it
|
||
would paste the wrong thing; and `Alt-.` inserts the previous command's last
|
||
word, repeating to walk further back. Any chord the editor has no answer for
|
||
— `Ctrl-T`, `Alt-U`, a `bindkey`-ed widget — now hands the line to the shell
|
||
so zle's keymap decides, instead of vanishing. Pressing any of them while
|
||
scrolled up snaps the viewport back to the live prompt first, so a recalled
|
||
line can't be edited off-screen. (#222)
|
||
|
||
- **One Dark Pro built-in theme** — a ninth built-in, slotted alphabetically
|
||
among the dark themes. Two deliberate departures from the VS Code theme's
|
||
terminal set, both caught by this repo's contrast tests: the accent is the
|
||
editor's focus blue `#528bff` rather than the syntax blue, which sits at the
|
||
same luminance as the switch knob it has to be legible against; and normal red
|
||
stays the classic `#e06c75`, because the Pro terminal red lands too close to
|
||
the warning orange to stay distinguishable. (#224)
|
||
|
||
- **Panes are told which terminal they're running in** — every pane now carries
|
||
`TERM_PROGRAM=tty7` and `TERM_PROGRAM_VERSION`, the de-facto standard pair
|
||
Apple Terminal introduced and iTerm2, WezTerm, Ghostty, VS Code and tmux all
|
||
set. `TERM` names terminfo capabilities and can't answer "which program is
|
||
this", so without the pair, capability probes (`supports-color`,
|
||
`supports-hyperlinks`, and the CLI ecosystem built on them), editors applying
|
||
terminal-specific workarounds, and shell prompts all fell back to their most
|
||
conservative behaviour. tty7's own `TTY7` marker doesn't help them — it exists
|
||
so globally-installed agent hooks stay silent in other terminals, and nothing
|
||
third-party knows to look for it. Unlike `TERM` and `COLORTERM`, both new
|
||
variables can be overridden from `env` in `config.json`: they name an
|
||
identity, not a capability, and posing as another terminal is a legitimate way
|
||
to get a tool that only recognises a fixed list to light up. Local panes only
|
||
— ssh forwards environment variables solely by agreement between client and
|
||
server, so a remote host still sees whatever it sets for itself. (#219)
|
||
|
||
- **Inactive panes only fade if you want them to** — a split tab dims every pane
|
||
but the focused one so the active terminal reads as foreground. That is the
|
||
right default, but it is not free: at 55% opacity a dim theme's comment color
|
||
or a long-running build's output in the pane you are *watching* rather than
|
||
typing into gets harder to read, and some people track panes by cursor alone
|
||
and never needed the cue. Settings → Appearance → Transparency now carries a
|
||
"Dim inactive panes" switch. On by default, so nothing changes for anyone who
|
||
was happy; off renders every pane at full opacity. (#214)
|
||
|
||
### Changed
|
||
|
||
- **One fewer duplicate SVG stack in the build** — the resvg 0.47 bump (#227)
|
||
left the gpui fork on 0.45, so the tree compiled two resvg/usvg/tiny-skia
|
||
stacks. The fork now pins 0.47 too, re-unifying its stack with the one tty7
|
||
uses for the tray icon. (#237)
|
||
|
||
- **The last duplicate SVG stack is gone** — after #227 and #237 unified tty7
|
||
and the gpui fork on resvg 0.47, the gpui-component fork still declared its
|
||
own `resvg = "0.45.1"`, keeping a legacy resvg/usvg/tiny-skia 0.45/0.11
|
||
stack in the tree. That fork now pins 0.47 as well, so the whole build
|
||
compiles a single resvg stack. (#238)
|
||
|
||
### Fixed
|
||
|
||
- **Box-drawing characters no longer break into dashes between rows** — they
|
||
rendered as font glyphs, and a glyph only covers the font's own line height
|
||
while the cell is `font_size × line_height` (1.4 by default). At any line
|
||
height above 1.0 every vertical run of `│` `╭` `╰` was perforated at each row
|
||
boundary: two-line prompts never closed their corners, TUI frames were dotted
|
||
down both sides. U+2500–U+257F and U+2580–U+259F are now drawn as native
|
||
geometry pinned to the cell's real edges — the same special case kitty,
|
||
Alacritty, WezTerm and iTerm2 all ship, and what tty7's Powerline separators
|
||
already did. Covers mixed light/heavy weights, the double-line set with its
|
||
junctions left open, rounded corners, dashes, diagonals, block eighths and the
|
||
░▒▓ shades. (#229)
|
||
|
||
- **Underlines survived box-drawing characters, and strokes stay one weight at
|
||
fractional DPI** — two things the native-drawing path changed without meaning
|
||
to, both invisible at the integer device scale it was developed on. An `ESC[4m`
|
||
span or a hovered URL showed a one-column hole wherever it crossed a box
|
||
character, because the underline rides the shaped text run and the native cell
|
||
returned before building one; the cell now shapes a space in its own style, so
|
||
gpui draws the line from the same `UnderlineStyle` as its neighbours. And at
|
||
125% / 150% / 175% display scaling a vertical rule alternated thin/thick across
|
||
every column, since a logical stroke width that isn't a whole number of device
|
||
pixels rounds differently per column; widths are now laid off the snapped near
|
||
edge in whole device pixels. Nothing changes at 1x or 2x. (#234)
|
||
|
||
- **Thai SARA AM lost its vowel and rendered as a dotted circle** — `ำ` (U+0E33)
|
||
is width 1 and correctly gets its own column, but it is not atomic to the
|
||
shaper: rustybuzz decomposes it and moves the nikhahit backwards onto the base
|
||
consonant, which needs the base in the same run. It was being segmented alone,
|
||
so there was nothing to reorder onto. A following SARA AM is now absorbed into
|
||
the preceding cell's cluster, so base, tone mark and vowel shape together. Lao
|
||
SARA AM (U+0EB3) takes the same path and comes along. (#230)
|
||
|
||
- **Italic CJK rendered as unrelated CJK on Windows** — every character came out
|
||
as a different character, one for one, consistently, so it read as a broken
|
||
locale or a mangled encoding. It was neither. Hack, the bundled default, has no
|
||
CJK, so those cells are shaped by the font-fallback chain; gpui's Windows
|
||
backend then threw away the face DirectWrite shaped with and looked a fresh one
|
||
up by family, weight and style. That round trip mapped DirectWrite's *italic*
|
||
to *oblique* — the two are numbered the other way around in the API — and a
|
||
family with no oblique face resolved to its upright one. The glyph indices were
|
||
right; the outlines they were pointing into belonged to a different face. Fixed
|
||
in our gpui fork by rasterizing the face DirectWrite actually chose, which also
|
||
closes a latent use-after-free in the same cache: it keyed fonts by a raw
|
||
pointer to a face nothing held a reference to. (#233)
|
||
|
||
## [26.7.5] - 2026-07-27
|
||
|
||
### Added
|
||
|
||
- **Tab completes remote paths in an SSH pane** — path candidates came off the
|
||
local filesystem, so a native-SSH pane deliberately passed no cwd and Tab
|
||
found nothing; a no-match hands the line to the shell, which costs the inline
|
||
editor until the next prompt. Since paths are what people complete most, in
|
||
practice every path Tab in an SSH session dropped the user back to the raw
|
||
shell. Tab now lists the remote directory over the pane's own authenticated
|
||
connection — the same request the SFTP panel browses with — so nothing is
|
||
echoed into the scrollback and no prompt hooks are involved. `cd` still filters
|
||
to directories. Command position and `~/` still fall through, as does a
|
||
foreground `ssh` typed into a local shell or a WSL pane, neither of which has a
|
||
tty7-owned connection to ask. (#217)
|
||
|
||
- **The last-window close confirmation can be turned off** — Settings → Window &
|
||
Tabs gains **Confirm before closing the last window**, on by default. The
|
||
prompt is a teaching device, not a safety net: ⌘Q, the tray's Quit and the
|
||
palette all quit without asking, and nothing is lost either way since the panes
|
||
keep running in the daemon. Once that model is learned, an extra dialog on
|
||
every quit is friction. Brings the window-close prompt in line with the SSH
|
||
close warning, which has had a toggle all along. (#206)
|
||
|
||
- **The detail panel and tab rail have a scrollbar** — Info / Outline / Changes,
|
||
the file tree, the remote SFTP listing and the tab rail all scrolled with a
|
||
container that painted nothing, so a deep tree or a long tab list gave no hint
|
||
that there was more content or where in it you were. The bar takes its colours
|
||
from the active theme rather than a stock grey, and follows the platform:
|
||
auto-hiding on macOS, permanently visible on Windows and Linux. (#193)
|
||
|
||
- **Foreground applications can negotiate the Kitty keyboard protocol** — the
|
||
embedded terminal config inherited `kitty_keyboard: false`, so the parser
|
||
ignored the negotiation sequences and an application asking for progressive
|
||
enhancement fell back to `modifyOtherKeys`, which tty7 doesn't implement. That
|
||
collapsed distinct chords onto the same legacy byte — `Shift+Enter` reached the
|
||
application as a plain carriage return, submitting a prompt instead of
|
||
inserting a soft newline. Legacy input is unchanged until an application opts
|
||
in. (#184)
|
||
|
||
- **The window's leading corner carries the app's mark off macOS** — macOS fills
|
||
the top-left with the traffic lights; on Windows and Linux that corner was
|
||
empty, with everything the caption row holds (the rail's "+" and collapse, the
|
||
corner chrome, the window controls) pushed to a right edge. The "duo" mark now
|
||
heads the tab rail on its content inset, the line the search box and every row
|
||
label below it start on, and follows the rail's controls into the title strip
|
||
when the sidebar is collapsed — so the corner never falls back to nothing.
|
||
Drawn, never clicked: it takes no hover capsule and no hit box, leaving the
|
||
strip grabbable through it. (#202)
|
||
|
||
### Changed
|
||
|
||
- **Interaction state and status colours are derived from the active theme**
|
||
([#197](https://github.com/l0ng-ai/tty7/issues/197)) — a segmented control's
|
||
selected option was indistinguishable from its neighbours on *every* bundled
|
||
theme (Dracula worst at 1.03:1), because the theme had a colour model but no
|
||
state model: fixed blend ratios scattered across the code, plus every
|
||
gpui-component field nobody had noticed still carrying stock greys and Tailwind
|
||
hues. Each painting surface now carries a state ladder derived to hit a
|
||
contrast *ratio* against that surface, so the result no longer drifts with the
|
||
theme — selected-vs-resting goes from 1.20–1.47:1 to 1.70–1.72:1, anchored so
|
||
Dracula's already-signed-off highlight is a no-op. Ladders are per surface, so
|
||
a menu row anchors to the popover it actually sits on. Taking a fill now also
|
||
takes its paired label colour, instead of each site hand-fixing that
|
||
separately. `danger`/`warning`/`success`/`info`/`link` come from the theme's
|
||
own ANSI-16 at 33 call sites — Dracula's delete button is literally the
|
||
`#ff5555` the terminal in the same window paints with. Switches were inverted
|
||
on every dark theme (a near-black knob on a light track, invisible on the dark
|
||
one) and now take the light end of the theme's axis with the accent on the
|
||
checked track. (#205)
|
||
|
||
- **The bundled icon set is redrawn to one humanist spec** — the previous set
|
||
accumulated one exception at a time: a heavier `plus` because a bare cross
|
||
looked frail, a `stock/` prefix to undo overrides that were too heavy at 16px,
|
||
two names sharing one drawing. All 17 glyphs now hold to a single spec — 2.1
|
||
stroke throughout, corner radii matching the app's 10px panels, near-square
|
||
boxes of equal apparent area, round caps and joins everywhere. Metaphors are
|
||
untouched, so nothing needs relearning; `folder-closed` gains an inner rule
|
||
that distinguishes it from `folder`, and `info` a title bar that stops it
|
||
colliding with `panel-left`. (#190)
|
||
|
||
- **The title bar spans the detail panel off macOS** — the panel was a
|
||
full-height column *beside* the bar, and the bar lays out ─ ▢ ✕ at its own
|
||
right end, so opening the panel on Windows or Linux stranded the window
|
||
controls mid-window with the panel's grey to their right. Layout moves to
|
||
`[rail | col(bar / row(body, panel))]` so the bar reaches the corner, with the
|
||
caption row over the panel painted in the panel's own surface — the column
|
||
reads as one continuous sidebar from the very top instead of starting 40px down
|
||
in a different colour. The panel's tab tiles move into the section header it
|
||
was already drawing, since a tile row of its own made three stacked headers
|
||
before a single line of content. macOS is untouched. (#188)
|
||
|
||
### Fixed
|
||
|
||
- **Splitting or un-maximizing a pane you were hovering no longer kills the app**
|
||
([#201](https://github.com/l0ng-ai/tty7/issues/201)) — a pane remembers the
|
||
cell under the pointer (that's what makes ⌘-hover underline links), and nothing
|
||
invalidated it when the grid shrank underneath. The remembered row then named a
|
||
line the grid no longer had, and the next modifier press indexed the grid with
|
||
it — inside a gpui input callback, which is `extern "C"` and cannot unwind, so
|
||
the process aborted with the message lost. `⌘⇧D`, `⌘⇧⏎` and dragging the window
|
||
smaller were all reliable ways to hit it. The hovered cell is dropped on resize
|
||
and the row is validated before indexing. Two other aborts went with it: `⌘T` /
|
||
`⌘D` against a dead daemon now restarts it and retries once, and session
|
||
restore with an unreachable daemon restores what it can instead of dying. A
|
||
panic anywhere in the GUI now also lands in `<config-dir>/crash.log` with its
|
||
message, location and backtrace — the OS report only ever kept the abort.
|
||
(#204)
|
||
|
||
- **Emoji written with a variation selector take their real width and
|
||
presentation** ([#203](https://github.com/l0ng-ai/tty7/issues/203)) — `🗂️`,
|
||
`❤️` and `⚠️` were budgeted one column, so the glyph bled over its neighbour
|
||
and every following cell on the line shifted left by one, taking selection and
|
||
click hit-testing with it. The selector is zero-width and lands *after* the
|
||
base's column budget is spent, and nothing revisited the decision. The same
|
||
gap made `❤️` render as the black text-presentation heart, identical to bare
|
||
`❤`, because only `cell.c` reached the shaper — dropping every combining mark,
|
||
not just variation selectors. The terminal now re-scores the sequence and
|
||
widens the cell, and carries the marks through to be shaped with their base:
|
||
`❤️` and `⚠️` come up in colour while their bare forms stay monochrome, and
|
||
`e` + U+0301 renders as `é`. Narrowing (`U+FE0E` on an already-wide emoji) is
|
||
still not implemented — it would have to free a column and reflow the line.
|
||
(#210, #216)
|
||
|
||
- **Untrusted terminal output can no longer freeze a pane** — with Kitty
|
||
keyboard negotiation enabled, an upstream `alacritty_terminal` bug became
|
||
reachable: `push_keyboard_mode` caps its stack by removing from the *title*
|
||
stack, a copy-paste slip that compiles because both are `Vec`s. With an empty
|
||
title stack that panics and kills the reader thread, freezing the pane; with a
|
||
non-empty one it silently drops a saved title, so a later restore returns the
|
||
wrong one. It doesn't take hostile output — a TUI that pushes without popping
|
||
reaches the 4096 cap on its own in a long session, and `cat` of a crafted file
|
||
or a remote host over SSH gets there in one ~20KB burst. Pinned to a patched
|
||
fork, with a regression test that guards the pin. (#194)
|
||
|
||
- **`theme_follow_system` no longer panics every launch on Linux** — flipping
|
||
"sync with system" on in Settings persists immediately, so the app then aborted
|
||
on every start until `config.json` was hand-edited back, with a backtrace
|
||
pointing at gpui internals and nothing connecting it to the theme toggle. gpui's
|
||
Wayland and X11 backends dispatch the appearance-changed callback while the
|
||
platform client's `RefCell` is already mutably borrowed, and reading
|
||
`cx.window_appearance()` from that callback re-borrows the same cell. The OS
|
||
appearance is now cached in a global that the observer fills from the window's
|
||
own cell, so nothing on the re-entrant path touches the client. macOS and
|
||
Windows were never affected. (#181)
|
||
|
||
- **A config file saved with a UTF-8 BOM no longer wipes your settings** — every
|
||
config-dir file is read by a loader that treats any parse error as "there is
|
||
no file", falling back to defaults. `serde_json` rejects the U+FEFF a BOM puts
|
||
before the opening brace, so a BOM didn't report a broken config — it reported
|
||
an absent one, and tty7 came up on defaults with nothing in the log to explain
|
||
it. Windows makes this easy to hit by accident: PowerShell's `>`, `Out-File`
|
||
and `Set-Content -Encoding utf8` all write a BOM, so editing `config.json`
|
||
from a shell was enough to lose every setting. `config.json`, `session.json`
|
||
(which lost every workspace the same way) and hand-authored `themes/*.yaml`
|
||
now skip a leading BOM. (#215)
|
||
|
||
- **New tabs and splits open in the right directory even when the shell can't be
|
||
instrumented** ([#187](https://github.com/l0ng-ai/tty7/issues/187)) — a pane
|
||
learned its directory from `OSC 7`, which only shells tty7 injects its
|
||
integration into ever emit. A shell that `exec`s into another one from its rc
|
||
file (`exec fish` at the end of `.zshrc`), a nested shell started by hand, or
|
||
any shell with no integration at all emitted none — and because a pane's
|
||
directory is seeded with the one it was spawned in, such a pane didn't report
|
||
*no* directory, it reported a permanently stale one. New tabs, splits, the git
|
||
probe and path completion all followed it to the wrong place, with nothing on
|
||
screen to say why. The pane now also reads its directory from the process
|
||
table, on the same half-second foreground poll that already detects SSH
|
||
sessions and coding agents. A shell that does emit `OSC 7` keeps its own
|
||
spelling of the path: `$PWD` preserves the symlinked route the user walked in
|
||
through, and that is the one a new tab should open in. macOS and Linux;
|
||
Windows has no equivalent process query and is unchanged. (#207)
|
||
|
||
- **Links open on Ctrl+click on Windows and Linux**
|
||
([#183](https://github.com/l0ng-ai/tty7/issues/183)) — the link modifier was
|
||
the platform key, which gpui maps to ⌘ on macOS but to Win/Super elsewhere, a
|
||
key the OS mostly swallows. Off macOS neither the hover underline nor
|
||
click-to-open could be triggered at all, and the only way to follow a URL was
|
||
to select and copy it — while `config.json`'s own docs had promised
|
||
"⌘/Ctrl-click" all along. Now the same portable secondary modifier the
|
||
keybindings already use, so ⌃-click keeps meaning right-click on macOS.
|
||
Settings copy and the docs follow the platform. (#192)
|
||
|
||
- **Option-as-Meta works with a CJK input source**
|
||
([#177](https://github.com/l0ng-ai/tty7/issues/177)) — macOS gives Option two
|
||
jobs, and routes the chord before the terminal sees it. With a non-ASCII input
|
||
source active, ⌥F went to the IME, which committed `ƒ` and consumed the event,
|
||
so the code that turns the chord into `ESC f` never got a say. The setting
|
||
worked on ASCII layouts and silently did nothing on CJK ones, which is why it
|
||
read as intermittent rather than broken. gpui can now decide per keystroke
|
||
rather than once per view, so Meta chords are claimed by the terminal while
|
||
ordinary text, dead keys and Pinyin composition still reach the IME. gpui
|
||
accordingly moves to the `l0ng-ai/zed` fork. (#191)
|
||
|
||
- **A whole command cycle arriving in one read reports both prompt states** —
|
||
the OSC sniffer folded a chunk's shell-integration marks into one state and
|
||
sent a single frame, hiding the prompt boundary the client counts to tell a
|
||
fresh prompt from a same-prompt redraw. Routine over SSH, where a fast
|
||
command's start mark, output and completion mark leave the remote in one
|
||
packet. Marks on the same side of a boundary still fold, so an ordinary prompt
|
||
draw costs one frame as before. (#217)
|
||
|
||
- **The detail-panel toggle stops drawing itself as selected, and the Files
|
||
dotfile switch moves into the tree's right-click menu** — follow-ups to the
|
||
caption-row rework, which left the toggle permanently lit and the dotfile
|
||
control competing for room on the header line. (#188)
|
||
|
||
- **The editor and diff overlays keep their header on the caption line when the
|
||
detail panel is open** — off macOS the title bar is hoisted above
|
||
`[terminal | panel]` so the ─ ▢ ✕ group can reach the window's corner, which
|
||
left both overlays — anchored to the terminal column — starting 40px down.
|
||
Their headers are drawn to *be* the title bar while they're up (its height,
|
||
its insets, a full chrome tile for their one control), and instead landed a
|
||
row low, level with the panel's tab row. They now hang on the row that owns
|
||
the bar, inset by the panel's width, so the corner chrome keeps its surface
|
||
and its clicks. (#202)
|
||
- **Those headers became real title bars** — dragging one now moves the window
|
||
and double-clicking zooms it. Both covered the caption row and neither did
|
||
either, with the panel open or closed, so opening a file turned the top of the
|
||
window into a 40px strip that looked exactly like a title bar and answered
|
||
nothing. Their controls (the ✕, the diff's back-to-all-files chip) are
|
||
`occlude()`d to keep taking clicks: a drag region on Windows is HTCAPTION, and
|
||
the OS claims the press before the app hit-tests. Double-clicking a stand-in
|
||
title bar zooms the window on Linux too. (#202)
|
||
- **The rail's top zone lines up with the title bar to the pixel** — the bar
|
||
reserves a hairline inside its own height that the rail's stand-in row didn't,
|
||
so everything in that row sat half a pixel low. Invisible on the line-art
|
||
tiles; not on the mark, which visibly hopped as collapsing the rail handed it
|
||
over to the bar. (#202)
|
||
- **CJK and emoji stop falling through to the OS on Windows and Linux** — the
|
||
default `font_fallbacks` named only faces that ship with macOS (Menlo, Apple
|
||
Color Emoji), so off macOS the entire chain matched nothing and every
|
||
ideograph and pictograph was resolved by the platform's own cascade instead.
|
||
The bundled Hack primary carries no CJK at all, so on Windows this was every
|
||
Chinese character in every pane. Defaults are now chosen per platform —
|
||
PingFang SC / Apple Color Emoji on macOS, Microsoft YaHei / Segoe UI Emoji on
|
||
Windows, Noto on Linux — and those stock names are appended to a
|
||
hand-written list as well, so a `config.json` that predates this change (or
|
||
was copied from another machine) is repaired at use time without being
|
||
rewritten. Maple Mono NF CN stays first on every platform: its 1.2em CJK
|
||
advance is the only exact fit for the two-column slot Hack's 0.60205em cell
|
||
produces, so a 1.0em stock face is left-aligned there with the remaining
|
||
~0.2em showing as a gap on the right of each character. (#195)
|
||
|
||
## [26.7.4] - 2026-07-26
|
||
|
||
### Added
|
||
|
||
- **Grok Build gets the full hook integration** — Settings → Agents grows a
|
||
sixth row, installing tty7's hooks to `~/.grok/hooks/tty7.json`. A grok pane
|
||
now carries the same live status the other agents do (blue "working" → green
|
||
"done", amber "needs you" when grok asks a question), and after a restart it
|
||
relaunches `grok --resume <id>` with the original launch flags instead of a
|
||
bare shell. Panes that only ever had the Claude Code hooks installed are
|
||
relabeled to grok rather than reporting as "Claude Code", and grok's brand
|
||
mark replaces the generic robot avatar. (#174)
|
||
- **Edit, File and Help menus** — copy/cut/paste/select-all, undo/redo and
|
||
find/find-next now live in a real Edit menu; `About tty7`, `Check for
|
||
Updates…`, `Hide tty7` and Services sit in the app menu; Docs, Discord and
|
||
`Report an Issue` are reachable from Help; and tab actions that used to exist
|
||
only in a context menu (Rename Tab, New Worktree Tab, Close Other Tabs, Close
|
||
Tabs to the Right, Copy Working Directory) are real actions you can also find
|
||
in the palette and rebind in Settings. (#175)
|
||
|
||
### Changed
|
||
|
||
- **The menu bar follows the HIG** — `tty7 · File · Edit · View · Window ·
|
||
Help` replaces `tty7 · Shell · Window · View`. The `Shell` menu is gone: its
|
||
new / close / split / rename items are File's job everywhere else, and the
|
||
name collided with Settings → Shell, which configures something entirely
|
||
different. View gained the layout toggles and pane commands that were
|
||
chord-only, and `Delete Workspace…` moved behind its own rule at the bottom
|
||
of File. (#175)
|
||
- **The command palette is ranked, banded, and says what it will do** — results
|
||
are scored rather than filtered, so word-initials and prefixes outrank
|
||
scattered letters; the idle list opens on `Recent` (frecency) followed by
|
||
Tabs & Panes · Workspaces · View · Terminal · SSH · Agents · Application
|
||
instead of 47 flat rows. Names describe the outcome and the current state
|
||
(`Hide Left Sidebar`, `Tab Bar: Move to Left Sidebar`) rather than the switch,
|
||
one namespace prefix per subsystem, and the naming grammar is written down at
|
||
the top of `palette.rs`. (#175)
|
||
- **Settings re-sectioned around what you're configuring** — the orphan `Shell`
|
||
page folds into Terminal's first group, a new `Input` page collects the prompt
|
||
editor, selection/clipboard and keyboard settings, Terminal drops from seven
|
||
groups to four, and notifications moved to Window & Tabs where the rest of the
|
||
app-level behaviour lives. The search index grew from 14 entries to 51 with
|
||
titles pinned to the rendered row labels, so `opacity`, `blur`, `completion`,
|
||
`ctrl-r` and `grouping` now find their rows. (#175)
|
||
- **The tab bar defaults to the left sidebar** — a fresh install opens with the
|
||
vertical tab rail instead of the horizontal title-bar strip. Anyone who has
|
||
flipped the setting or hit `ToggleTabSidebar` has an explicit value persisted
|
||
and keeps their layout. (#171)
|
||
- **Idle chrome tiles paint at the rail's ink weight** — title-bar toggles,
|
||
panel tabs, `+` and their siblings now draw their glyph in
|
||
`sidebar_foreground` instead of the near-black `foreground`, with full
|
||
strength reserved for the selected tile. The chrome no longer reads a step
|
||
darker than everything it neighbours, and "on" gets a second cue beyond the
|
||
grey capsule. (#176)
|
||
|
||
### Fixed
|
||
|
||
- **Non-ASCII output survives a Finder/Dock launch** — macOS GUI launches can
|
||
omit every locale variable, leaving the shell in the C locale: `ls` printed
|
||
one `?` per non-ASCII byte, so a CJK filename came out as `??????`, and tmux
|
||
replaced each Unicode cell with `_`. New shells now receive a UTF-8 character
|
||
locale (`LC_CTYPE` only, so message/date/number localization is untouched)
|
||
derived from the system locale the way Terminal.app does it, and only when no
|
||
inherited or configured locale exists — explicit locale choices still win.
|
||
The derived name is checked against the locales actually installed before it
|
||
is exported, so it stays loadable on remote hosts too, where ssh forwards
|
||
`LC_*` by default.
|
||
([#178](https://github.com/l0ng-ai/tty7/issues/178), #173, #180)
|
||
- **Right-click Paste dropped images, and Copy looked disabled** — copy, cut,
|
||
paste and undo each had two implementations, a chord handler and an action
|
||
handler, which had drifted: the context menu's Paste skipped the image branch
|
||
⌘V had, so pasting a screenshot into an agent pane worked by keyboard but not
|
||
by menu, and Copy rendered disabled whenever the selection lived in the prompt
|
||
editor rather than the terminal grid. Both now route through one method each.
|
||
(#175)
|
||
- **The palette named the other window's sidebar** — the stateful palette titles
|
||
read the config copies of `sidebar_collapsed` / `right_panel_visible`, which
|
||
only record whichever window toggled them last. With two windows in different
|
||
states, one window's palette offered "Show Left Sidebar" while its rail was
|
||
already out. Each window now labels the toggles from its own chrome state.
|
||
(#175)
|
||
- **No more update prompts for a half-built release** — the release step ran
|
||
inside the build matrix, so the first platform to finish published a release
|
||
carrying only its own assets, and that release became `/releases/latest`
|
||
immediately. The in-app update check polls exactly that endpoint, so users
|
||
were prompted to download a version whose `.dmg` was still notarizing. All
|
||
four platforms now upload artifacts and a single gated job assembles one
|
||
**draft** release with every asset. (#172)
|
||
|
||
## [26.7.3] - 2026-07-25
|
||
|
||
### Added
|
||
|
||
- **Multiple windows, one workspace each** — `New Workspace` (⌘⇧N) opens a
|
||
second window with its own tabs, splits, and chrome state. A workspace is
|
||
the persistent thing: closing its window puts it away with its panes still
|
||
running in the daemon, and the title bar's workspace menu, the command
|
||
palette, and the home page's picker all bring it back. `Stop Workspace`
|
||
ends one for real (no default chord — it kills sessions), `Delete
|
||
Workspace` also forgets the layout, and a workspace can be renamed from
|
||
the title-bar chip. (#169)
|
||
- **Workspace switcher, in the two places you'd look** — a title-bar chip
|
||
(monogram + chevron) whose menu lists every workspace with a monogram badge
|
||
and a corner dot for the ones whose shells are still running, and the macOS
|
||
**Window** menu listing the same set: on screen first, then the detached ones
|
||
with how long ago you left them. Both show the first nine. (#169)
|
||
- **Detail panel** — a docked right-hand column that shows what the active
|
||
pane *is* rather than what it's printing: **Info** (cwd, branch, shell,
|
||
agent, the process tree and the TCP ports the pane is listening on),
|
||
**Outline**, **Changes**, and **Files** (a local file tree). Opening a file
|
||
from it lifts a code editor overlay over the terminal. (#158)
|
||
- **Port forwarding and SFTP live in the detail panel** — a native-SSH pane's
|
||
forwards are listed, added and removed from the Info tab, and its remote
|
||
filesystem browses from the Files tab, with transfers reported in a footer
|
||
that rides under every tab. (#166)
|
||
- **Shell integration in native SSH panes** — OSC 133 prompt marks, exit
|
||
codes and cwd reporting now reach zsh, bash and fish on the far side, so
|
||
the inline line editor works in an SSH pane exactly as it does locally.
|
||
(#152)
|
||
- **Live drag-to-reorder** — tabs rearrange under the cursor the way Chrome
|
||
and Warp do (the dragged tab stays in the list and the ones it passes slide
|
||
out of the way) in the strip, the sidebar rows, and whole repo groups.
|
||
(#151)
|
||
- **The rail's top strip drags the window** — the sidebar's title-bar-height
|
||
top zone now moves the window on drag and zooms it on double-click, like
|
||
the title bar it sits level with. (#153)
|
||
- **The ⌃R history menu can be switched off** — Settings → Terminal →
|
||
Keyboard, or `history_search` in `config.json`. With it off, the prompt
|
||
line is handed to the shell and the raw `^R` follows it, so a binding
|
||
of your own (fzf, percol, plain reverse-i-search) answers instead of
|
||
tty7's menu. (#163, #170)
|
||
|
||
### Changed
|
||
|
||
- **Sidebar and right-panel visibility are per-window** — toggling one
|
||
window's chrome leaves the others alone. The config value is now what a
|
||
*newly opened* window starts with; panel width stays shared. (#169)
|
||
- **Chrome icons draw at the size they were asked to** — every tile glyph in
|
||
the window (title bar, rail, panel tabs, overlay headers) had been rendering
|
||
at 12px regardless of the size its call site set, because the button widget
|
||
overwrites its icon's size from its own. The tile rhythm is now stated once
|
||
and applied from one helper, so the marks read at their intended weight and
|
||
the hover capsule keeps a consistent gap from the window edge. (#169)
|
||
- **The panel and chrome glyphs are drawn to one spec** — the detail panel's
|
||
tab marks and the title bar's own were redrawn so a row of icons stops
|
||
reading as a row of icons from different sets, and every icon tile shares
|
||
one soft hover fill. (#161)
|
||
- **Multi-line commands submit as one bracketed paste** — a 30-line block
|
||
costs one prompt cycle instead of thirty, so it no longer retypes itself
|
||
down the screen on Enter. (#164)
|
||
- **The file tree lists directories off the render thread** — `read_dir`, the
|
||
`.gitignore` chain and the search walk left the paint, so a cold cache or a
|
||
large repo no longer stalls a frame. (#160)
|
||
- **The LSP client is gone** — opening a `.rs` file in the code panel silently
|
||
spawned rust-analyzer, which indexed the whole workspace for hundreds of
|
||
megabytes of RAM. A terminal shouldn't do that to you on a click, and the
|
||
fix is removal rather than a switch for something nobody asked for. (#159)
|
||
- **Sidebar rows float their close button** — the ✕ no longer reserves a
|
||
permanent trailing column, so labels and branch lines get the full rail
|
||
width, and it stays clear of a row's `+n −n` counts. (#145)
|
||
- **Daemon wire protocol is now v2** — WSL panes carry a remote-context kind
|
||
that a v1 client can't decode, so it would drop the pane's connection
|
||
instead of ignoring the unknown value. The version handshake now sees that
|
||
skew and offers to restart the daemon, rather than letting a downgraded
|
||
build lose panes silently. (#169)
|
||
- **`Cargo.lock` is checked against `Cargo.toml` in CI** — release builds now
|
||
run `--locked`, so a drifted lockfile fails the build instead of quietly
|
||
resolving something else. (#150)
|
||
|
||
### Fixed
|
||
|
||
- **⌃J and ⌃M submit the line again** — accept-line's control codes were
|
||
swallowed at the prompt as unrecognized Ctrl chords, so the keys did
|
||
nothing. They now take the same path Enter does, completion picker and
|
||
history menu included. (#163, #170)
|
||
- **The sidebar's git counts keep up** — `+N −N` refreshes as an agent's tool
|
||
calls land (throttled per repo) and when the window regains focus, instead
|
||
of going stale until you happened to run a command in that pane. (#149)
|
||
- **`ToggleSftp` earns its name** — pressing it while the panel is already on
|
||
Files closes the panel rather than being a dead press, and the remote
|
||
browser's 500ms transfer poll now ends itself when the panel closes instead
|
||
of depending on the render path to retire it. (#167)
|
||
- **Closing the last window from the home page quits** — it used to leave a
|
||
windowless process in the Dock that no longer responded to being clicked.
|
||
(#147, #148)
|
||
- **Windows titlebar chrome** — the `⋯` menu sits back on the native
|
||
window-control rhythm, and the sidebar's collapse / new-tab buttons take
|
||
their clicks again instead of being swallowed by the drag region. (#162)
|
||
- **Windows: the theme panel's close button did nothing** — and the repo group
|
||
header showed a plain arrow where every other row shows a pointer. (#155,
|
||
#156)
|
||
- **Settings keeps its stock glyphs** — the page's own icons stopped being
|
||
restyled by the chrome tile pass, and its close button is sized to the title
|
||
bar's rhythm rather than reading undersized beside the window controls.
|
||
(#157, #165)
|
||
|
||
## [26.7.2] - 2026-07-21
|
||
|
||
### Added
|
||
|
||
- **WSL shell integration** — prompt marks, cwd tracking, and agent
|
||
detection now work inside WSL distros. The integration tags the shell
|
||
it actually launches, never blocks the spawn path while probing the
|
||
distro's shell, and declines untranslatable cwds rather than
|
||
misreporting them. (#134)
|
||
- **Git Bash shell integration on Windows** — Git Bash panes get the
|
||
full integration, reporting proper Windows paths for cwd-dependent
|
||
features. (#130)
|
||
- **Smart double-click selection** — double-click selects the word under
|
||
the cursor with language-aware boundaries, including CJK segmentation
|
||
(the OS tokenizer on macOS, jieba elsewhere); angle-bracket pairs must
|
||
hug their contents and contraction apostrophes stay out of quote
|
||
pairing. (#128)
|
||
- **Configurable file-link command** — file links can open with a custom
|
||
command instead of the default editor. Contributed by @ayamir. (#143)
|
||
- **Agent resume keeps launch flags** — resuming an agent session
|
||
carries the flags the agent was originally launched with onto the
|
||
resume command. (#144)
|
||
- **Sidebar git line follows the agent's cwd** — the branch/status line
|
||
under an agent tab tracks the directory the agent is actually working
|
||
in, not just the pane's shell. (#127)
|
||
|
||
### Changed
|
||
|
||
- **Tab close button floats over the label** — the close affordance no
|
||
longer reserves a slot in every tab, so labels get the full width
|
||
until hovered. (#126)
|
||
- **Dependency updates** — resvg 0.47, sha2 0.11, a cargo minor-patch
|
||
group bump, and newer CI artifact actions. (#137, #138, #139, #140,
|
||
#141)
|
||
|
||
### Fixed
|
||
|
||
- **Tab completion falls through to the shell** — a Tab tty7 has no
|
||
candidates for now hands the line to the shell's own completion
|
||
instead of being swallowed (remote panes included); `cd`/`pushd` stop
|
||
offering files; and the whole feature can be turned off with
|
||
`tab_completion` in config or Settings → Terminal → Keyboard. (#146)
|
||
- **Remote panes never leak local filesystem operations** — a remote
|
||
pane's cwd is no longer used for local path lookups, and the agent-cwd
|
||
bypass validates inherited directories. (#133)
|
||
- **macOS text input goes through the IME** — synthesized keystrokes
|
||
keep their text (remote/automated input no longer degrades), and Kitty
|
||
full mode stays off the IME path. (#132)
|
||
- **No more console flashes on Windows git probes** — git status probes
|
||
and ssh ProxyCommand children no longer flash console windows. (#129)
|
||
- **Windows titlebar menu spacing** — the titlebar "..." aligns with the
|
||
window-control rhythm. (#131)
|
||
- **Codex avatar and macOS tray sizing** — the Codex tab avatar uses its
|
||
black brand field, and the tray icon renders at 22pt instead of the
|
||
hardcoded 18pt. (#142)
|
||
- **Agents settings layout** — rows stay aligned and notes terse. (#125)
|
||
|
||
## [26.7.1] - 2026-07-17
|
||
|
||
### Added
|
||
|
||
- **Follow the OS appearance** — a "Sync with system" mode with separate
|
||
light and dark theme slots: the theme flips live when the OS switches
|
||
appearance, native chrome follows along, and picking a theme while
|
||
syncing writes the slot matching the current mode. Old configs are
|
||
unchanged (sync defaults off). (#121)
|
||
- **Mark as Unread on tabs** — the tab context menu can re-flag a finished
|
||
agent result you've already looked at, re-arming the unread badge on the
|
||
Done dot. Agent tabs only; disabled while the agent is still working.
|
||
(#120)
|
||
|
||
### Changed
|
||
|
||
- **"Duo" logo refresh** — a new brand mark (two offset session panes,
|
||
mint behind ink, with a prompt chevron) replaces the orange window
|
||
identity across every icon asset: app icon, logo, tray glyph, favicon,
|
||
and social preview. Bare macOS binaries (`cargo run`) now show the icon
|
||
in the Dock too. (#124)
|
||
|
||
### Fixed
|
||
|
||
- **Linked git worktrees group under their main repository** — the sidebar
|
||
keys groups on the repository home instead of each worktree's own root,
|
||
so a repo and its worktrees share one header while branch status stays
|
||
per-worktree. (#118)
|
||
- **macOS tray icon stays a template image in every state** — the
|
||
attention state no longer swaps in a grey non-template glyph that was
|
||
illegible on many menu-bar appearances; agent status lives in the
|
||
tooltip and tray menu. (#122)
|
||
- **No more console-window flashes from Windows agent hooks** — each hook
|
||
emitter frees its throwaway console before it can paint (debug builds
|
||
only; release builds were unaffected). (#119)
|
||
|
||
## [26.7.0] - 2026-07-16
|
||
|
||
First CalVer release: versions are now `YY.M.PATCH`, so the number says when
|
||
it shipped rather than what changed.
|
||
|
||
### Added
|
||
|
||
- **Coding-agent detection on Windows** — the agent status dot now works on
|
||
Windows: agents are detected from the shell-integration command capture
|
||
(no `/proc` there), hook status events reach the daemon via the agent's
|
||
ancestor console, and mark-derived detection only re-fires when the
|
||
command capture actually changes. (#115)
|
||
- **Clipboard image paste for agents off macOS** — pasting a screenshot into
|
||
a coding-agent pane on Windows/Linux stages the image to a temp file and
|
||
pastes its shell-escaped path (the same route drag-and-drop uses); Windows
|
||
BMP screenshots are transcoded to PNG since agent vision rejects BMP.
|
||
macOS keeps the higher-fidelity pasteboard read. (#117)
|
||
- **Nightly build channel** — `main` is built every night into a rolling
|
||
`nightly` prerelease with all six platform artifacts; the in-app update
|
||
check is prerelease-aware so nightly users ride the channel while stable
|
||
users never see it. (#114, #116)
|
||
- **Sidebar groups tabs by git repository** — vertical tabs cluster under a
|
||
repo header, groups persist across restarts, ⌘N numbering follows visual
|
||
order, and same-named repos are disambiguated by their parent directory.
|
||
(#110)
|
||
- **System tray icon with agent status menu** — a tray/menu-bar icon
|
||
summarizes agent activity across sessions and jumps to a pane from its
|
||
menu. (#105, #109)
|
||
- **Gradient and image theme backgrounds** — themes can render gradient or
|
||
image backgrounds with global window opacity and blur, hot-reloaded like
|
||
the rest of the theme. (#106)
|
||
- **Double-click a tab to zoom the window** — matching titlebar behavior;
|
||
rename moved to the context menu. (#103)
|
||
|
||
### Changed
|
||
|
||
- **Settings sheet refinements** — controls right-align in their rows with
|
||
hover feedback, the column is tighter, and the theme card is richer.
|
||
(#108)
|
||
- **Settings copy polish** — clearer wording throughout, and the SSH
|
||
security defaults stay visible instead of hiding behind a toggle. (#104)
|
||
|
||
### Fixed
|
||
|
||
- **Ctrl+C after a copy sends SIGINT again** — copying with Ctrl+C and
|
||
pasting to the PTY now consume the selection, so the next Ctrl+C reaches
|
||
the program instead of copying the same text twice. (#113)
|
||
- **Shell vi mode is supported** — vi-mode prompts are detected via durable
|
||
signals and no longer confuse the prompt gap hold. (#102, thanks @ayamir)
|
||
- **App cursor shape is respected** — programs that set the cursor shape
|
||
(e.g. vim) see it honored. (#101, thanks @ayamir)
|
||
|
||
## [0.17.0] - 2026-07-16
|
||
|
||
### Added
|
||
|
||
- **Per-tab context menu with worktree tabs** — right-clicking a tab chip or
|
||
sidebar row opens a menu: rename, splits, copy working directory, and a
|
||
close group. *New Worktree Tab* creates a git worktree under the repo's own
|
||
`.tty7/worktrees/<name>` (kept out of `git status` by a self-ignoring
|
||
`.gitignore`), with editable name / branch / start point and a live path
|
||
preview. Closing a tab that sat in a managed worktree offers to remove the
|
||
checkout — unless another pane still lives in it, and dirty checkouts
|
||
default to keeping. (#96)
|
||
- **Unread pane count in the tab status dot** — a split tab can finish
|
||
several agent turns while you're away; the green Done dot now swells into
|
||
a badge counting the unread panes (clamped at 9) and shrinks back once
|
||
every pane has been seen. (#98)
|
||
|
||
### Changed
|
||
|
||
- **The sidebar diff overlay is per-tab and side-by-side** — the overlay now
|
||
lives on its tab: switching away hides it, switching back restores it
|
||
(re-probing when the status cache disagrees), closing the tab drops it,
|
||
and Esc keeps working. The body switches from a unified diff to a
|
||
GitHub-PR-style side-by-side view with positionally aligned
|
||
removed / added columns. (#100)
|
||
- **macOS-style popup panels** — menus get a rounded pill highlight, inset
|
||
hairline separators, a 10px panel radius, and a floatier shadow;
|
||
searchable lists get a taller Spotlight-style search row, and the palette
|
||
viewport holds a whole number of rows so the last visible row is never
|
||
cut mid-height. (#97)
|
||
- **README rewritten as a minimal index** — feature details, keybindings,
|
||
and performance notes moved to `docs/features.md` (en + zh-CN); the
|
||
tagline now positions tty7 as a terminal workbench.
|
||
|
||
### Fixed
|
||
|
||
- **The agent dot no longer sticks on Waiting after you approve a
|
||
permission** — Claude Code has no "permission replied" hook, so a new
|
||
PostToolUse hook emits a tool-complete event and the first tool that
|
||
finishes after approval flips the dot back to Working. Existing hook
|
||
installs surface as Outdated in Settings → Agents with an Update
|
||
button. (#99)
|
||
|
||
## [0.16.1] - 2026-07-15
|
||
|
||
### Fixed
|
||
|
||
- **Sidebar diff overlay only opens from the `+N`/`−N` counts** — clicking
|
||
anywhere on a tab row's git line (branch icon, branch name) used to toggle
|
||
the diff overlay, hijacking ordinary clicks on the lower half of the row.
|
||
Now only the diff counts are the click target; the rest of the line
|
||
activates the tab like the rest of the row. (#95)
|
||
|
||
## [0.16.0] - 2026-07-15
|
||
|
||
### Added
|
||
|
||
- **Click a sidebar git line to open a working-tree diff overlay** — the
|
||
branch/diff row in the sidebar is now clickable and opens an in-app overlay
|
||
showing the working-tree diff against `HEAD`, file by file with expandable
|
||
hunks. It rides the shared git-status signal: when fresh numbers land that
|
||
disagree with what it shows, it re-probes the full diff so the overlay stays
|
||
live. (#92)
|
||
- **Window size and position are remembered across launches** — tty7 saves
|
||
the window geometry on quit and restores it next launch, re-centering if the
|
||
saved bounds no longer overlap any display. Can be toggled off with the
|
||
`remember_window_size` config key. (#94)
|
||
|
||
### Fixed
|
||
|
||
- **Attach replay no longer duplicates TUI output into scrollback** — the
|
||
daemon's replay ring is now segmented by the geometry its bytes were
|
||
recorded under, and attach replays a `Size` → `Snapshot` pair per segment.
|
||
Previously the whole ring replayed at the final PTY width, so any resize
|
||
during a session (a pane split, a window drag) re-wrapped older output and
|
||
a TUI's cursor-up redraws (Claude Code's inline renderer, most visibly)
|
||
landed mid-frame, flooding the reattached pane's scrollback with stale
|
||
frame copies that never existed live. The ring also caps its segment count
|
||
so a long-lived session with many resizes can't grow it without bound. (#91)
|
||
- **Agent hooks no longer hang when stdin is a terminal** — the hook runner
|
||
skips the stdin read when fd 0 is a tty, so a hook invoked interactively
|
||
(rather than with a piped payload) emits the bare event instead of blocking
|
||
forever on a read that never returns. (#93)
|
||
|
||
## [0.15.0] - 2026-07-15
|
||
|
||
### Added
|
||
|
||
- **Daemon protocol version handshake** — the GUI asks a running daemon
|
||
which wire protocol it speaks before reusing it; after an app upgrade a
|
||
mismatched daemon is kept alive and a prompt offers Keep Sessions or
|
||
Restart Daemon instead of silently killing every persisted session. (#90)
|
||
|
||
### Changed
|
||
|
||
- **Tab close affordance** hides until hover on the active tab too, so the
|
||
sidebar and tab strip read clean. (#90)
|
||
- **Command palette** no longer offers the Claude-only hook install entry;
|
||
Settings → Agents owns hook installs with per-agent state.
|
||
|
||
### Fixed
|
||
|
||
- **SSH connection state** shows as a corner status dot on the tab avatar
|
||
(amber connecting, green connected, red failed) in the same semantic
|
||
colors as agent dots, replacing a theme-grey border ring that read as no
|
||
state at all. (#90)
|
||
- **Sidebar git branch/diff line** is shared per repository: panes in one
|
||
work tree read one snapshot refreshed by whichever pane probed last, so
|
||
rows for the same directory no longer show stale or missing +/− counts.
|
||
(#90)
|
||
- **Command palette** no longer pins Connect/Save rows above command
|
||
matches for bare words like `java`; QuickConnect rows require a
|
||
host-like query (`@`, `:` or `.`). (#90)
|
||
|
||
## [0.15.0-beta.1] - 2026-07-15
|
||
|
||
### Added
|
||
|
||
- Per-agent hook integrations (Claude Code, Codex, Copilot CLI, OpenCode,
|
||
Pi) with install state and actions in Settings → Agents. (#87)
|
||
- CLI coding agents and the git branch are recognized and shown in the
|
||
sidebar. (#85)
|
||
- Multi-line prompt editor, plus an I-beam mouse pointer over text. (#80)
|
||
- SSH: Unix GSSAPI (Kerberos) authentication. (#81)
|
||
|
||
### Changed
|
||
|
||
- Splitting an SSH pane opens another SSH pane on the same host. (#83)
|
||
|
||
### Fixed
|
||
|
||
- The grid shifts up when wrapped command input overflows the bottom of
|
||
the screen, keeping the caret visible. (#86)
|
||
- Each tab keeps its own active pane across tab switches. (#84)
|
||
- The theme panel stays on-screen on narrow windows. (#82)
|
||
|
||
## [0.14.0] - 2026-07-14
|
||
|
||
### Added
|
||
|
||
- SSH connection manager: a native russh client with saved connection
|
||
profiles, password and public-key auth, port forwarding, and SFTP. (#74)
|
||
- Buffer search overhaul — richer in-terminal search with rebindable,
|
||
cross-platform shortcuts. (#75)
|
||
- Vertical tab sidebar, and Settings reworked into a full-window page. (#70)
|
||
- Tab title now follows the active pane. (#73)
|
||
- New Settings controls: bell, notify threshold, mouse reporting, and
|
||
session restore. (#68)
|
||
|
||
### Changed
|
||
|
||
- SSH saved profiles are now the single source of truth for connections. (#77)
|
||
- Bump memchr 2.8.2 → 2.8.3. (#69)
|
||
|
||
### Fixed
|
||
|
||
- SSH auth-sheet polish and softer primary buttons. (#79)
|
||
- The Cmd+F find bar now owns the top-right slot over the SSH action
|
||
icons. (#76, #78)
|
||
- Windows: stop the daemon before install/uninstall so it can replace
|
||
`tty7.exe`. (#72)
|
||
- Moved SSH forwards into the pane context. (#71)
|
||
|
||
## [0.13.0] - 2026-07-13
|
||
|
||
### Added
|
||
|
||
- SSH loopback forwarding for links. (#58)
|
||
- Editable keybindings: rebindable shortcuts, pane/tab actions, and a tmux
|
||
preset. (#65)
|
||
|
||
### Fixed
|
||
|
||
- Trim the first tab's left gap flush to the traffic-light reserve. (#62)
|
||
- Remove the active-pane corner indicator dot. (#63)
|
||
- CI: format code and platform-gate the ctrl glyph in the keymap test. (#67)
|
||
|
||
## [0.12.0] - 2026-07-13
|
||
|
||
### Added
|
||
|
||
- Ship a Linux AppImage alongside the tarball. (#55)
|
||
- Title-bar overflow menu for the command palette and settings. (#57)
|
||
|
||
### Changed
|
||
|
||
- Redesigned theme picker with a slide-in panel. (#56)
|
||
|
||
### Fixed
|
||
|
||
- Stop the title-bar strip from clipping the Windows close button. (#60)
|
||
- Restore the original terminal-window logo, reverting the branding
|
||
change. (#59)
|
||
|
||
## [0.11.0] - 2026-07-12
|
||
|
||
### Added
|
||
|
||
- File-based themes, an in-app theme editor, and a UI/branding refresh. (#54)
|
||
|
||
## [0.10.0] - 2026-07-11
|
||
|
||
### Added
|
||
|
||
- Tab completion now executes the completion specs' *dynamic generators*:
|
||
positions whose candidates come from the live system get real values — git
|
||
branches on `git checkout <Tab>`, container names for docker/podman,
|
||
`package.json` scripts for npm/pnpm/bun/yarn, cargo/rustup/tmux/brew/apt/pip
|
||
listings, and more. Scripts run off the main thread in the session's cwd
|
||
(800 ms timeout, output capped, short-lived cache) and their results merge
|
||
into the already-open menu as they arrive; a slow or failing generator just
|
||
contributes nothing. (#52)
|
||
- When shell integration never engages in a pane, pressing Ctrl+R now explains
|
||
why the history menu can't appear (once per pane, dismissed by the next
|
||
keystroke) instead of failing silently — naming the wrapper when a
|
||
figterm-style PTY shim (`kiro-cli-term`, `figterm`, `qterm`) is intercepting
|
||
the shell's OSC 133 reports. The chord still reaches the shell, so its own
|
||
reverse-i-search keeps working. (#46)
|
||
|
||
### Fixed
|
||
|
||
- `ssh <Tab>` (and scp/sftp/rsync) now completes host aliases from
|
||
`~/.ssh/config` — `Include` files honored, wildcard patterns skipped — and
|
||
hosts from `known_hosts`, instead of falling back to listing the current
|
||
directory. (#51)
|
||
|
||
## [0.9.0] - 2026-07-10
|
||
|
||
### Changed
|
||
|
||
- Ctrl+R history search is now a browsable menu: matching is fuzzy
|
||
(subsequence with word-boundary/consecutive bonuses; space-separated terms
|
||
must all match) blended with frecency, and the ranked candidates float
|
||
beside the prompt — matched characters highlighted, selection moved by
|
||
Ctrl+R/↓ and Ctrl+S/↑, Enter to edit, Cmd+Enter to run outright. An empty
|
||
query lists the whole history by frecency, so bare Ctrl+R is a "recent &
|
||
relevant" browser. The classic `(reverse-i-search)` line stays. (#45)
|
||
- History records now carry when the command ran and its exit code: new
|
||
entries are `<ts>\t<exit>\t<cwd>\t<command>`, written when the command
|
||
*finishes* (zsh `INC_APPEND_HISTORY_TIME`-style, exit code sniffed from
|
||
OSC 133;D daemon-side); older formats still load. The Ctrl+R menu shows
|
||
"ran 3h ago" and a `✗` badge on commands whose last run failed; timestamps
|
||
from zsh/bash history files are carried over when seeding. (#45)
|
||
|
||
## [0.8.0] - 2026-07-10
|
||
|
||
### Added
|
||
|
||
- Copy on select: an opt-in Settings → Terminal → Clipboard toggle (config
|
||
key `copy_on_select`) that copies a mouse selection — drag, double-click
|
||
word, or triple-click line, over terminal output or the prompt's command
|
||
editor — to the clipboard the moment the gesture ends, no ⌘C needed. Off
|
||
by default so a stray selection never overwrites the clipboard. (#34)
|
||
|
||
### Fixed
|
||
|
||
- The held-⌘ tab-number badges no longer stick on after the window loses key
|
||
status mid-hold (⌘-Tab, Spotlight, a click into another app). The ⌘ release
|
||
goes to whatever app is key by then, so the window never saw it; the badges
|
||
— and any pending reveal — are now dismissed on the activation flip itself.
|
||
|
||
## [0.7.0] - 2026-07-10
|
||
|
||
### Added
|
||
|
||
- Terminal ANSI colors (`color0`–`color15`) can now be overridden individually
|
||
via `ansi_colors.*` in `config.json`, layered on top of the active theme
|
||
preset — with a color picker per slot under Settings → Appearance → ANSI
|
||
Colors. Malformed values are ignored, and clearing an override falls back to
|
||
the preset's palette. (#37)
|
||
|
||
- Font ligatures can now be enabled for terminal text. A new optional
|
||
`font_features` config passes OpenType features (e.g. `{"calt": true}`)
|
||
through to the renderer, and Settings → Appearance grows a toggle for the
|
||
common `calt`/`liga` pair. Ligatures stay disabled by default for cell-grid
|
||
safety, and changes hot-apply to open panes. (#38)
|
||
|
||
### Fixed
|
||
|
||
- Ctrl+L now clears the screen while the prompt-local line editor is active.
|
||
The readline dispatcher used to swallow it as an unrecognized chord; it now
|
||
forwards the same form-feed byte the raw terminal path sends, so the shell
|
||
repaints its prompt as expected. (#36)
|
||
|
||
## [0.6.2] - 2026-07-08
|
||
|
||
### Changed
|
||
|
||
- Context menus and the "+" dropdown now highlight the hovered row with the
|
||
same soft fill the command palette uses for its selected row, instead of the
|
||
stock saturated accent that snapped hard against the rest of the UI. The
|
||
hover text stays at the normal foreground so it reads clearly on the quieter
|
||
fill.
|
||
|
||
### Fixed
|
||
|
||
- On Windows, a pane no longer hangs open when its shell exits on its own.
|
||
Typing `exit`, pressing Ctrl-D, or a shell crash ends the shell, but ConPTY's
|
||
output pipe never reports EOF on a natural exit — and tty7 detected a shell's
|
||
death solely from that EOF — so the pane was left wedged open, dead but
|
||
visible. A Windows-only monitor now waits on the shell process directly and
|
||
reports the exit through the same path a Unix `read()` EOF drives, so the pane
|
||
closes as it does everywhere else. Closing a tab from the UI was already
|
||
unaffected; macOS and Linux are unchanged. (#30)
|
||
|
||
- Nerd Font prompt icons no longer render sliced off on the right. A non-Mono
|
||
Nerd Font (and the proportional `➜`/`❯` the OS cascade hands back when nothing
|
||
in your font list covers them) gives an icon a single-cell *advance* but draws
|
||
ink up to ~1.9 cells wide, and tty7 clipped every lone glyph to exactly one
|
||
cell — severing the overflow into the half-icons and cut-off arrow from the
|
||
report. A single glyph now paints into a two-cell window, so it renders whole
|
||
(bleeding into the trailing blank the way iTerm2 and Terminal.app do), bounded
|
||
at two cells so a stray face can't smear across the row. Pairs with the native
|
||
powerline separators from #19; Mono Nerd Fonts are unchanged. (#17)
|
||
|
||
- New tabs and splits no longer stall for seconds while a zsh plugin manager
|
||
reinstalls itself. tty7 launches zsh through a throwaway `ZDOTDIR` (so it can
|
||
layer its shell integration on top of your config), but it used to leave
|
||
`ZDOTDIR` pointing at that empty temp dir the whole time — so tools that find
|
||
their own state via `${ZDOTDIR:-$HOME}` (Zim, oh-my-zsh, `compinit`'s
|
||
`.zcompdump`) looked in the wrong place and rebuilt from scratch on every
|
||
pane, e.g. Zim reprinting `modules/…: Installed` and hanging for ~3s. Each
|
||
redirector now points `ZDOTDIR` back at your real config dir while your
|
||
startup files run, and restores it for the live session, so plugin managers
|
||
and completion caches resolve correctly and load instantly. As a bonus this
|
||
also fixes the classic relocated-config layout (a tiny `~/.zshenv` that sets
|
||
`ZDOTDIR=~/.config/zsh`), which previously loaded your config but silently
|
||
dropped tty7's integration. (#15)
|
||
|
||
## [0.6.1] - 2026-07-08
|
||
|
||
### Fixed
|
||
|
||
- Tab completion (and other line editing) now stays out of the way over `ssh`.
|
||
A remote shell that emits its own prompt marks — fish 4.x on a Linux server,
|
||
most visibly, which ships OSC 133 on by default — used to engage tty7's
|
||
*local* line editor, so Tab ran completion against the local machine's
|
||
filesystem instead of reaching the remote shell. tty7 now only drives the
|
||
inline editor while the shell it launched is itself idle at its prompt;
|
||
whenever a foreground command (ssh, a TUI, a nested shell) owns the terminal,
|
||
keystrokes pass straight through to it. (#26, follow-up to #18)
|
||
|
||
## [0.6.0] - 2026-07-08
|
||
|
||
### Added
|
||
|
||
- The "+" button now opens a shell picker: tty7 detects the shells installed
|
||
on this machine (on Unix the login shell, `/etc/shells`, plus well-known
|
||
shells found on `PATH` — fish, nushell, pwsh and friends installed by
|
||
Homebrew/nix are never registered in `/etc/shells`; on Windows PowerShell 7,
|
||
Windows PowerShell, Command Prompt, Git Bash and WSL distributions)
|
||
and lists them in a dropdown, so opening a tab in a different shell
|
||
no longer requires editing `config.json`. The default entry leads the menu,
|
||
⌘T / Ctrl+T still opens a default tab in one keystroke, and splitting a pane
|
||
inherits its shell — a fish tab splits into more fish, not back to the
|
||
default. Shells picked this way aren't remembered across restarts (restored
|
||
panes re-attach to their still-running shells anyway).
|
||
|
||
### Changed
|
||
|
||
- The Windows default shell now prefers PowerShell 7 (`pwsh.exe`) when
|
||
installed — probed across Program Files (x64/x86/ARM), the Microsoft Store,
|
||
scoop, dotnet tools and `PATH` — and falls back to Windows PowerShell as
|
||
before. Set `shell` in `config.json` to override, as ever.
|
||
|
||
### Fixed
|
||
|
||
- Powerline prompt separators (powerlevel10k, oh-my-posh, oh-my-zsh) now render
|
||
pixel-perfect at any font, size and line-height: the eight solid separators
|
||
(sharp triangles, round caps, slants) are drawn natively as fill paths sized
|
||
to the exact cell instead of relying on a Nerd Font, so segments meet their
|
||
backgrounds cleanly with no gaps, narrow wedges or tofu. The bundled Hack font
|
||
is also appended to every font-fallback chain, so common prompt glyphs (➜, ❯,
|
||
box drawing) no longer render truncated or missing when no Nerd Font is
|
||
installed. (#17)
|
||
- A URL glued directly to a full-width bracket with no space — e.g.
|
||
`…/pull/343(fix/… → dev)` — no longer swallows the bracket text into the
|
||
link. URL detection now stops at the first non-URL character (a CJK glyph,
|
||
full-width bracket, arrow or emoji), while interior ASCII parens
|
||
(Wikipedia-style URLs) are still preserved.
|
||
- Fish, nushell, pwsh and other shells installed by Homebrew or nix now appear
|
||
in the "+" shell picker even when they aren't registered in `/etc/shells`
|
||
(which those package managers leave to the user): a curated set of well-known
|
||
shells is now probed on `PATH` as a catch-all, after the `/etc/shells`
|
||
entries. (#18)
|
||
- Upgrading tty7 while an older daemon is still running in the background no
|
||
longer breaks new tabs. A stale daemon that accepts the connection but can't
|
||
serve the new client's request is now restarted once and retried
|
||
automatically; on macOS the GUI also forwards the shell it was launched with
|
||
to the detached daemon, so panes use the right shell instead of a stale
|
||
`$SHELL` inherited from LaunchServices.
|
||
|
||
## [0.5.0] - 2026-07-07
|
||
|
||
### Added
|
||
|
||
- Windows releases now ship an Inno Setup installer
|
||
(`tty7-<version>-windows-x86_64-setup.exe`) alongside the portable zip. It
|
||
installs per-user by default (no admin prompt, with an all-users option),
|
||
adds a Start Menu shortcut and an "Apps" uninstall entry, and offers an
|
||
optional desktop icon. Still unsigned, so SmartScreen warns on first launch.
|
||
- Startup update check: tty7 asks GitHub once, in the background, whether a
|
||
newer release has shipped. If so, it pops a one-time "Update available" dialog
|
||
(once per version — remembered in `update.json`, so it never nags twice for
|
||
the same release) and keeps a persistent "Download" prompt in Settings →
|
||
About. Both open the Releases page; tty7 never downloads or updates itself —
|
||
you still install by hand. Turn the check off with `check_for_updates` in
|
||
`config.json` or the "Check for updates on launch" toggle in About. A failed
|
||
or offline check is silent.
|
||
- ⌘K (Ctrl+K on Windows/Linux) clears the screen and scrollback — the same
|
||
"Clear" the right-click menu already offered, now on the keyboard shortcut
|
||
Terminal.app, iTerm2, and Ghostty users expect. Also available from the
|
||
command palette, and remappable as `ClearScrollback` in `keybindings`.
|
||
- ⌘⏎ toggles window fullscreen (new `ToggleFullscreen` action, also in the
|
||
View menu and command palette), matching the Ghostty/iTerm2 default. It
|
||
previously toggled pane maximize — which silently did nothing in a
|
||
single-pane tab, so the chord felt dead.
|
||
- The right-click menu now shows each item's keyboard shortcut. Copy, Paste,
|
||
Select All, and Find previously showed nothing (they're dispatched inline,
|
||
with no bound key for the menu to read a hint from) while the other items
|
||
did, so the menu looked half-labelled. ⌘A / ⌘F stay hint-less on
|
||
Windows/Linux, where those chords keep their readline meaning.
|
||
|
||
### Changed
|
||
|
||
- Maximize / restore pane moved from ⌘⏎ to ⌘⇧⏎ (Ghostty's `toggle_split_zoom`
|
||
default), making room for fullscreen on the bare chord. An existing
|
||
`ToggleMaximizePane` override in `keybindings` still wins.
|
||
|
||
### Fixed
|
||
|
||
- Windows: launching tty7 no longer opens a stray console window behind the
|
||
app. Release builds are now linked with the `windows` subsystem; debug
|
||
builds keep the console so `println!` output stays visible. (#10)
|
||
- The right-click "Select All" now matches the ⌘A shortcut: at the prompt it
|
||
selects the edited command line, otherwise the whole terminal buffer. It
|
||
previously always selected the whole buffer, so click and keystroke behaved
|
||
differently at the prompt.
|
||
- Ctrl+R reverse-search now accepts plain ASCII keystrokes. The query only
|
||
took text from the IME commit path, so a non-CJK input source on macOS — and
|
||
all typing on Linux — was swallowed: the search box opened but ate every key.
|
||
Reported on V2EX.
|
||
|
||
## [0.3.0] - 2026-07-07
|
||
|
||
### Added
|
||
|
||
- PowerShell shell integration: `powershell.exe` and `pwsh` now emit the OSC 133
|
||
semantic-prompt marks and OSC 7 cwd that zsh/bash/fish already do, injected
|
||
via `-EncodedCommand` after the user's profile loads (their config is never
|
||
touched). This turns on the inline line editor at the PowerShell prompt — so
|
||
clicking positions the caret and new tabs/splits inherit the working
|
||
directory — which is what previously made mouse clicks a no-op at the prompt
|
||
on Windows.
|
||
|
||
### Fixed
|
||
|
||
- Typing `exit` (or Ctrl-D) left a dead "process exited" pane behind instead
|
||
of closing it. A pane whose shell genuinely ends now closes itself —
|
||
collapsing its split, or closing the tab when it was the only pane (the
|
||
last tab falls back to the home page), like every other terminal. A pane
|
||
that merely *lost its daemon connection* still stays visible: auto-closing
|
||
those would silently discard — and kill — sessions that may still be alive
|
||
daemon-side. Panes that died while detached clean themselves up on the next
|
||
attach the same way.
|
||
|
||
- A full-screen TUI dying without restoring the terminal — the canonical case
|
||
being an ssh session dropping mid-`htop`/`vim` — left the pane stranded on
|
||
the alt screen with a hidden cursor and live mouse reporting: a visible
|
||
prompt with no cursor anywhere, mouse clicks echoing `0;19;42M`-style junk,
|
||
and broken scrollback. The client now scrubs this residue the moment the
|
||
shell reports its next prompt (OSC 133): it leaves the stranded alt screen,
|
||
re-shows the DECTCEM-hidden cursor, and disables stale mouse/focus reporting
|
||
and kitty keyboard flags — each reset only when its mode is actually set.
|
||
Reattach self-heals the same way, since the daemon replays the prompt state
|
||
after the ring.
|
||
|
||
- Windows shell integration never engaged even for the default shell: detection
|
||
keyed off `portable-pty`'s `get_shell()`, which reports `%ComSpec%` (cmd.exe)
|
||
regardless of what's actually spawned, so the PowerShell default was mistaken
|
||
for an unsupported shell. It now resolves to `powershell.exe` directly.
|
||
|
||
## [0.2.0] - 2026-07-04
|
||
|
||
### Added
|
||
|
||
- Underline styles: undercurl, double, dotted, and dashed underlines render distinctly.
|
||
- `config.json` hot reload — edits apply to the running app without a restart.
|
||
- Desktop notifications driven by OSC 9 / OSC 777 escape sequences.
|
||
- Kitty keyboard protocol (CSI u progressive enhancement) for TUI apps like Neovim and Helix.
|
||
- Shell integration for bash and fish, alongside the existing zsh support.
|
||
- Windows support: cross-platform daemon, PowerShell as the default shell, embedded app icon.
|
||
- Linux support: builds against gpui's x11/wayland backends, `/proc`-based foreground cwd + pane-title tracking, Linux CI job, and documented build dependencies.
|
||
- Downloadable builds for every platform: the release workflow now packages and uploads all four targets — signed/notarized macOS DMGs (arm64 + x86_64) plus unsigned archives for Windows (`.zip`) and Linux (`.tar.gz`), each via its own `.github/scripts/bundle-<os>` script.
|
||
- Settings UI: terminal / appearance / behavior options are configurable from the GUI, with a searchable font-family dropdown and a wider theme gallery.
|
||
- Configurable default shell.
|
||
|
||
### Changed
|
||
|
||
- Project renamed to **tty7**.
|
||
- macOS releases ship as drag-to-Applications DMGs instead of zips, and the
|
||
Intel build moved to the `macos-15-intel` runner (`macos-13` was retired,
|
||
which had silently kept x86_64 assets from ever publishing).
|
||
- Pixel-smooth scrollback: scrolling carries a sub-line fraction and shifts the paint instead of jumping whole lines.
|
||
- Smoother scrolling on dense screens: glyph shaping is batched and wakeups are coalesced.
|
||
- CJK-dense screens paint ~2.4× faster: consecutive wide glyphs batch into single shaped runs (two columns per glyph) instead of painting cell-by-cell; the grid snapshot buffer is reused across frames and the selection/search overlay scans are skipped when nothing is highlighted. Release builds now use thin LTO.
|
||
- Type-ahead is integrated into the line editor instead of being stranded on zle's line.
|
||
- New tabs open next to the active tab instead of at the end.
|
||
- Terminal throughput ~12× faster (11 MB `cat`: ~2.0 s → ~0.16 s; DOOM-fire: ~47 fps → ~920 fps, both at 155×40 on an M1 Pro — now ahead of Alacritty/Ghostty on the same machine): the daemon's replay ring is a `VecDeque` so a full ring no longer memmoves 8 MiB per ~1 KiB PTY read, and the per-connection writer coalesces queued `Output` frames (≤256 KiB) so a flood reaches the client as a few large frames instead of thousands of tiny ones. A backpressure gate (4 MiB high-water) pauses the PTY reader while the client catches up, so a runaway `yes` can't grow daemon memory without bound. `TTY7_TRACE=1` prints per-second reader-loop accounting on both sides for future diagnosis.
|
||
- Second throughput pass, another ~1.4× on bulk output (11 MB `cat`: ~160 ms → ~100 ms; sustained plaintext drain 124 → 148 MB/s, vs ~170 MB/s for a raw do-nothing PTY reader on the same machine; DOOM-fire is unchanged — it is producer-bound at ~96 MB/s): the backpressure high-water grows to 16 MiB so a big burst drains at PTY speed while the client parses in its own time; daemon⇄GUI socket buffers grow from macOS's 8 KiB default to 256 KiB; the client applies consecutive `Output` frames as one batched parser pass (one term-lock + wakeup per burst, latency-free — the batch never waits for unarrived bytes); the shared OSC tokenizer skips Ground/Ignore runs with SIMD `memchr`; the gate's hot path is a lock-free atomic (previously a Mutex plus an unconditional `notify_all` per socket write); and the four threads on the interactive output path ask macOS for `USER_INTERACTIVE` QoS to stay off the efficiency cores (`TTY7_NO_QOS=1` opts out).
|
||
|
||
### Fixed
|
||
|
||
- A long `--config-dir` path crashed the GUI at startup ("path must be shorter than SUN_LEN"): when `<config>/daemon.sock` would exceed the OS socket-path limit (104 bytes on macOS), the endpoint now falls back to a short per-user path keyed by a stable hash of the config dir ($XDG_RUNTIME_DIR, else the OS temp dir). Short paths keep the original layout, so existing daemons stay reachable.
|
||
- Typing right after a command finished could leave a stray echoed character plus zsh's reverse-video `%` in the scrollback: the "command finished" mark (OSC 133;D) is now emitted the instant the command exits — prepended ahead of the user's precmd hooks (zsh/bash) — instead of after slow prompt frameworks (oh-my-zsh git status, conda), so the local input editor takes keystrokes back hundreds of milliseconds sooner.
|
||
- Typing while a command was still running stranded those keystrokes on zle's line at the next prompt — un-editable and double-drawn under the line editor's overlay. Type-ahead adoption (wipe the shell's line, seed the editor) now runs at every prompt, not just the shell's first, and the wipe waits until zle is actually reading (the live `133;B` mark) so it is consumed silently instead of being kernel-echoed into the scrollback as a literal `^U`.
|
||
- Typing ahead of a fast command left kernel-echoed debris in the scrollback (`ls` plus zsh's reverse-video `%`). Reconstructable gap input is now held client-side for up to 150 ms: a command that finishes inside the window hands the keystrokes straight to the line editor with the PTY untouched — zero echo; a longer command (or one reading stdin) gets the bytes released verbatim, so REPLs and password prompts still work.
|
||
- fish shell integration silently never installed, so fish users got no prompt marks or cwd tracking.
|
||
- **Security:** pasted clipboard content is stripped of ESC bytes, closing a bracketed-paste escape that could inject auto-executing commands.
|
||
- Crash when copying/cutting right after a forward word/line delete left a stale selection anchor.
|
||
- `Ctrl+Alt+<letter>` was indistinguishable from `Ctrl+<letter>` because the legacy key encoder dropped the Alt ESC prefix.
|
||
- Plain Enter/Tab/Backspace were wrongly CSI-u-encoded at the kitty-keyboard DISAMBIGUATE level, which could wedge the shell after a crashed TUI.
|
||
- No-op edits (e.g. Backspace at the start of the line) no longer swallow the first undo.
|
||
- OSC scanners (daemon-side and notification-side) dropped a well-formed sequence that directly followed an unterminated one.
|
||
- Daemon pane teardown is hardened: process-group kill, bounded join, dead panes are reclaimed.
|
||
- New shells default to `$HOME` when launched from the app bundle with cwd `/`.
|
||
|
||
## [0.1.0] - 2026-06-30
|
||
|
||
Initial release.
|
||
|
||
- Sessions live in a persistent daemon and survive window close / app restart.
|
||
- GPU-rendered terminal grid on [gpui], backed by Zed's `alacritty_terminal` fork.
|
||
- Tabs and pane splits (split right/down, maximize, focus movement).
|
||
- Command palette with fuzzy search over every action.
|
||
- Smart line editing: inline completion, syntax highlighting, history, in-terminal search.
|
||
- zsh shell integration (OSC 7 cwd + OSC 133 prompt marks) via a throwaway `ZDOTDIR`.
|
||
- Native macOS light/dark themes that follow the system appearance.
|
||
|
||
[26.9.1]: https://github.com/l0ng-ai/tty7/compare/v26.9.0...v26.9.1
|
||
[26.9.0]: https://github.com/l0ng-ai/tty7/compare/v26.8.3...v26.9.0
|
||
[26.8.3]: https://github.com/l0ng-ai/tty7/compare/v26.8.2...v26.8.3
|
||
[26.8.2]: https://github.com/l0ng-ai/tty7/compare/v26.8.1...v26.8.2
|
||
[26.8.1]: https://github.com/l0ng-ai/tty7/compare/v26.8.0...v26.8.1
|
||
[26.8.0]: https://github.com/l0ng-ai/tty7/compare/v26.7.6...v26.8.0
|
||
[26.7.6]: https://github.com/l0ng-ai/tty7/compare/v26.7.5...v26.7.6
|
||
[26.7.5]: https://github.com/l0ng-ai/tty7/compare/v26.7.4...v26.7.5
|
||
[26.7.4]: https://github.com/l0ng-ai/tty7/compare/v26.7.3...v26.7.4
|
||
[26.7.3]: https://github.com/l0ng-ai/tty7/compare/v26.7.2...v26.7.3
|
||
[26.7.2]: https://github.com/l0ng-ai/tty7/compare/v26.7.1...v26.7.2
|
||
[26.7.1]: https://github.com/l0ng-ai/tty7/compare/v26.7.0...v26.7.1
|
||
[26.7.0]: https://github.com/l0ng-ai/tty7/compare/v0.17.0...v26.7.0
|
||
[0.10.0]: https://github.com/l0ng-ai/tty7/compare/v0.9.0...v0.10.0
|
||
[0.9.0]: https://github.com/l0ng-ai/tty7/compare/v0.8.0...v0.9.0
|
||
[0.8.0]: https://github.com/l0ng-ai/tty7/compare/v0.7.0...v0.8.0
|
||
[0.7.0]: https://github.com/l0ng-ai/tty7/compare/v0.6.2...v0.7.0
|
||
[0.6.2]: https://github.com/l0ng-ai/tty7/compare/v0.6.1...v0.6.2
|
||
[0.6.1]: https://github.com/l0ng-ai/tty7/compare/v0.6.0...v0.6.1
|
||
[0.6.0]: https://github.com/l0ng-ai/tty7/compare/v0.5.0...v0.6.0
|
||
[0.5.0]: https://github.com/l0ng-ai/tty7/compare/v0.4.0...v0.5.0
|
||
[0.4.0]: https://github.com/l0ng-ai/tty7/compare/v0.3.0...v0.4.0
|
||
[0.3.0]: https://github.com/l0ng-ai/tty7/compare/v0.2.0...v0.3.0
|
||
[0.2.0]: https://github.com/l0ng-ai/tty7/compare/v0.1.0...v0.2.0
|
||
[0.1.0]: https://github.com/l0ng-ai/tty7/releases/tag/v0.1.0
|
||
[gpui]: https://github.com/zed-industries/zed
|