The interactive cursor-agent TUI fires beforeSubmitPrompt and stop; only
print mode (-p) lacks them. Mapping postToolUse to prompt-submit froze the
activity counter (opportunistic git refresh, agents --changed), shortened
turn timing and hid tool-less turns. Map it to tool-complete instead.
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.
With a long prompt the inline editor started at the prompt's end column,
leaving e.g. 24 of 143 columns for the first row of input. When fewer than
max(20, cols/3) columns remain (and the prompt is at least as wide as what
it left, so a new row actually gains room), the input overlay now starts at
column 0 of the row below the prompt.
The start position is decided by one pure fn, input_start(), and fed to
everything that lays out the input: the overlay rows and overflow shift,
click-to-index mapping, vertical caret movement, the rendered bar, the
completion menu anchor and the IME candidate anchor.
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
No message changes. A same-dialect install trusts whatever binary already
sits at tty7-server-c9p6 and Update Server only restarts it, so daemon-side
fixes (#828 modes restored on re-attach, #857 replay ring / clipboard
sniffer) never reached remote hosts. A new filename makes clients upload
the matching server.
Claude-Session: https://claude.ai/code/session_01YX786Hyr4ivijWxv66zVkf
Narrowing a pane under a shell prompt whose right side sits near the edge
(an RPROMPT clock, a right-aligned segment) left fragments of the old prompt
behind, and a drag that narrowed and widened the window a few columns at a
time filled the pane with them. No size disagreement is needed: the reflow
wraps the prompt line onto a second row and keeps the cursor on the wrapped
tail, and the shell's SIGWINCH redraw - which moves up by the rows it last
drew, returns to column 0 and clears below - starts from there. The head of
the old prompt stays behind, still wrapping into the new one, and widening
joins every such head into one long line.
A column change at a prompt (shell integration active and at_prompt, primary
screen, not a ConPTY, not a replayed Size) now clears the cursor's line from
its first row down before the reflow and puts the cursor back as many rows
below that line's start as it was, which is the count the shell moves up by.
Applied where the grid reflows: the daemon's live Size echo and the
request-time path for routes without one.
Tests replay zsh 5.9's captured redraw sequence against the grid: a stepped
drag (checked to strand fragments without the guard), a wrapped command
line, back-to-back reflows before the redraw, a line at the bottom of the
screen, and the cases left alone (away from a prompt, rows only, alternate
screen); plus one end to end over the reader's Size echo.
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.
Two places could turn the tail of an escape sequence into visible text.
The replay ring evicts from the front at an arbitrary byte once it reaches
its cap, and a replay starts the client's emulator in its ground state. A
cut through Pi's `ESC ] 133;C BEL` line mark was painted as `33;C` on every
re-attach; a cut through a CJK character as a replacement glyph. The ring
now folds evicted bytes through a minimal VT state machine and keeps
evicting until the front is outside every sequence and not on a UTF-8
continuation byte.
The OSC 5522 tokenizer dropped a held `ESC ]` (plus any undecided
identifier bytes) and the `ESC` that interrupted it, so `ESC ]` split from
`ESC [31m` by a read boundary reached the client as literal `[31m`. It now
forwards those bytes and lets the new sequence be judged normally; only an
interrupted clipboard write's own payload is still dropped.
The daemon's out-of-band stripping moves into `lift_out_of_band` so tests
can drive it: a Pi-style streamed frame passes through byte for byte at
every split point, and the client's emulator parses it to the same grid at
every split point.
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.
On a resize-echoing route (the local daemon and current remote servers)
the grid only reflows when the daemon's Size echo comes back. The
per-frame early-out trusted the request instead: once a size had been
asked for it was never sent again, so a Resize or echo that went missing
left the pane painting a grid shorter than its bounds, with the child
still drawing for the old size, until a divider drag asked for a
different size.
The early-out now checks that the grid actually holds the requested
geometry. While an echo can still be in flight (RESIZE_ECHO_GRACE) it
keeps waiting as before; past that, a grid that disagrees gets the size
sent again. The check try-locks the grid like the painter does, so the
UI thread never queues behind the reader. A size comparison that honours
alacritty's minimum grid also stops a one-column pane from resending on
the non-echo path.
Tests: a lost echo is retried after the grace and not before, a late
Size frame is corrected on echoing routes too, and a relink delivers a
resize made while the old link was down.
`\e[0 q` / `\e[ q` already fall back to `Config::cursor_style`: alacritty
resolves an unset style to `default_cursor_style`, which we seed from the
config and re-apply on every live config change. What nvim actually sends
on exit is terminfo `Se`, and for `xterm-256color` that is `\e[2 q` -- an
explicit steady block, since that is xterm's default -- so the emulator
honours it and every prompt after nvim/vim is stuck on Block.
Track the shell-integration command marks in the pty reader, cut at their
exact byte offsets: note the cursor style at OSC 133;C and, if the command
left a different one behind by 133;D, restore it (back to "unset" when the
prompt was on the configured default, so it keeps following live config
changes). D is the first thing our precmd writes, so shell hooks that style
their own cursor (vi-mode plugins, precmd echoes) still have the last word.
Replayed snapshots go through the same cuts.
Tests: reset via `\e[0 q` and `\e[ q` lands on a configured bar/underline;
C, `\e[2 q`, D restores bar/underline, in one frame and split across frames;
unit tests for the mark parser and restore rules.
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.
responsibility_get_pid_responsible_for_pid reports a process as its own
responsible process both when it truly is and when the one it inherited
has exited. A daemon that outlived its GUI - the state a handoff is
meant to repair - therefore looked already disclaimed and was skipped.
Verified headless: a daemon from origin/main launched by a process that
then exits gives new panes no attribution (the shell reports itself);
an in-place restart to this build keeps the pid and existing shells, and
new panes then report the daemon as responsible; a fresh start does too.
macOS attributes Local Network (and other TCC) decisions to a process's
responsible process, fixed at spawn and inherited from the parent. The
daemon is spawned by the GUI, so it and every shell it forks answered to
that GUI. Once the GUI exits while the daemon lives on - every in-place
update, a crash, a force-quit - new panes are no longer attributed to
tty7.app and non-platform binaries get EHOSTUNREACH on the LAN until a
full quit-and-relaunch.
At the top of run_daemon, re-exec in place via posix_spawn with
POSIX_SPAWN_SETEXEC and responsibility_spawnattrs_setdisclaim, so the
daemon (tty7.app's own executable) is responsible for itself. Running
there also covers the far side of a handoff, which repairs a daemon
started by an older build without losing its panes. The SPI is looked
up with dlsym; if it is missing or anything fails, startup continues as
before. A marker argument prevents a second attempt if the first did not
take.
write_atomic renamed its temp file over the given path. When that path is
a symlink (config.json linked into a dotfiles repo), rename(2) replaces
the link itself, so the first save — dragging the sidebar, running a
palette command — silently turned it into a plain file and broke sync.
Resolve the symlink chain first and write next to, and rename over, the
file it finally points at. Covers every write_atomic caller (config.json,
views.json, window.json, machines, presets, agent hooks).