* feat(panel): CPU% and memory per process, and a total, in Info → Processes
The daemon adds rss, cpu_ns and a start stamp to each ProcEntry (serde
default, so an old daemon or GUI skips them). The GUI turns two CPU-time
samples into a ps-style % and sums both into a Total line. `tty7 procs`
gains an RSS column.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(panel): no CPU% across a poll gap, and test CPU time is in ns
A new poll chain starts after the panel was shut or the pane changed;
comparing against the sample from before the gap showed a long-run average
as the current figure. Reset the tracker so the first round shows a dash.
Replace the self-usage test's cpu_ns > 0 (fails on Linux before the first
10 ms tick, passes unconverted mach ticks) with a check that the process's
CPU time covers this thread's own burned CPU time.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(sidebar): Move to Group is a submenu listing every sidebar group
The tab menu's Move to Group listed only pinned groups, laid out flat on
the grounds that there are never many. A sidebar that auto-groups by repo
holds dozens of groups, none of which could be picked. It is now a submenu
with every group the sidebar draws, in sidebar order, the tab's current one
checked, then Ungrouped (back to auto grouping) and New Group. Picking an
auto group pins it, as its header's pin does, and keeps the tab there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(sidebar): Move to Group review fixes
- Ungrouped row replaced by Remove from Group, enabled only for a tab kept
in a group; move_targets is pure (no locale lookup) over GroupKey.
- move_tab_to: an auto group is pinned with the tab in one edit
(pin_auto_group_with), gpui-tested.
- A separator splits pinned from auto groups; targets are taken before the
submenu borrows cx.
- No CHANGELOG hunk; the description carries it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(sidebar): Move to Group comment and pin_auto_group_with tidy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(sidebar): Move to Group's labels line up with the tab menu
One left-checked row makes gpui_component reserve a check column on every
row of that menu, so the whole submenu sat an icon's width right of the tab
menu it hangs off. Putting the check on the right keeps the labels at the
parent's inset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(sidebar): pinning a repo whose folder is kept joins the kept group
A tab dragged out of a folder group while still in the repo is drawn
under the repo's auto group, beside the folder group keeping the same
directory. Move to Group lists both; picking the auto one (or its
header's pin) pushed a second folder group on the same path, two
identical headers splitting the folder's tabs by list order. Join the
kept group instead, as pin_folder already does.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* feat(terminal): program notifications follow the policy; read kitty OSC 99
OSC 9/777 notifications used to post from the reader thread regardless of
focus or the Never setting, and clicking one revealed nothing. They now queue
for the view, which applies notify_on_command_finish (Unfocused holds one back
only while its pane is focused in the key window), posts it clickable for the
pane, and titles it with the agent's name when it brings none. A pane whose
agent reports through tty7's hooks skips the copies the hooks already cover.
Kitty's OSC 99 is read too, chunked by i= with d=/p=/e=, in the client and in
the daemon sniffer, so a hookless agent's kitty notification marks it Waiting.
Claude Code's ghostty, kitty and iterm2 Notifications channels all land here;
its default, auto, sends nothing under TERM_PROGRAM=tty7.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(terminal): agent hook notices follow the per-pane rule
An agent's Waiting and Done notices from tty7's hooks were only posted while
the window was in the background, so an agent in another tab of the front
window never reached you. They now share shows_notification with program
notifications: Never posts nothing, When unfocused holds a notice back only
while its pane is focused in the key window, Always always posts. A hooked
pane still skips its program's own copies, so nothing doubles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Revert "feat(terminal): agent hook notices follow the per-pane rule"
This reverts commit 7df400043c. The hook
notice change is a behaviour change of its own and moves to the stacked
branch upstream/hook-notices-per-pane.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(terminal): NotifyMode::allows, a Note type, and a cap on queued notes
One rule for every notification path: a mode allows a notice unless it is
Unfocused and the reader is watching. Kitty's pending chunks and a
finished note get names instead of tuples, and a pane whose view is not
polling keeps only its newest eight notes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(changelog): drop the Unreleased entry; the release notes carry it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(terminal): a burst of program notes shows its newest three, and an exiting shell's are shown
`take_osc_notes` hands over the newest three queued notes and drops the
rest, so a program that writes a burst doesn't spray the desktop. The view
shows them ahead of `poll_foreground`'s exit check, so what a program said
just before its shell exited still gets through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(terminal): one cap on queued program notes, three
The queue kept 8 and `take_osc_notes` then handed over the newest 3: two
caps for one rule. The queue now keeps 3 and `take_osc_notes` drains it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(terminal): at most five program notes per pane every ten seconds
The queue cap applies between polls, so a pane that keeps writing
notifications still posted a few every 300ms. Each pane now has a
NoteBudget: at most five notes per ten seconds reach the desktop, and the
rest are dropped and said once, as "More notifications from this pane
weren't shown", when the window turns over. The budget is asked on every
poll, held-back ones included, so that note comes even after the flood
stops and never for a count gone stale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(terminal): kitty control payloads and id-less chunks don't build a note; OSC 9;10-12 aren't notes
A p=close, p=alive or p=? with the id of an unfinished OSC 99 note
finished and posted it. They are commands about notifications, so they
now leave pending notes alone. Chunks without an i= are each their own
notification per the spec, and no longer join an earlier id-less d=0
chunk. ConEmu's OSC 9;10, 9;11 and 9;12 (a prompt mark some shells emit
every prompt) were posted as notifications; they are subcommands like
9;1-9;9.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(terminal): tell a pane's program when the theme turns light or dark (DEC 2031)
Claude Code's `theme: auto` reads OSC 11 at startup and re-reads it only
when the terminal sends `CSI ? 997 ; 1|2 n`, which it asks for with
`CSI ? 2031 h`. tty7 ignored 2031, so a running Claude kept its light diff
colours after a flip to dark. Track 2031 per pane (and across reattach),
push a 997 when the theme's background changes, and answer `CSI ? 996 n`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(terminal): report the colour scheme on reattach; review cleanups for DEC 2031
A pane reattached with 2031 on now hears the current scheme once the
replay is folded, so a theme flip while it was detached still reaches
the program. report() uses the presets' is_dark, watch reads
view.terminal directly, TRACKED lists 2031 inline, and the gpui test
moves into view.rs's gpui_tests harness with a reattach case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(changelog): drop the Unreleased entry; the release notes carry it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(terminal): one scheme report per reattach, not one per replayed frame
A replay is a modes Snapshot plus one Snapshot per ring segment, and each
sent its own 997. The Snapshot arm now only folds and flags; the report
goes out once, on the first frame after the replay. The tests use the
harness's next_input_until_timeout instead of a reader thread, and the
reattach test replays two Snapshots.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(terminal): send the reattach scheme report when the replay has been read
It went out on the first frame after the replay, so an idle pane, with no
live frame coming, never heard it. It is now sent once the reader's buffer
drains. TRACKED keeps its one line and names `COLOR_SCHEME_UPDATES` rather
than repeating 2031. The reattach test replays Snapshot, Size, Snapshot,
Snapshot with nothing after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(terminal): a shell prompt ends DEC 2031, so a dead program's mode never reports into the shell
A program that switched 2031 on and died without `?2031l` left the mode on
in the pane's tracked modes, its ring, and the client's fold. A reattach
replay then still ended with 2031 on, so the reader sent its post-replay
report, and every theme flip sent another, into the shell's command line.
`TerminalModes` now drops 2031 on an OSC 133 `A`, `B` or `D` mark: the
shell owns the terminal again, so whatever asked for reports is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(terminal): send the reattach scheme report after the replay, not at a read boundary
The reattach report went out whenever the reader drained what it had read.
An 8 MiB ring arrives over many reads, so a read that ended after the ring
segment holding a dead program's `?2031h` and before the one holding the
shell prompt that ended it typed `CSI ? 997 ; n` into the shell. The report
now waits for the first frame past the ring (the replayed Prompt, Cwd and
the rest, or live Output once it is folded).
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(themes): accept integer colour components in .itermcolors files
iTerm2 and plistlib write exact 0 and 1 as <integer>, which the loader
rejected, dropping the whole theme.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(changelog): drop the Unreleased entry; the release notes carry it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): selecting a sidebar tab no longer scrolls the list
activate() called sidebar_scroll.scroll_to_item(tab_index), but the
sidebar list's children are group blocks and dividers, not tab rows, so
the index named some other group and a click on a lower row jumped the
list. The row now brings itself into view when it is drawn: a row with
any part on screen stays put, one out of view scrolls in to the nearest
edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): key the sidebar reveal by tab id and drop it when the row is not drawn
An index went stale when tabs were reordered or closed before the next
frame, and a reveal aimed at a row in a folded group fired whenever the
group was opened, long after the tab was selected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(changelog): drop the Unreleased entry; the release notes carry it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): a first row revealed from above brings its group header
When the sidebar scrolled a row in from above, the row landed on the top
edge with its group's heading still cut off above it, so the tab came into
view without the name of the group it is in. The first row of a group that
draws a header now counts the header (and the gap under it) as part of
itself when coming in from above. Rows on screen and rows below are
unchanged.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
An agent reporting a file it wrote prints `原型:/tmp/a.html` or
`原型:/tmp/a.html` with no space. The label rule only accepted ASCII
labels behind an ASCII colon, so the whole token was read as a relative
path and the click reported it missing under the pane's directory.
Accept any alphabetic label, one character long when it is not ASCII,
behind either colon. A single ASCII letter stays a Windows drive.
Closing the last file left nothing in the editor panel to hold the focus,
so it fell back to the terminal pane and the next ⌘W closed the terminal
instead of the empty editor.
The empty panel now has its own focus handle: it takes the focus over in
the frame after its last file closes (only for the tab whose editor had
it), and a click on it focuses it. ⌘W from there, or from the file tree
an empty ⌘⇧E hands the focus to, hides the panel and returns the focus to
the terminal. With the focus in the terminal, ⌘W is still the terminal's.
Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
* fix(mobile): bump iroh to 1.3.0 so a stuck relay cannot stall direct dials
iroh 1.2.0 sends a connection's first datagrams to every known path one
after another inside the remote-state actor, awaiting each. When the relay
is unreachable its send queue fills and the actor blocks, so handshake
packets for a direct path that does work (a mesh VPN address, a public
IPv6) queue behind it and the dial times out. 1.3.0 sends to all paths
concurrently with a bounded wait (n0-computer/iroh#4512).
The desktop workspace was already on 1.3.0; the app has its own lockfile
and was left behind.
* fix(gateway): reach the relay through a proxy when that is the only way out
iroh dials its relay with its own resolver and its own TCP, ignoring the
system proxy. On a machine whose network only works through a local proxy
(Clash and the like, system-proxy or TUN mode alike) that dial never
succeeds, and nothing says so: phones on the same network still connect,
while a phone on cellular times out, because without a relay nothing
coordinates hole punching and the home router drops unsolicited inbound
packets.
The gateway now lists the ways out it knows of, most likely first:
tty7's own http_proxy setting, the system proxy, the environment's, then
direct. It starts on the first without waiting, and when the relay stays
unreachable for 10s it tries the others on a throwaway endpoint, switching
to the first that reaches a relay (same key, same port, so pairing codes
keep working). While none does, it looks again every minute.
A pairing code now names the relay only once it is actually connected;
before, it named whichever relay iroh picked by latency, reachable or not.
The platform proxy readers in daemon::install::proxy now also hand back
the proxy as a URL, for a client that is not ureq.
Claude-Session: https://claude.ai/code/session_018wE9ZRyxgWW2f55VSy9FvZ
Built from the files the night actually produced, so one that did not build is left out rather than linked dead. Links go through the rolling nightly tag.
Release and nightly now build the desktop app for aarch64-pc-windows-msvc
alongside x64, publishing tty7-<version>-windows-arm64-setup.exe and
tty7-<version>-windows-arm64.zip.
- Cross-compiled on the x64 Windows runner, like server-windows' ARM64
leg, and marked experimental until it has shipped once: a failure drops
the ARM64 packages instead of holding up the release.
- Vendors Microsoft's arm64 ConPTY pair from the same package version as
the x64 one, so ARM64 panes keep the OSC 11 fix (#345).
- Each Windows package bundles the WSL server for its own architecture.
- The installer's architecture comes in as a define: x64compatible for the
x64 build (still installable on ARM64 under emulation), arm64 for the
native one. One AppId, so either upgrades the other.
- The in-app updater on an ARM64 build asks for the arm64 packages; an x64
build under emulation keeps taking x64 ones.
Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
android init left the Android project on the template's icon. It is now an adaptive icon: the mark on a transparent layer, sized inside the safe circle any launcher mask keeps, over the tile's colour, with square and round ones for older launchers. Drawn from the same mark as the iOS icon; icons/android keeps the same files.
A laptop in UTC+8 numbered its builds eight hours ahead of CI, so a CI upload of the same version within eight hours of a local one came out lower and would be refused.
A `mobile-v<x.y.z>` tag now ships both platforms at one version: the
Android APK to a draft release as before, and an iOS build to
TestFlight. iOS no longer depends on one machine signed in to Xcode.
- The version is tauri.conf.json's; a tag that disagrees with it fails
the run, so the repo always says what shipped. Set to 0.1.1, the
Android build already out.
- scripts/testflight.sh signs and uploads with an App Store Connect API
key when ASC_KEY_ID and ASC_ISSUER_ID are set, with a certificate
Apple keeps in the cloud. That needs an Admin key.
- A manual run builds both and uploads nothing.
Restructure both READMEs around what tty7 is for: shells that outlive the
window, agents that drive other agents, and remote machines that work the
same way because they run the same server. Performance becomes a short
supporting section with concrete numbers; the rest of the feature list
collapses into one paragraph that links to the docs.
Also:
- Correct the persistence claim: shells survive quitting the app, not a
reboot; after a reboot the layout, the screen tail, and supported agent
conversations come back.
- Add a past-sessions column to the agent matrix, matching the agents
agent_history::scan reads.
- Drop the wrapper-script / agent_launch paragraph (already in the
configuration reference) and the separate full-quality video link.
- docs/window/search-everywhere.mdx: the Sessions tab has listed every
supported agent, on remote workspaces too, since #977; the page still
described only Claude Code and Codex on this computer. Also document the
Cmd/Ctrl-E row actions.
* feat(mobile): run on Android
The app builds and runs on Android, and fits there.
- Back button and gesture close what is open over the screen, then go
back a screen, and from the first one send the app to the background.
Left to the WebView they closed the app, since it has no history.
- The system bars: the page asks their size of the app (`insets`, over
JNI), since WebViews before 140 report the safe areas as 0 even edge
to edge. The CSS reads them as `--inset-*`, which iOS still fills
from `env()`.
- The keyboard: edge to edge, the WebView is not resized for it and its
visual viewport stays whole, so the keyboard covered the message box.
MainActivity hands its height to the page, which lays out as on iOS.
- The title folds into the bar from the scroll position on every screen.
The observer it used reported a title in plain view as out of sight on
Android, so the bar started folded.
- src-tauri/gen/android is kept in the repo for that MainActivity.
* ci(mobile): release a signed Android APK
A `mobile-v<x.y.z>` tag builds the app for arm64 at that version, signs
it with the release key and attaches it to a draft release. The mobile
app is versioned apart from the desktop's `v*` tags.
- The release is never marked latest: the desktop updater reads
/releases/latest and would take it for a desktop release.
- The APK's certificate is checked against the release key's, since one
signed with another key could not be installed over earlier ones.
- The NDK is pinned, and the version must be x.y.z: Tauri derives the
versionCode from it, and Android installs over a build only when that
is higher.
- Gradle signs a release build when keystore.properties names a key;
CI writes it from secrets.
- The release library is stripped: 30 MB to 20, the APK 32 to 23.
* ci: tell people how to install the phone app
The nightly release notes and each mobile-v release now say how to get
the app on a phone: the TestFlight link for iPhone, and the newest
mobile-v release's APK for Android. Both read .github/mobile-install.md.
* feat(tree): keep each workspace's recently closed tabs in the machine tree
A workspace now keeps the tabs closed from it in its machine tree
(`Workspace::closed`), so reopening one no longer depends on the window
that closed it still being open.
- `TabCloseRemembered` closes a tab into that list: the tab leaves the
workspace as a close takes it (other windows hear `TabClosed`), its panes
are stopped with their last screens kept on disk, and their records stay
in the pane list. Panes the client held that the tree never recorded are
ended outright.
- `TabReopen` takes the named or newest entry off the list and answers it
with its pane records, for the client to rebuild on fresh shells that
open on the old screens. The records it leaves behind are claimed by the
successors' seeds instead of being refused as duplicates.
- Entries last 24 hours and a workspace keeps the newest 20. The daemon's
scrollback keeper expires them on its existing timer, and an entry that
goes takes its records and stored screens with it.
- Both requests are gated on a new `closed-tabs` hello feature, offered only
by a peer that serves a tree and panes, so an older daemon or remote
server is never sent them.
The field is `#[serde(default)]` and skipped while empty, so older trees
load unchanged and an older build reads this one's.
Refs #1021
Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
* feat(tabs): reopen closed tabs from the machine, and a setting for when closing asks
Closing a tab now closes it into its workspace's recently-closed list on the
machine that holds the workspace, instead of into a list that ended with the
window. ⌘⇧T asks that machine for the newest entry, so a closed tab comes
back after quitting and relaunching the app, from any window of the
workspace, and in remote workspaces whose server keeps the list.
- The explicit close (⌘W, the tab's close button, bulk closes) registers the
tab with the window's next sync, which turns that tab's `TabClose` into a
`TabCloseRemembered`. The machine stops the panes and keeps their screens;
the window no longer kills them itself. A tab dragged to another window or
never rebuilt still goes out as a plain close.
- If the close cannot reach the machine (no `closed-tabs` feature, a window
that has not pulled its layout, a sync that fails or is thrown away), the
window falls back to what it did before: the tab goes on its own list and
its panes are killed from here.
- Reopening waits briefly for this window's queued edits to land, so an
immediate ⌘⇧T undoes the close it means, then rebuilds the tab through the
existing restore: each pane a fresh shell in its old cwd, opening on its
last screen, with its shell, SSH target and agent resume facts. The tab
keeps its id. The window's own list is used when the machine has nothing.
- The home screen offers the next tab ⌘⇧T would reopen, read from the
machine mirror, which now tracks this window's remembered closes.
A new setting, `confirm_close` (General > Tabs, "Confirm before closing"),
decides when closing a tab or pane asks first: `never`, `when-busy` (the
default and the old behaviour) or `always`. The SSH "Warn before closing"
opt-in is honoured under every mode. Under `always`, Close Other Tabs and
Close Tabs to the Right ask once for the whole batch.
Refs #1021
Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
* fix(tree): a remembered close marks its panes' records as no longer live
The records stay in the pane list for the reopen, but the panes are stopped
right after the close. Left at live: true, `tty7 wait` on a pane of a
closed tab read it as a running agentless shell and waited forever instead
of reporting it exited.
Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
One workspace is driven by one window at a time, whether that window runs on
the machine itself or on a remote client. The local GUI used to connect to its
daemon without ever claiming a workspace, so a remote client and a local
window could drive the same panes at once and fight over their size.
- The local link now claims (WorkspaceAttach) every local workspace a window
shows. Opening one on purpose (switcher, tty7 open, a new window onto it,
switching in place) takes it over; restore, relaunch and reconnect only
claim a workspace nobody else holds, and otherwise open taken over.
- Preempted events for this computer put the window in the same taken-over
state remote workspaces use: panes detached (processes untouched), the
status strip naming the holder, the input pill, Take Back, and the
switcher's 'taken over' badge.
- A hydration of a local workspace another client holds does not attach its
panes, so a restored window never resizes them under the holder.
- A reconnect re-claims what the window held and leaves what it was pushed
off alone. Nothing but Take Back or an explicit open reclaims.
- The local hello carries the machine's hostname instead of the literal
'this computer', which is what a displaced remote client displays.
A remote workspace refused any machine that was not Linux or macOS. This
teaches the installer and the link to reach a Windows OpenSSH host:
- Detection: `uname -sm` is still asked first, unchanged. When it fails
(cmd.exe / PowerShell) or answers from Git for Windows, MSYS2 or Cygwin,
a PowerShell probe reads PROCESSOR_ARCHITECTURE. The detected platform
must match the SFTP home's shape, so a WSL DefaultShell over Windows
SFTP is refused instead of installing a Linux server at /C:/...
- Assets: tty7-server-windows-{x86_64,aarch64}.exe, verified against
checksums.txt like every other server; built by new server-windows
jobs in release.yml and nightly.yml (ARM64 leg non-blocking), and the
CI vcruntime guard now covers tty7-server.exe.
- Install: %USERPROFILE%\AppData\Local\tty7\bin\tty7-server-cXpY.exe via
SFTP (/C:/... spelling). No mode bits are required or set. A running
image is renamed aside to free its name and swept on the next install.
- Commands: every Windows command is a PowerShell script sent as
-EncodedCommand, which survives cmd.exe, PowerShell and bash as the
DefaultShell. The daemon is launched through Win32_Process.Create so
it outlives the SSH session's job object, with this session's
environment handed over; restarts use a new `tty7-server --stop`.
- Link: a Windows server is always reached by session exec with a plain
`"C:\...\tty7-server-cXpY.exe" --stdio` (no env probe, no
stream-local forward).
- Server: `--stdio` (control and --pane) now bridges on Windows to the
daemon's loopback TCP endpoints instead of refusing.
Tested against a fake Windows host in install/windows_tests.rs; not yet
run against a real Windows machine.
The remote shell probe only understood POSIX answers, so a native SSH pane
on a Windows OpenSSH host always fell back to a plain shell with no prompt
marks, cwd reporting or title.
The probe is now a polyglot every default shell can read: POSIX shells
answer as before; PowerShell answers with its edition and $PSHOME, which
name the exact executable; cmd.exe echoes the line back verbatim and is
recognised, then deliberately left alone. For PowerShell the bootstrap
starts a second, interactive PowerShell of the same executable with the
existing integration via -NoLogo -NoExit -EncodedCommand (comments
stripped, about 6.5K chars total, well inside the Win32 command-line
limit), and a redialled pane's start directory (/C:/..., C:/..., UNC) is
restored with Set-Location -LiteralPath inside the encoded script.
Rows scrolled under the floating bar showed through sharp and cut in
half in the gap beneath it. A scroll-edge fade from the bar down to the
screen's edge softens them into the canvas, as the system's own bars do.
Claude-Session: https://claude.ai/code/session_01HVosmYyVSH3BrsLYx3tcSS
`tauri ios init` fills the asset catalog with Tauri's placeholder icon,
so a regenerated gen/ shipped build 0.1.0.9 with the wrong icon. Copy
src-tauri/icons/ios over it on every run.
Claude-Session: https://claude.ai/code/session_01HVosmYyVSH3BrsLYx3tcSS
* fix(ssh): start a redialled SSH pane in the remote directory it was in
A native SSH pane dialled again (restore after a daemon restart, Reconnect,
a split, ⌘T on an SSH tab, waking a sleeping tab) always landed in the login
directory. The client already sent the pane's remote cwd as
`SpawnNativeSsh.cwd`, but the daemon dropped it on the floor.
The daemon now threads it through `Pane::spawn_native_ssh` and
`SshManager::run_session` into the shell-integration bootstrap, whose first
line becomes `builtin cd -- '<dir>' 2>/dev/null` (fish-quoted for fish). It
is never typed at the prompt, never lands in history, and a directory that
is gone leaves the shell in the login directory without a word. Sessions
without integration take the plain shell request as before, so jump-host
menus never see it. The per-host probe cache still holds only the shell.
Only absolute paths are honoured. No wire or protocol change.
Callers now say what they mean: a saved host or quick connect passes no
start dir instead of the local cwd of whatever tab was in front; ⌘T and a
split on an SSH pane pass that pane's remote cwd; a sleeping SSH tab keeps
its remote cwd in the session layout. The daemon's replay now sends the
remote context before the cwd, so a window that reattaches to an SSH pane
does not wipe the remote directory it was just told.
Refs #1028
Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
* fix(ssh): a local shell standing in for a restored SSH leaf starts locally
pane_to_session now keeps a native SSH leaf's far directory, so the
session_to_pane fallback that brings such a leaf back as a local shell
(the redial failed, or its daemon pane is gone on reattach) would hand
that remote path to a local spawn. Pass no cwd there for an SSH leaf.
Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
* feat(settings): an Appearance option to show the title-bar buttons only under the pointer
26.9.2 painted the new-tab and sidebar tiles only while the pointer was
over the bar they sit in; 26.9.3 took that out so the buttons would stay
discoverable. Both are fair, so it comes back as a choice: Appearance >
"Show title bar buttons on hover" (`auto_hide_titlebar_buttons`), off by
default, which keeps today's always-visible bar for everyone who has not
asked otherwise. A config written before the key existed reads as off.
With it on, the rail's two tiles follow the rail, and the collapsed
rail's pair and the trailing panel toggle follow the tab strip. The
window mark stays put, the tiles keep their layout slot so a reveal
never shifts anything, and the title-bar search box is always drawn.
The trailing toggle stays painted while the detail panel is open, as
before, since the panel's own tab row beside it always is.
The reveal is the hover sheet from 26.9.2 — a transparent last child
over each region, because `group_hover` loses the region the moment the
pointer reaches an occluding tile. Resting tiles go to zero opacity
rather than `invisible()`: gpui skips a hidden element's paint pass,
which is where its click and accessibility actions are registered, so a
screen reader could find a hidden tile by label and then not press it.
Shortcuts are actions and never depended on the tiles.
Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
* fix(titlebar): centre the search box on the window, not the bar after the traffic lights
On macOS `TitleBar` leaves an 80pt lead for the traffic lights before
the tab strip — whether or not the rail stands in front of them — and
the Search Everywhere box was centred on the strip. So it sat 40pt right
of the terminal column's middle: with the rail collapsed, 40pt right of
the window's (#1033). Fullscreen added the bar's own inset on top.
The band the box centres in now reaches back over that lead, so its left
edge is the terminal column's on every platform (12pt elsewhere). Its
right edge is unchanged: the strip's end on macOS, the terminal column's
end beside a docked panel or document off it.
Reaching back puts the collapsed rail's tiles inside the band, so the
box now keeps an equal clearance at both ends — two springs with a
minimum width either side of it. In a narrow column it shrinks instead
of sliding under New Tab or the panel toggle, and stays centred while
it does. The geometry is a pure `search_band`, tested for the macOS
rail-collapsed, rail-open and fullscreen cases and off macOS.
Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
* fix(titlebar): clear a hover flag whose sheet is not on screen; document the key
A title-bar hover flag is written only by its sheet, and only when the
sheet sees the pointer cross its edge. Hide the rail from its own tile, or
turn the switch off mid-hover, and the sheet leaves the tree with its flag
still set: the next time it is built the tiles came back painted with
nobody pointing at them, until the pointer happened to pass through and
out again. Clear the flag of any sheet not built this frame.
Also list auto_hide_titlebar_buttons in the configuration reference.
Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
The phone can now hand a pane a photo or file, read what an agent changed,
answer its prompts with a tap, and keep its link across leaving a pane.
Gateway and protocol:
- `Open::Upload` puts a file (up to 20 MB) in the directory the desktop
keeps pasted images in, under a name a shell takes unquoted, and answers
with its path. Panes on this machine only for now.
- `Open::Diff` answers with the working tree's changes against HEAD and its
untracked files, for the directory a pane is in.
- Both are one-shot streams; an older gateway drops them unanswered, which
the client reports as "update tty7".
App:
- Attach button in the message box: the uploaded path goes into the draft.
- Changes sheet from a pane's menu: a block per file, lines coloured.
- Settings → Key bar: remove, reorder and add keys, from a catalog or
written out (`/compact\r`, `^C`), across pages; reset to the default.
- An agent's numbered choices show as buttons while it waits.
- Pinch to zoom, tap a link to open it, find in the scrollback, a Copy
button for text a program copies (OSC 52), and an optional Face ID lock.
- Swipe from the left edge to go back; the iOS WebView has no page stack.
- One workspace at a time on a machine, picked from a row of chips.
- Agent avatars in their own colours.
- Each screen's watch is its own: a second watch no longer ends the first,
which showed as "Can't reach" and a reconnect after leaving a pane.
- Dialing no longer holds the session lock, so Forget works while a
machine is still connecting; its confirmation is an in-app sheet, since
`window.confirm` shows nothing in the iOS WebView.
- iOS 15 minimum, as App Store Connect will require from April 2027.
- `scripts/testflight.sh` archives, signs for the App Store at export (the
team has no devices for a development profile) and uploads.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(switcher): say Offline in words and dim the avatar; drop the link dot for a reachable workspace
The normal case carries nothing extra. An offline machine's row reads
"Offline" under the tab count, where Open and This window go, and its
avatar is dimmed. A dot stays only for states that want attention:
connecting, reconnecting, failed, or taken over by another client.
* fix(tabs): ⌘T on an SSH tab dials the same host and files the tab under it
A new tab from an SSH pane used to open a local shell in the default
directory, which landed in Ungrouped instead of under the host the user
was on. A native SSH pane now dials again with its spec, as ⌘D already
did; a shell that ssh'd onward from a local prompt gets a local tab that
types the same ssh command at its first prompt. Either way the tab is
seeded into the host's auto group before its pane reports in.
* fix(ssh): a connection at the server's session limit is full, not dead
sshd allows ten sessions per connection by default and every remote pane is
one of them. The eleventh was refused with ConnectFailed, and the connection
was marked dead for it: taken out of the cache while all ten panes on it were
still using it. Every pane after that dialled its own fresh link, paying a full
handshake and server probe, and so did every short-lived route afterwards,
because nothing held the replacement and it went away as soon as that route
closed.
The cache now keeps a pool per destination. A refusal marks that connection
saturated for a while instead of dead; the pool hands out the first live one
with room and dials another beside them only when all are full. A route or a
native SSH pane that lands on a connection that turns out to be full goes back
to the pool for another.
* perf(remote): prove a server that is already running in one round trip
Every new connection to a machine tty7 had been to before spent six
sequential round trips proving what it already had: uname, a home lookup and
a stat over SFTP, a control probe and a process scan. The SFTP session it
opened also stayed open for the life of the connection, one of the handful of
sessions the server allows it.
One `sh -c` script now answers the common case: this dialect's server is
installed, answers the bridge probe, and is the one running (or a build of the
same dialect). Anything else falls through to the full install path unchanged.
* fix(remote): don't replace a pane that could not be reached with a fresh shell
Reattaching a restored pane treated every failure other than silence as "the
pane is gone on its machine" and spawned a fresh shell in its place. That
includes the link never opening at all — the server refusing another session,
a transport error — when the pane is still running over there. The tab lost
its session, and the old shell was left orphaned on the remote daemon; one
workspace had accumulated 53 shells for 17 panes.
Only the daemon's explicit "no such pane" now means gone. For a remote pane,
any other failure leaves it pending, and a pane that stands in for a running
one retries on its own a few times (2s, 4s, 8s, 16s, 16s) before leaving it to
Try Again. Local panes keep their previous behavior.
* perf(remote): ask a machine which panes are live over its control link
The pane liveness probe opened a pane route every ten seconds, which is an SSH
session channel on the remote side — one of the few the server allows per
connection. Once panes had taken them all, each probe dialled a whole new
connection. Ask over the control link that is already up instead: the
machine tree's pane records carry the daemon's own liveness.
* fix(scm): stop two windows from trading one repository watch every frame
Who holds a repository open is a global, reconciled by every window every
frame, but it was keyed by watcher kind alone. Two windows whose file trees or
panels sat in different repositories took the hold from each other each
frame, and every hand-over dropped the watch and opened it again. On a remote
workspace that is four round trips per cycle — about 28 control requests a
second for as long as both windows were open.
Holds are now keyed by window as well, and a closed window gives back what it
held.
* perf(ssh): open channels on one connection concurrently
The russh handle sat behind a tokio mutex held across every request on it,
and each request waits a full network round trip for its reply. So every
pane on a connection opened its channel after the one before it: on a link
with a few hundred milliseconds of latency, a workspace of twenty tabs came
back one tab at a time over about ten seconds.
Every call on the handle takes `&self` and waits on a reply channel of its
own, so the lock bought nothing. Drop it.
Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
Bump the gpui fork to paint an aligned line into a layer that covers its
glyphs. The layer was sized from the unaligned origin, so the Changes panel's
flush-right directory shared a draw order with the opaque action strip painted
over it, and sprites draw after quads within one order.
* fix(palette): an open palette keeps workspace keys, and ⌘P answers from the Settings window
While the palette was open, any app binding it did not handle — ⌘1–9,
⌘D, ⌘W — fell through to the workspace behind it. The Settings window
has no palette listener, so the palette chord did nothing there. A
keystroke interceptor, which gpui runs before matching any binding,
now handles both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(palette): shifted editing letters are not the query field's
The modal rule let every secondary+A/C/V/X/Z chord through as the query
field's own, shift included, so on macOS Cmd+Shift+A (New Agent Tab)
still opened a tab behind the open palette. Only redo keeps its shift.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(ui): refocus the app when focus is lost so global shortcuts answer
With focus on nothing, or on a handle whose element is no longer drawn (a
closed file panel, a dismissed menu), gpui dispatches keys on the window root
alone, one level above Tty7App's listeners. TogglePalette, ToggleSwitcher,
OpenSettings and every other tty7-root action went dead. on_focus_lost now
hands focus back via focus_active.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): refocus settings only in the window that draws it
Settings opens in a window of its own, but focus_active handed focus to
the page's handle whenever settings was up, including in the workspace
window. There that handle is not drawn, so the refocus on focus loss
parked focus on nothing and the workspace shortcuts stayed dead while the
settings window was open. Focus the page only when this window draws it,
and fall through to the workspace's own panes otherwise.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(agents): a Claude resume that finds no conversation starts fresh under its id
A tab opened and never used, or run with transcript saving off, has no
saved conversation, and `claude --resume` then stops at an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(agents): only chain the fresh Claude start where the pane's shell has ||
Windows PowerShell 5.1 and nu reject a line containing `||` outright, so
the fallback took the resume down with it there. The restore line now
chains the fresh start only for shells known to have the operator, judged
by the pane's own shell: its spawn spec, else the configured or login
shell for a local pane. A workspace pane with no explicit shell stays
unknown and gets the plain resume, since its default lives on its host.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* feat(daemon): panes set FORCE_HYPERLINK=1
tty7 renders OSC 8 links, but supports-hyperlinks only trusts a fixed
TERM_PROGRAM list and strips them for tty7, so Claude Code's statusline
PR links came out as plain text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(daemon): note that FORCE_HYPERLINK also reaches redirected output
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* ci: seed rust-cache from main, skip Rust jobs on docs-only PRs
Every PR branch saved its own ~2 GB rust-cache per target, readable only
by that branch, so the repo sat over its 10 GB quota with nothing from
main and every branch built cold. Save from main pushes and manual runs
only, keep the cache when a test step fails, and build with
line-tables-only debug info so the cached target dir is smaller.
Docs-only PRs skip the Rust work: a changes job diffs the merge commit
against its base, and build skips its steps rather than the job, so the
required build & test checks still report instead of pending forever.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ci: count both sides of a rename in the docs-only check
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
The New Workspace form prefilled a random codename and offered no button,
only an "Enter to create" hint. The name box now starts empty and must be
filled; the footer carries Cancel and Create, with Create disabled (and
Enter inert) while the name is blank.
Bumps gpui-component to the tty7 branch commit that gives Button and
Input a role and a name and exposes popup menu items, and names the icon
buttons whose tooltips are elements rather than text (New Tab, the
sidebar and detail-panel toggles, Switch Workspace) and the file search
toggles (Match case, Match whole word, Use regular expression).
Editor tabs are tabs named by file (and unsaved state) with a named close
button; Info rows read as 'label, value'; keyboard shortcut rows as
'action, keys'; SSH hosts as 'name, address' with their expanded state.
File-tree rows open on the press, which assistive tech cannot send, so
they take its press action directly: a screen reader could select a file
but never open it.
Each row is a group named by its title and description, so the control in
it is heard with its setting's name; switches report on or off, the reset
link is a button, and the Modified only switch has a name of its own.
Group headers are buttons named with their count and expanded state; files
are tree items named by path and group; directories by name with their
expanded state.
Settings sections are tabs with their names and selected state, file-tree
rows are tree items named by file with selection and expansion, and the
Settings window carries a title for VoiceOver and the Window menu.
Each is a tab with its full title and selected state, and pressable by
assistive tech. Before, VoiceOver saw the window, its buttons and the
terminal, and nothing between them.
Sent the moment the pane was up, the line was echoed by the tty above
everything the shell printed while starting, then read after it. It now
waits for the first prompt, for at most three seconds.