Settings used to cover the workspace it was opened from, hiding the
terminal a setting was being tried on. It now opens a separate window.
- The state and page stay on the workspace's Tty7App; a thin
SettingsWindow view borrows it to draw, observes it to repaint, and
sets its own rem size. The state is built against the new window, so
its inputs and subscriptions belong to it.
- Opening again raises the window instead of closing it. Esc, Cmd-W and
the close button go through close_settings_checked, so unsaved forms
and theme drafts still prompt; closing hands focus back to the
workspace window. The window goes when its owner is released.
- open_settings_then / close_settings_then run follow-ups in the right
window: loading an SSH form happens in the settings window, and
Connect opens its tab in the workspace window.
- Window opacity, blur and backdrop are pushed to every window, since
they are now changed from a window that is not the workspace.
- Tests keep the in-window overlay so they can drive settings through
their single test window.
Folded, the history section is the last line in the window and sat flush
against the bottom edge, inside the window's rounded corner. Pad it 2px
above and 4px below; the expanded section is unchanged, so its
commits-per-height pin still holds.
The v5 restyle turned the tab rows, the search field and the workspace
switcher into full pills, which read too round at this density. Put
back CARD_RADIUS on the rows, rounded_lg on the search field and
rounded_md on the switcher (and its rename field), keeping the 28px
row height. PILL_RADIUS has no users left and goes with them.
The workspace switcher painted its picked row a hard-coded #1768CF with
white text, in the workspace list, the tab list and the New Workspace
host dropdown. It bypassed the theme and was the loudest block on
screen. Use the popover's neutral selected step and keep each row's own
inks, so a taken-over warning still reads as one.
- Rails share the window fill and are separated by a hairline only;
the current tab is a neutral selected step plus a semibold title
instead of the blue navigation wash (the unused role is removed).
- Default Light theme takes ink #0F1419 and accent #1F6BF0; the caret
follows the accent.
- Sidebar: 28px pill rows with 18px avatars (two-line rows keep card
corners), pill search field and workspace switcher, group headings
in body ink with the chevron shown only when folded.
- Right panel: word tabs (Info / Changes / Files) with an underline
bar on a closing hairline. The Changes count is dropped before any
label is cut, and the panel floor grows with the measured labels so
the chrome tiles are never pushed off the edge.
- Headings are no longer uppercased; popovers use a 10px radius.
- docs/design-system.md updated to match.
On macOS the panel toggle and app menu tiles sat at the end of the terminal
column's strip whenever the detail panel was closed, including when a docked
diff or file column stood to its right, leaving them in the middle of the
window beside the document's header. Hide them while a document is docked and
stop reserving their width for the tab chips.
Three things went wrong with a Files panel rooted on a distro's \\wsl$
share:
- A drop brought a `name:Zone.Identifier` file along with every
downloaded file. fs::copy is CopyFileEx on Windows, which copies the
NTFS alternate data streams, and the share has no streams to keep one
in, so it lands as a file of its own. A copy onto a WSL share now
moves the contents only; one onto NTFS still goes through fs::copy
and keeps the mark.
- A file removed from the shell in the distro stayed in the tree. The
share accepts a ReadDirectoryChangesW and never reports a change, so
the watch looked healthy and was deaf. Directories on a WSL share are
now watched by a notify PollWatcher every 2s instead, made the first
time one is needed.
- Right-clicking a row did not select it, so nothing marked the row the
menu was about. A right click now selects the row without opening it.
Dropping a RemoteWatch sent WatchClose with a blocking call, and the last
handle is usually let go on the UI thread: from Tty7App::render via
scm_sync_watchers, and from scm_watch_opened when an open lands after its
subscription moved on. Every window froze for a round trip each time, up
to WatchClose's 5 s deadline on a quiet link. Sampling a live instance
caught ~1.1 s of such stalls in 10 s.
- ControlClient::post sends a request without registering for its reply;
the reader already drops replies nobody is waiting for.
- RemoteWatch::drop posts WatchClose instead of calling it.
- pane_workspace_for built the full SSH spec, keychain lookups included,
only to strip the secrets again. It now builds it from NoCredentials,
which takes the securityd trips off pane_liveness::sweep.
Claude-Session: https://claude.ai/code/session_01YX786Hyr4ivijWxv66zVkf
SelectWorkspace1-9 and the Window menu numbered workspaces by most
recent use, so switching to one moved it to slot 1 and reshuffled the
rest. Number them by the order this client first had them instead
(the append-only views list), skipping synced remote references until
they are opened; opening one moves it to the end so it takes the next
free number. The switcher keeps its MRU list and shows each row's
number.
Add a macOS-only `font_thicken` key (default true) and a Settings row
under Appearance > Terminal text. When off, AppleFontSmoothing is pinned
to 0 in this process's NSArgumentDomain before gpui's text system first
reads it, so glyphs render at the face's own weight. The volatile domain
is in-memory only: nothing is persisted and no other app is affected.
gpui caches the preference in a OnceLock, so a change applies after a
restart; no gpui fork change is needed.
A remote workspace's Files tree took uploads by drag-and-drop but had no
way to bring a file back. Right-click a file -> Download now reads it
through Host::read_file and saves it into this machine's Downloads
folder, named and numbered the way the SFTP panel's Download does.
Capped at the same ~63 MB as uploads (one control frame); an oversized
file is refused up front with the limit and nothing is transferred.
Local trees keep Reveal in that slot; folders are not offered.
Closes#723
Cursor's ~/.cursor/hooks.json is a flat hook map — `command` directly on
each entry under `hooks.<event>` — so it reuses the flat writer Crush
introduced. Two things are Cursor-specific:
- The file needs `"version": 1` at its root or Cursor ignores it. tty7
now writes it when it creates the file or finds it missing, never
overwrites a version the user set, and reports a versionless file that
holds our hooks as Outdated so refresh repairs it.
- Payloads name the session `conversation_id` (only sessionStart repeats
it as `session_id`), and most events carry `workspace_roots` instead of
`cwd`. Both are now read as aliases, which is what Copy Session ID and
resume were missing.
Events: sessionStart, beforeSubmitPrompt, postToolUse, stop, sessionEnd.
cursor-agent does not fire beforeSubmitPrompt today (a known gap on
Cursor's side), so postToolUse also reports prompt-submit: the first tool
call opens the turn and Cursor's stop, which the CLI does fire, closes it.
A turn with no tool calls never shows as working. None of Cursor's
permission hooks are installed, since those would block on the empty
stdout tty7's hook prints.
Add SelectNextTab / SelectPrevTab, which move to the neighbouring tab in
the order the strip or sidebar shows them, wrapping, with no popup.
Defaults: Cmd+Shift+] / Cmd+Shift+[ on macOS, Ctrl+PgDn / Ctrl+PgUp
elsewhere. The tmux preset's prefix n / p now bind these (tmux
next-window semantics) instead of the MRU switcher, which never
auto-committed there.
NextTab / PrevTab keep their config names and Ctrl+Tab, but are labelled
Recent Tab Switcher on the Keybindings page; the palette's Next/Previous
Tab entries now show the SelectNextTab chord they actually run.
The code panel's Preview/Edit and Wrap buttons only toggled state in their
click handlers, so there was no way to reach them from the keyboard. Register
both as actions, unbound by default like ToggleDocumentFill and the
DocumentWidth* actions, list them in the command palette and the Keybindings
page, and route the buttons through the same methods.
Instead of config keys for the initial state, the last-used state is
remembered (editor_soft_wrap / editor_markdown_preview), the way diff_view
and scm_graph_expanded already are. A file opened at a line target always
opens as source so the cursor is visible.
CodeBuddy Code takes Claude Code's hooks as-is: the same nested
`hooks.<Event>[].hooks[{type, command}]` shape in ~/.codebuddy/settings.json
(or $CODEBUDDY_CONFIG_DIR/settings.json), the same event names, and the same
session_id/cwd payload fields. So it rides the shared hook-map installer and
needs no payload aliases.
The event table follows Qoder rather than Claude: CodeBuddy has a
first-class PermissionRequest and Elicitation, so it gets no Notification
hook, and StopFailure ends a turn like Stop. CodeBuddy also emits
SessionStart with source "compact" mid-turn, which is filtered out the same
way Qoder's is so the pane does not drop back to idle.
Detection covers all three npm bins (codebuddy, codebuddy-code, cbc).
Resume is `codebuddy --resume <id>` and fork appends --fork-session; stale
session and worktree flags are dropped from the replayed launch argv, and
--no-session-persistence disables both commands.
Icon, en/ja/zh names and search keywords, and docs are updated.
Reopening the SFTP panel went back to the last browsed folder; a fresh
open now prefers the pane shell's cwd, falling back to the last browsed
folder, then the login directory. Following a pane switch with the panel
still open keeps each pane's last browsed folder.
Also pin the test config dir in the sftp gpui harness so filtered runs
do not trip the real-config guard, and fix docs/remote/sftp.mdx, which
said the panel opens on the remote home directory.
Closes#826
Local: restarts the local daemon onto the app's build through the existing
restart flow and confirmation, or says it is already current when the probed
daemon reports this build and no mismatch is pending.
Remote: a new RouteAction::UpdateServer makes the local daemon force-install
this build's server over one already speaking our dialect
(Installer::replace_forced: upload to a temp name, probe, rename, then
cycle_daemon), for SSH and WSL. Offered only when the window's workspace is
on a host whose server we install; gated on the local daemon advertising
FEATURE_UPDATE_SERVER, since an older one cannot decode the action.
Claude-Session: https://claude.ai/code/session_01YX786Hyr4ivijWxv66zVkf
On Linux the Keybindings page wrapped action names one to three CJK
glyphs per line and spilled them over the rows below, even though the
keycaps beside them left plenty of room. The label was a shrinkable
block sized to its own measured text, so any layout pass that measured
it narrower than it finally ended up left its painted lines wrapped at
that narrower width.
An action name is a single line: make it nowrap, and beside the keycaps
let it take the space they leave (flex_1) instead of sizing to its text,
so neither its box nor its lines depend on that measurement. Stacked
rows keep the whole row for the name as before.
The Agents half of the report (a long Codex install-failure note
squeezing the agent name to a glyph per line) is #897, already fixed on
main by #898 and not yet in a stable release.
A native SSH pane is owned by this machine's daemon, so its paths are
kept away from every Host call and it never gets a git_status_cwd. The
sidebar only grouped from git_status_cwd, which left every native SSH
tab in Scratch under repo-or-directory grouping, even though the remote
shell reports its cwd over OSC 7.
Fall back to that reported cwd for native SSH panes (absolute POSIX
paths only) and resolve it as a settled 'no repo': repo-or-directory
files the tab under the folder, repo grouping keeps it in Scratch.
Typed 'ssh' and WSL panes are unchanged.
Fixes#891
A pane running wsl.exe reports its cwd as a POSIX path from OSC 7, but its
host is this machine. The Files panel rooted the tree at that path verbatim
and handed it to the local read_dir, so on Windows /home/me was read as
C:\home\me and the panel showed "Could not be read"; drops into the tree
failed the same way since they target the rooted directory.
Root the tree at TerminalView::files_cwd instead: a cwd the pane's host
resolves is used as-is, a WSL pane's POSIX cwd goes through the distro's
\\wsl$ share (the same mapping Tab completion already uses), and a cwd no
host here can read (a shell ssh'd onward) roots nothing instead of an
unreadable directory.
#827 stopped a declined prompt from being asked again, but left the
prompts themselves with no notion of whether anyone was still waiting on
them. A routed auth prompt sat in the mailbox, in the parked queue behind
the sheet on screen, or on screen itself, until somebody answered it —
even after the connection attempt that raised it had timed out and moved
on. Answering it sent the secret into a dropped channel.
That is the report's sequence. A link drops while nobody is at the
keyboard; each reconnect attempt raises a password prompt and times out
unanswered, and before #827 the supervisor dialled again and again, so one
dead prompt per attempt piled up behind the first sheet. The user comes
back, types the password into a sheet nobody is listening to, the next one
comes up, one of them happens to be the live attempt and connects — and
the dead ones keep coming up however they are closed. #827 ends the
attempt loop on a timed-out password, but a key passphrase declined by
timeout still falls through to other methods and a transient failure, and
a single stale sheet is still left on screen either way.
A PendingAuth now carries a weak handle whose only strong count lives in
the responder for as long as it waits, so it can say when it has been
abandoned. The pump drops abandoned prompts instead of raising or parking
them, and takes down an on-screen routed sheet whose asker has gone,
moving on to whatever else is asking. The GUI also waits no longer than
the daemon's handshake does (the broker's 120s rather than 180s), so the
sheet comes down when the attempt behind it actually fails, not a minute
later.
Un-zoom routed focus through the tab's `last_focused`, which only
focus-in writes, so any gap between that record and the pane actually
zoomed put the cursor in a different pane when the split came back.
Since #843 the record is kept current on focus-in, which is why the
reported sequence no longer reproduces on main, but the zoomed pane is
the answer outright: hand it to the tab before focusing, so un-zoom no
longer depends on the record having caught up.
Zooming with focus off the panes (a palette just closed, the tab strip)
also zoomed the tab's first leaf rather than the pane the tab
remembers; it now falls back to `focus_target`, the same pane a switch
back to the tab would focus.
The last two passes at the avatar — a flat theme disc, then a bare mark
painted in the brand colour — each gave something up the solid disc had:
the flat disc lost the hue that tells one agent from another down a
column of rows, and the bare mark read lighter and less settled than the
disc beside the status dot. Neither was better than where it started.
Back to a solid fill of the agent's brand with the mark in its own ink,
and the hairline `needs_edge` adds for Codex and Grok's pure black on a
dark window. The shell avatar goes back to its muted disc. `mark_ink`
goes with the style it served; the toolbar changes from the same PR stay.
The sidebar avatar dropped its brand-coloured disc for a flat theme one,
which left a column of identical grey marks: hue had been the fast way to
tell one agent from another down twenty rows, and the disc was the only
thing carrying it. Painting the mark itself restores the reading at a
third of the coloured area, so it no longer competes with the status dot
beside it — colour says who, the dot says what it wants. `mark_ink`
keeps the hue and only lifts a value that cannot be seen on the surface
under it, which is Codex and Grok's pure black on a dark theme.
The toolbar above it had the opposite problem: `+` and the panel toggle
were drawn at `sidebar_foreground`, the rung a tab title uses, so the two
controls were the darkest marks in the sidebar. They drop to
`muted_foreground`, level with the workspace chip, and the hover fill the
variant already carried answers the pointer.
The glyphs themselves: the panel icons fill their compartment so they
read as a sidebar rather than a split box, and the plus carries a sixth
more stroke than the closed shapes beside it — an open form at the family
weight reads both larger and fainter, which is what made it the odd one
out. Its cap now lands on a half pixel rather than a whole one; the
whole-pixel rungs are 10px and 12px in a 16px box, and neither is the
size.
Claude-Session: https://claude.ai/code/session_01FG2s9mbZu6LbjjmU54X7kt