A tab that reaches a window as TabCreated — from `tty7 tab new` or another
window — started its entry watch as a restored tab, so opening one inside a
pinned folder left it in the auto group below. It is as new as a tab opened
here, and now joins the folder like one; every window that hears of it
reaches the same answer.
Rewrite the sidebar page's grouping section around "grouped by repo
automatically; pin what you want to keep": the divider, folder and label
groups, the edge-triggered join, every way to pin, and the header menus. The
configuration reference swaps `sidebar_grouping` for `sidebar_auto_grouping`,
the CLI reference describes the GROUP column as the pinned group, and the
changelog gains an Unreleased entry (#955).
A window draws its first frames before its copy of the workspace's groups
lands, so every tab's entry watch recorded "in no folder" — and the groups
landing then read as each tab walking into its folder. A restored tab, or one
dragged out of its folder group, was pulled back in on every launch.
Groups adopted from a pull or from another window's `GroupsChanged` now start
every tab's watch over from where it is; only this window's own pin gathers
the tabs inside the folder, and says so tab by tab. A tab also goes up with
the group it names even when the window does not know that group yet, so a
sync in that same gap cannot send every kept tab back to auto grouping.
Every way into a pinned group the design calls for:
- Drop a folder from Finder or Explorer onto the sidebar to pin it (a local
workspace only — a dropped path is this machine's, and a folder group keeps
a directory on the workspace's host). Files are let fall.
- "Pin as Group" on a folder in the file tree, on local and remote workspaces
alike, since the tree and the group are both on the workspace's host.
- Palette "New Group" makes an empty label group and opens its name for
typing; "Open Folder as Group…" picks a folder with the system picker, pins
it and opens a tab in it. The picker browses this computer, so that one is
not offered on a remote workspace.
- Tab right-click "Move to Group" lists the pinned groups plus "New Group…",
which files the tab in a fresh label group with its name open for typing.
Pinning a folder already pinned hands back the group that keeps it rather
than making a second one to split its tabs with.
The sidebar now reads as two halves: the groups you keep, in the order you
put them, then a divider, then the groups it works out (Arc-style).
- Pinned headers drag-reorder among themselves (their own reorder surface, so
a pinned header cannot be dropped among the derived ones); the order lands
on the workspace's group list, not on the tabs.
- An auto header carried above the divider is pinned when let go. With
nothing pinned yet the divider appears during that drag as a "Drop here to
pin" zone, since a hairline at the top of the list is nothing to aim at.
- A tab kept in a pinned group and dropped anywhere below the divider goes
back to auto grouping; the divider lights to say so.
- An empty pinned group stays, with a "+ New Tab" row that opens a tab in its
folder (or where ⌘T would, for a label group) and files it there.
- Folder groups carry a pin mark that unpins on click and a tooltip with the
folder; auto headers show pin and "+" on hover.
- Header menus: pinned — Rename, Set Folder… (local workspaces), Use Current
Tab's Folder, Clear Folder, New Tab, Unpin (folder groups), Delete. Auto —
Pin Group, New Tab. Nothing renames an auto group; nothing pins implicitly.
Replace the sidebar's hand-made groups with the model from #955: the sidebar
groups tabs by repo automatically, and you pin what you want to keep.
- Pinned groups (`PinnedGroup`: id, optional name, optional folder, fold) are
stored on the workspace in the machine tree, in display order, and a tab
points at one by `GroupId`. Everything else is an auto group worked out
every frame and never stored: by repo home, or by `user@host` for an SSH
pane — native or a shell that ssh'd onward — so `/home/ubuntu` on two
machines no longer lands under one header.
- A tab whose cwd *enters* a pinned folder joins it (deepest folder wins; a
repo home equal to the folder counts, which keeps worktrees with their
repo). It is edge-triggered through `EntryWatch`, so a tab dragged out
while still inside the folder stays out until it leaves and comes back, and
a tab restored at launch is not pulled in by where it already sits.
- Groups sync as one `WorkspaceSetGroups` / `GroupsChanged`, pushed only from
an edit and adopted from every pull, so a fresh window can never push an
empty set over the workspace's. The machine hands tabs naming a dropped
group back to auto grouping in the same mutation. Control dialect → v11.
- Config: `sidebar_grouping` (three modes) and `sidebar_collapsed_groups` give
way to one `sidebar_auto_grouping` toggle; folds live with the workspace.
- `tty7 tab ls` reports the pinned group a tab is in (name or folder leaf;
JSON carries id, name and folder).
The README now opens on an animated WebP of the one-minute tour (GitHub
won't play an mp4 inline), linking the full-quality mp4. The docs home
page autoplays the mp4 in the hero slot. The old hero screenshot and the
separate tour poster block are gone.
Every placeholder frame in docs/ now shows a real capture of the current
build: 20 screenshots plus two short looping clips (prompt editor, pane
drag). The README and the docs home page link a one-minute tour covering
agent status across repos, one agent driving another through the CLI,
the prompt editor, diffs, pane dragging, and sessions surviving a quit.
A working agent's dot now alternates every 600ms between its colour and
a paler, still opaque, blend of it over the dot's ring, so a column of
tabs shows at a glance which agents are still going.
It rides the existing half-second home-cursor tick rather than an
animation: two repaints a second while some tab has a working agent,
and none once no agent is working (the dot settles back on its full
colour with one last frame).
On Windows and Linux the tabs sit under a full-width title bar, and a
second 40px band left them well down from the window's top. The row is
now the tab's hover pill plus 2px either side.
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
The reader parsed each batch — and a relink's whole replay, up to 8 MB —
in one hold of the grid lock, and every UI-thread handler that touches
the grid queues on it. Feed the emulator in MAX_LOCKED_FEED slices, the
same bound Alacritty's reader uses.
Claude-Session: https://claude.ai/code/session_01YX786Hyr4ivijWxv66zVkf
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.
A portable ZIP install whose folder has both the .tty7-portable marker
and a data\ folder next to tty7-app.exe now uses <exe dir>\data as its
config directory, so settings, sessions, scrollback and the daemon's
socket travel with the program. --config-dir and TTY7_CONFIG_DIR still
win. The folder is an explicit opt-in because the marker has shipped in
every portable ZIP since the in-app updater arrived: keying on it alone
would move existing users onto an empty directory at their next update.
The portable directory is not the machine's config dir, so a portable
copy never adopts the installed tty7's legacy tree. An updater test pins
that data\ is not a managed root and survives an in-place update.
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.