The link modifier was `mods.platform`, which gpui maps to ⌘ on macOS but
to the Win/Super key elsewhere — a key the OS mostly swallows, so on
Windows and Linux neither the hover underline nor click-to-open could be
triggered at all. `config.json`'s own docs already promised "⌘/Ctrl-click";
this is the implementation catching up.
Use `Modifiers::secondary()` (⌘ on macOS, Ctrl elsewhere) at all three
sites: the click handler, the hover probe, and the app-level modifier
tracking that pushes the state down to background panes. Not
`platform || control` — that would steal ⌃-click on macOS, where it means
"right click".
Ctrl+click still falls through to mouse-tracking TUIs when there's no
link under the cursor, since `open_link_at` reports whether it consumed
the click.
Fixes#183
All 17 tty7-drawn glyphs are redrawn to a single spec: stroke 2.1, round
caps and joins, radius 3.4-4.4, a near-square 3.4->20.6 box, and filled
nodes at r >= 1.6. Metaphors stay conventional throughout - a magnifier
is a magnifier, a folder is a folder - so nothing needs relearning; what
changed is how they are drawn.
The previous set mixed weights (1.9 with a 2.4 exception for plus), left
folder and folder-closed resolving to one identical drawing, and mixed
terminal treatments across the row. This one holds every glyph to the
same five rules: optical rather than geometric sizing, handles meeting
circles on the tangent, even interior spacing, one terminal treatment,
and equal apparent area rather than equal bounds.
Two notes for future edits, both in the module docs: nothing here relies
on partial alpha surviving rasterisation (the dock block is a flat fill),
and folder-closed carries an inner rule so it stops sharing art with
folder.
The eye tile spent the panel's scarcest row — the header the tab tiles now
share — on an option that gets set once and then forgotten. It moves into
the menu the tree rows already have, as a row under "Reveal in Finder"
whose label says what the click will do ("Show Dotfiles" / "Hide Dotfiles")
rather than checking off the current state, since one checked item makes
`PopupMenu` reserve an icon gutter on every row in the menu.
The SFTP browser's header keeps its own controls: Refresh and the "⋯" menu
are actions on a live connection, not a view option.
The rail's toggle has never lit up when the rail is out, and for the same
reason the detail panel's shouldn't: the panel being open is already on
screen — it *is* the panel — so a selected capsule only restates it, and
with one toggle lit and the other not, a chrome tile's fill stopped meaning
anything in particular. The state moves to the tooltip's verb (Show / Hide
Detail Panel), which is what the rail's toggle already does.
Off macOS the detail panel was a full-height column beside the title bar,
so the bar — which lays out the ─ ▢ ✕ group at *its* own right end — ended
before the panel did, stranding the window controls mid-window with the
panel's grey to their right. On macOS the same layout is right: the traffic
lights are on the left, over the rail, and nothing collides.
So off macOS the bar now spans the panel too, reaching the real top-right
corner, and the panel hangs below it:
- The caption row over the panel is painted in the panel's own surface, so
the column still reads as one continuous sidebar from the top of the
window rather than starting 40px down in a different colour.
- The corner chrome (detail-panel toggle, workspace chip) stays in the
strip, but over the panel it aligns with the column it sits on — pinned
to the panel's leading inset instead of hugging the controls.
- The panel's tab tiles no longer earn a row of their own. Under a caption
row that already carries chrome they made three stacked headers before a
single line of content, so they move into the section header the panel
was drawing anyway: name and count at one end, the four tiles at the
other, a hairline under it. A tab's own control (Files' dotfile toggle)
still sits on that line, ahead of the tiles.
macOS keeps the full-height panel column with its own title-bar-height top
zone, drag region and corner chrome, unchanged.
Verified on Windows 11 in both tab-bar modes, panel open and closed, and
with the code overlay open (which now stops under the bar, so the controls
stay clickable and its header lines up with the panel's).
On Linux, `"theme_follow_system": true` made the app panic on launch with
`RefCell already borrowed` from gpui's Wayland/X11 client, on both backends.
Once a user turned "sync with system" on, the setting persisted and every
subsequent launch died — a hard brick until `config.json` was hand-edited back.
gpui's Linux backends dispatch a window's appearance-changed callback while the
platform client's `RefCell` is already mutably borrowed: both XDP handlers hold
`client.borrow_mut()` across `set_appearance`, which invokes the callback
synchronously. `cx.window_appearance()` re-borrows that same cell.
Our appearance observer called straight into `apply_theme` →
`effective_preset_id` → `system_dark` → `cx.window_appearance()`, so it
re-entered that borrow and panicked. Only while following the system: with the
setting off the observer returns before reading the appearance, which is why
the default install was fine. It reproduced on *every* launch because the XDG
portal source emits one appearance event during startup.
Cache the OS appearance in a `SystemAppearance` global instead. The observer
fills it from `Window::appearance()` — the window's own cell, released before
the callback runs — and `system_dark` reads the cache, so no platform read
happens on the re-entrant path. macOS still re-seeds the cache from the
platform right after `apply_theme` releases the native appearance pin, since
the window reports the pin rather than the OS setting until then.
Zed keeps a `SystemAppearance` global for the same reason, and gpui-component
switched to `window.appearance()` over the same Linux panic
(longbridge/gpui-component#104).
Fixes#179
The stateful palette titles (Hide Left Sidebar / Show Right Panel) read
`cfg.sidebar_collapsed` and `cfg.right_panel_visible`, but both of those are
per-window state living on `Tty7App` -- the config copies only record whichever
window toggled them last. With two windows in different states the palette in
one of them named the other's rail: the row read "Show Left Sidebar" while the
rail was out, and clicking it hid it.
Pass the calling window's own state in as a `ChromeState` instead. Deliberately
not `left_panel_open()`: that also folds in `!tabs.is_empty()`, and on the home
page the command still flips the collapse flag, so the title has to describe
the flag rather than what is currently on screen.
The tab bar's side stays on `Config` -- that one really is app-wide.
Chrome tiles painted every glyph at full `foreground` (#111 on white) —
the darkest ink in the window sitting on its lightest surface, a full step
darker than the tab rail's inactive rows and two steps darker than every
other secondary icon. The title bar read as a hard black row against the
soft greys around it.
Idle tiles now take `sidebar_foreground`; a selected tile keeps full
`foreground`, so "on" is carried by darker ink as well as the filled
capsule instead of the capsule alone.
The menu bar shipped four menus in the order App / Shell / Window / View
with no Edit menu at all, so Copy and Paste existed nowhere but a
right-click, and About / Check for Updates / Hide / Minimize / Help had
no home. It now follows the macOS HIG's standard set.
The palette listed 47 commands in three competing naming styles, ranked
only by catalog order, with no grouping and no way to reach most of what
the tab context menu could do. It now has one documented grammar, a
scored fuzzy ranker, group bands with a frecency-driven Recent, and the
commands it was missing.
Settings had a three-row Shell page indistinguishable from Terminal, a
seven-group Terminal page that had become a junk drawer, two different
groups called "Window", and a search index that had drifted so far from
the rendered rows that "opacity" and "completion" returned nothing.
Also folds copy / cut / paste / undo onto one code path each, which fixed
two real drift bugs: the right-click Paste skipped the image-paste branch
that Cmd+V had, and Copy rendered disabled whenever the selection was in
the prompt editor rather than the grid.
Grok was drawing the generic robot glyph on a slate disc — the fallback
picked back when no usable mark was bundled.
xAI publishes its symbol only as a ~2:1 landscape lockup that bleeds off
its own canvas; traced and fitted to a 24x24 box it is unreadable as a
16px silhouette, which is what the tab chip and sidebar render. So the
bundled mark is lobehub/lobe-icons' square transcription (MIT), drawn for
exactly this avatar use. Its notice rides in the SVG.
The slate accent goes with it. That mid-tone exists for vendors whose
monochrome mark is grey or a gradient (Cursor), because a white field
vanishes on a light theme; a black field has no such problem — it stays
darker than even the darkest theme background and the white mark carries
the badge. Grok brands in black, like Codex, so it keeps that.
Adds a guard test: every CLIAgent::icon_path must resolve through the
asset source. A brand mark means touching two files, and forgetting the
registration costs the agent its avatar silently.
CHANGELOG: keep both Unreleased sets, with the history-search entry under
Added beside the multi-window ones and the Ctrl+J/M fix in its own Fixed
section.
Review follow-ups on top of the multi-window work.
- A brand-new workspace came up on the home page with no shell, because
`claim` always hands back an (empty) session and the window treated that
as "restore this". A first run and `New Workspace` now take the
first-run path again and spawn a terminal; the launch that exists to
show the workspace picker asks for an empty window explicitly
(`FreshStart`).
- The close-window prompt promised sessions "will be restored the next
time you open tty7", which is no longer what happens — the workspace
detaches and waits in the picker. Both it and the one-time detach hint
now point at the title bar's workspace menu rather than the macOS
Window menu, which does not exist on Windows or Linux.
- `ToggleSftp` read the panel state off the config, which is now only
what a *new* window starts with; it reads this window's own state.
- `SelectWorkspace1..9` were unbindable: registered as actions but absent
from the keymap tables. Added with no default chord (⌘1–9 is the tab
row's).
- `theme_commands`' doc comment had been captured by a function inserted
above it, and the Window menu's slot→action mapping was a second copy
of the title-bar chip's.
- CHANGELOG: drop the ⌘1–9 claim (no such binding ships), and document
the chrome tile sizing that rode along with this branch.
The local command editor consumed every Ctrl chord at the prompt, matched
or not, so two things the shell owns quietly stopped working (#163).
^J and ^M carry accept-line's control codes — Enter by another name — but
fell into `apply_readline_ctrl`'s no-op arm, so the keys did nothing at
all. Route them through the same path Enter takes, via a shared
`accept_line`, so the completion picker and the history menu treat them
identically.
^R was recognized, but only ever opened tty7's own history menu, with no
way back to a `bindkey`ed widget (fzf, percol). Add `history_search`
(default on, Settings → Terminal → Keyboard): with it off, the edited
line is handed to the shell and the raw ^R follows it, so whatever is
bound there answers. The "shell integration never engaged" notice stays
quiet in that case — ^R reaching the PTY is then the point, not a gap.
`handoff_tab_to_shell` generalizes to `handoff_line_to_shell(chord)` to
carry the ^R handoff; the Tab path is a thin wrapper over it and its
behavior is byte-for-byte unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Conflict in the sidebar's control row: main wrapped both tiles in
`occlude()` so Windows' HTCAPTION drag doesn't swallow their clicks, while
this branch moved their geometry onto `chrome_tile_sized` / the shared
`TILE_*` constants. Kept both — occluded wrappers around helper-sized tiles.
tty7 had exactly one window, so `main` opened it inline and every app-wide
duty — tray, menus, the quit hook — lived in `Tty7App`'s constructor. This
splits those apart: a *workspace* is the persistent identity (tabs, splits,
cwds, name) and a *window* is a transient view onto exactly one of them.
- `ui::windows` — the app-level window registry and the single place that
opens a window. Exactly one window per workspace is enforced there: the
daemon gives each pane a single subscriber, so a second window on the same
panes would silently steal the first's output. `open` focuses the existing
window instead. New windows cascade so one never lands on top of another.
- `WorkspaceStore` owns session.json, so windows never race each other as
writers. Closing a window *detaches* — panes keep running in the daemon
and the entry stays for the picker; `StopWorkspace` kills the sessions and
keeps the layout; `DeleteWorkspace` also forgets it.
- Window menu lists every workspace with a monogram badge and a liveness
dot, ⌘1–9 for the first nine. Same list in the palette; closed ones also
appear in a home-page picker with a coarse relative age.
- Sidebar collapse and right-panel visibility move onto `Tty7App`, so
toggling one window's chrome leaves the others alone; the config value
becomes what a new window starts with. Panel *width* stays shared — a
width is a preference, not a view state.
- Tray, menus, and the quit hook now walk the registry rather than
belonging to a single window.
Protocol goes to v2: `RemoteKind::Wsl` is a new enum variant, which is not
the additive change it looks like — the enums carry no `#[serde(other)]`, so
a v1 peer fails the whole decode and drops the pane's connection. The
handshake now catches that skew and offers a restart.
ToggleSftp only ever opened: it set the detail panel to Files and stopped,
so a key bound to it was a dead press once you were already there. Pressing
it on Files now closes the panel, flipping the same preference
toggle_right_panel does.
The remote browser's 500ms transfer poll also ended only when the browser
did, and the browser was retired solely by render_right_panel. That made a
loop holding a daemon round-trip and a full re-render depend on a caller it
can't see being invoked every frame. It now makes the same closed-panel
check on its own tick and retires itself, so the render path is an
optimisation (a frame sooner) rather than the only thing standing between a
closed panel and a poll that runs forever.
Three follow-ups to the detail-panel move:
- The browser's 500ms transfer poll only ends when the browser does, and
render_right_panel bailed on a closed panel *before* the retire check —
so closing the panel left a daemon round-trip and a full app re-render
running twice a second for a column nobody can see. Retire first, and
treat a closed panel as "not looking at that pane".
- The procs poll captured its forwards flag for the life of the loop.
That flag flips without a pane switch — a native-SSH pane you are
already watching on Info finishes connecting — and neither the pane id
nor the generation changes, so the loop never started listing. Record
it on the panel state and re-read it on each reschedule.
- Clear the job list when the browser closes, so the footer doesn't
report the previous pane's transfers until the new pane's first poll
lands.
Both SSH tools floated over the terminal: a tunnel icon and an SFTP icon
pinned top-right, opening a 460px popover and a bottom dock. They are
pane facts, so they now live where the pane's other facts already are.
Port forwarding becomes a Forwards band on the Info tab, under Ports —
one says what the pane listens on locally, the other what it routes
across the connection. Rows take the panel's language: a mono kind
letter, the bound port as the same chip a listening port gets, hover to
remove, click to edit. The add form is inline, stacked to fit the
column. The list re-lists on the Info tab's existing 2s poll, so a
forward that dies remotely turns red on its own.
SFTP becomes the Files tab's remote mode: the tab follows the detail
pane, showing a local repository tree or that machine's filesystem. Same
browsing model as before (breadcrumb, filter, `..`-led list, per-row
right-click) relaid out for ~260px — the toolbar collapses to refresh
plus a `⋯`, and the permissions column moves into the chmod form, which
now names the mode it is editing. The header carries the hostname:
the tab swaps between two filesystems as the pane changes, and it can
rename and delete.
Transfers become a footer on the panel column rather than a tray inside
SFTP. It sits below every tab, so reading Info doesn't hide a running
upload, and stays pane-scoped rather than aggregating every pane, which
would quietly make the panel a window-level transfer centre.
Opening the browser gained a step: the shell's cwd needs tty7's shell
integration on the remote, which a freshly-connected host rarely has, so
it fell through to `/`. A new SftpOp::Realpath resolves the login
directory instead. Per-pane positions are recorded on arrival, so a
first landing at `/` can no longer be remembered as a preference.
With nothing floating over the terminal any more, the ⌘F find bar gets
its top-right slot back — it used to be suppressed while those icons
were up.
The icon overrides in `assets.rs` are keyed on the asset path, and
`icon_named!` derives `IconName` from those same filenames, so #161's
redraws took effect app-wide rather than only in the detail panel. Three
Settings glyphs came along for the ride and are worse at the 16px that
page uses: both magnifiers (thinner ring, stubbier handle) and the SSH
profile row's "..." — whose filled r=2 dots are weighted for the title
bar's 18px tiles and smear into three blobs when scaled down.
Adds a `stock/` path prefix that bypasses the override table and returns
gpui-component's own glyph, and points those three call sites at it. A
prefix rather than a second copy of the art: nothing is duplicated, and
the page keeps following upstream if these glyphs ever change there.
Everything else keeps the redraw, as intended — the detail panel, the
title bar chrome, the sidebar branch mark, the file tree and the SFTP
browser are all unchanged.
The detail panel mixed four stock lucide glyphs from four different
weights and frames, and the title bar's "+" and "..." were the stock
thin cross and three stroked r=1 rings sitting next to the app's own
filled dock tiles. Side by side that read as glyphs from unrelated sets.
Thirteen icons now share one spec: stroke 1.9, corner radius 2.6-3.2, a
3.2->20.8 optical span on both axes (circles widen a little, since a
round shape reads smaller at equal geometry), and every node filled
rather than stroked -- a stroked dot hazes over below 16px, which is
where the stock ellipsis and list bullets fell apart. "+" is the one
departure: a bare cross has no enclosing shape to carry weight, so it
runs 2.4 on a tighter span.
Two shapes changed metaphor rather than just proportion: Info drops the
circled "i" -- the most-drawn icon there is, and one that says "help" as
readily as "details" -- for a panel with two lines written in it, a
picture of what the tab opens; Changes drops lucide's long arc slung
between two floating rings for an actual commit graph, which also fixes
the 11px branch mark on every sidebar row.
The override is keyed on the asset *path*, and `icon_named!` derives
`IconName` from those same filenames, so each arm redraws its glyph
app-wide rather than only in the panel. Two consequences are handled
here rather than left to discover:
- `folder` and `file` are redrawn too. Overriding only `folder-open`
would have split the file tree down the middle -- expanded rows on
the new spec, collapsed rows and every file still stock.
- the Settings nav's About row keeps its circled "i", now carried by
its own `circle-info.svg`. There the glyph labels a section, and a
document shape says nothing about *About*.
Because the paths resolve on their own, the glyphs need no call-site
changes: `Icon::new(IconName::Search)` already renders the new art.
Call sites stay on the typed enum rather than stringly paths, which a
rename would break silently.
The rail's control row is a `WindowControlArea::Drag` so the window can be
dragged by it. On Windows that maps to HTCAPTION, so the OS claims clicks as
window-drags before GPUI hit-tests — the "+" and collapse tiles never fired
their `on_click`, and collapsing the sidebar did nothing. (Fine on macOS,
where titlebar dragging doesn't gate child hit-testing, so it looked correct
there.)
Wrap both tiles in an `occlude()` div — a BlockMouse hitbox that stops
hit-testing on the button — exactly as the title-strip chrome tiles already
do.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The de3896c chrome redesign moved the right-corner chrome into
`window_chrome()` and reset its non-macOS right padding to `pr_3` (12px),
silently reverting the `pr_1` fix from 9b1c7bf. That put the "⋯" glyph centre
~45px from the minimise glyph — visibly adrift from the native window
controls, which sit on a 34px rhythm.
Restore `pr_1` (4px) so the "⋯" centre lands ~37px out, reading as part of the
same row with just enough slack not to be mistaken for a fourth window
control. macOS is unaffected (controls are on the left; the content-inset
padding still holds).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chrome tiles — sidebar collapse, the detail-panel toggle, the "⋯" menu,
the panel's own tabs — had no hover state at all. `chrome_tile_variant` set
one, but gpui-component derived the custom variant's hover from its resting
`color` and ignored the colour the caller named; that resting colour is
transparent, and transparent shaded by transparent is still transparent. The
fork now reads `hover`/`active` (l0ng-ai/gpui-component@469ab7a, picked up in
Cargo.lock), so the value here paints for the first time.
With it painting, the old `sidebar_accent.opacity(0.55)` was too faint to see
on a light background. Hover is now that fill at full strength — a 12% mix,
exactly #E2E2E2 on the default light theme, the same grey the sidebar rows
use — and selected/pressed drop one step to `list_active` so a lit toggle
still reads as on under the pointer.
The sidebar's "+" was a `ghost()` button, whose hover is a heavier grey mixed
a different way; it joins the tiles so the whole chrome answers alike.
Two things the async rewrite let slip.
The search walk bakes the dotfile setting in — hidden and ignored entries
never reach the hits — so the eye toggle did nothing while a query was up:
`retarget` only compared the query string, and the query hadn't moved. It
now compares the setting too, and flipping it re-walks.
`file_tree_refresh_roots` returning early on an unchanged root set also
skipped the watcher rebuild, which spans *every* tab's roots — so closing a
tab left its root watched for the rest of the session. The cache-drop still
hangs off the per-tab check; the rebuild now compares the union itself.
The tree did its filesystem work inside the paint. `ensure_loaded` ran a
`read_dir` plus a `.gitignore` chain compile for every root and every
expanded directory that wasn't cached, and a non-empty search box replaced
that with a breadth-first walk of up to 2000 directories — re-run on every
keystroke, because the search input notifies per `InputEvent::Change`. On
a cold cache or a large repo that is a visibly stalled frame.
Render now only reads caches. A miss becomes a queued load that runs on
the background executor and lands with a `cx.notify()`, so a
just-expanded directory fills in on the next frame rather than blocking
this one — the same trade every editor makes.
- `TreeLoader` owns the filesystem half, including its own `.gitignore`
matchers (`Arc`, not `Rc`, so they can cross threads). It is seeded from
the UI thread's compiled matchers and hands back the ones it compiled,
so the cache still warms. The ignore semantics are unchanged: deepest
match wins, `!` whitelists un-ignore, `.git` is always ignored.
- `Loads` tracks what is in flight and what a watcher event superseded
while it flew, so a repaint can't spawn a second load for the same
directory and a listing that predates a change can't install itself.
- The search is debounced 200ms, walks on the background executor, and
carries a generation so a slow walk can't overwrite a newer query's
answer. Previous hits stay on screen while the next walk flies rather
than blanking per keystroke.
- `file_tree_refresh_roots` returns early when the root set is unchanged.
It is called from render whenever the roots are empty, and it clears the
caches and notifies — so a tab that can produce no roots spun the
render loop.
Watcher events deliberately do *not* restart an open search. Doing so
starves it: the watcher's own debounce delivers a batch about as often as
the search debounce waits, so under sustained churn (a build writing into
`target/`, which the watcher reports because it knows nothing about
gitignore) every walk bowed out before reading a single directory and the
list stayed empty. A snapshot that is stale until the next keystroke is
the better failure.
Opening a `.rs` file in the code panel silently spawned rust-analyzer,
which then indexed the whole workspace — hundreds of megabytes of RAM and
a busy core — with no setting to turn it off. A terminal emulator should
not do that to its user on a click, and rather than add a flag to disable
something nobody asked for, the integration goes.
Removed: the JSON-RPC client and reader thread (`ui::lsp`), the per-server
registry, the completion / hover / definition providers installed on the
buffer, document sync (didOpen/didChange/didSave/didClose), diagnostics,
Go to Definition (F12), Find References (⇧F12) and its drawer, and the
status bar's server indicator. With them go the `lsp-types`, `ropey` and
`url` dependencies — all three were used only by this code (they remain in
the lock file as transitive deps of gpui-component and gpui, which is
expected).
Kept, and deliberately so:
- **Syntax highlighting**, which is tree-sitter, not LSP: gpui-component's
`tree-sitter-languages` feature, `InputState::code_editor(language)` and
`language_for_path` are all untouched. It is static, in-process, and
costs nothing beyond parsing the open buffer.
- ⌘S save, dirty tracking, the external-change watcher and its conflict
banner, markdown preview, soft wrap, and open-from-the-file-tree.
The module header now records *why* there is no language server, so the
next person to reach for one finds the reasoning instead of a gap.
Net −975 lines.
The branch had `gpui-component` pointed at a sibling checkout by absolute
path, which is why every CI job failed at manifest load. Point it back at
the fork's `tty7` branch (now carrying the custom-button label-color fix
the chrome tiles depend on) with the `tree-sitter-languages` feature, and
re-lock.
Review fixes on top:
- **Changes tab churned.** `right_panel_invalidate` dropped the cached
diff on every `GitStatusCache` notification — including unrelated
repos' — so the list blanked to "Loading…" and spawned a fresh
`git diff` several times a second while a pane produced output.
Replaced by `right_panel_refresh_changes`, which compares branch and
totals first and re-probes in place, mirroring the diff overlay.
- **Changes tab could wedge on "Loading…".** A probe dropped because the
cwd changed mid-flight left `diff_cwd` set and `diff` empty, and the
render path only spawns when the cwd *changes* — so nothing re-probed.
Spawn when nothing is cached and nothing is in flight.
- **Find references blocked the UI thread.** `cx.spawn_in` runs on the
main thread; the up-to-200 `read_to_string`s for the row previews now
run on the background executor, as the comment already claimed.
- **LSP frames could be lost or reordered at startup.** `send` checked
`ready` outside the `queued` lock, so a frame could park behind a
handshake that had just finished and never go out. `ready` now flips
under that lock in `mark_ready_and_flush`.
- `MarkScanner`'s ESC-in-payload branch bypassed the payload cap, so a
stream of bare ESCs inside an unterminated OSC grew the buffer without
bound.
- The file tree's search frontier used `Vec::remove(0)`; a wide tree made
that quadratic. `VecDeque`.
- `procs()` documented a pane check it didn't make; it takes the pane id
and makes it.
- Four doc comments had been orphaned onto newly inserted functions
(`pty`, `smooth_scroll`, `foreground_agent`, `file_expanded`).
Rework the detail panel's four tabs and the window chrome to read as a
deliberate, commercial-grade surface while keeping the terminal's calm.
- Chrome icons: filled-block panel toggles; Outline gets a list glyph,
Changes a git-branch, Info a redrawn mark; glyphs go to 18px on the full
foreground instead of the faint 15px secondary tone.
- Panel body: labelled Session / Processes / Ports bands, mono values,
pid/port pills, status dots on Outline, neutral M/U badges on Changes,
live counts in the headers, and roomier rows. Hue only ever lands on the
git add / remove / fail semantics, everything else stays neutral grey.
- Fixes: the corner "…" no longer jumps a pixel when the panel opens (the
top strip mirrors the TitleBar's hidden bottom border); that strip now
drags the window and double-click zooms, like the rail's; and the process
list stops flickering — the poll guard now spans the whole 2s cycle
instead of being restarted by every repaint between ticks.
The settings page's `×` sits at the window's top-right corner — the spot
the native window controls own, whose tiles are 34px. As a `small` icon
button it was 24px, which reads undersized standing there alone once the
overlay has covered the real title bar.
Give it the shape the title bar's own corner button already uses: 30px
square, a 15px glyph, `rounded_lg` (see the "⋯" in `tab_strip`). Matching
that beats the component's medium default (32px) — it is the size this
app already puts in this corner. `top` drops 6 → 5 so the taller button
still centres in the title bar's band.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The settings overlay covers the real title bar, so it lays its own drag
band across the top 40px -- absolute, full width, tagged
`WindowControlArea::Drag`. The theme panel docks to the window's top edge
beside it, which puts its header's `×` inside that band.
On Windows the band is `HTCAPTION`. gpui resolves the control area in
`Window::hit_test` by walking hitboxes top-down and stopping only at a
`HitboxBehavior::BlockMouse`; a plain `Button` isn't one, so the band's
hitbox stayed in `mouse_hit_test.ids`, the hit-test callback answered
`Drag`, and the OS took every press on the `×` as a window-drag. The
button's `on_click` never fired.
`occlude()` on the button ends the walk there, which is the same fix the
tab-strip chips carry and the one the page's own `×` has had since it was
written. Scoped to the button so the rest of the header still drags the
window. No-op on macOS and Linux.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The group header is drag-only -- it does nothing on click -- so it hovered
under `cursor_grab()`, an open hand that says "pick me up". gpui's Windows
backend has no mapping for `CursorStyle::OpenHand`: `load_cursor` matches
IBeam, Crosshair, PointingHand, the resize family and OperationNotAllowed,
then falls everything else through to `IDC_ARROW`. Win32 has no open-hand
system cursor to map it to either.
So on Windows the one affordance the header has read as "nothing to do
here", while the rows beside it (`cursor_pointer()` -> `IDC_HAND`) looked
interactive. Point there instead: not as apt as the open hand, but it is
the same cursor the rows use and it does say the header responds. macOS
and Linux keep the hand -- both backends implement OpenHand.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a right-hand detail column showing what the active pane is, not what
it prints: session facts plus its process tree and listening ports
(daemon-side procinfo, pull-based via QueryProcs), the working-tree diff,
and the file tree. Tab row lives in the title bar, body in right_panel.
Also record OSC 133 command marks client-side so the panel's Outline can
list a pane's commands and scroll back to one, keyed on row text since
absolute scrollback indices drift once history fills.
In vertical tab-bar mode the sidebar is a full-height left column and
the real TitleBar only spans the right column, so the rail's
title-bar-height top strip was dead space — you couldn't grab the window
by it. Give it the title bar's behaviour: drag to move, double-click to
zoom, driven the same way TitleBar (and the settings overlay's stand-in
strip) does it, so a plain click and a double-click still land intact.
Native-SSH panes reported no OSC 133, so the inline line editor, exit-code
marks and cwd tracking were all inert there — the daemon's OSC sniffer was
already wired up for them and simply never received anything.
Every existing integration configures a *local* process spawn (ZDOTDIR, a
bash --rcfile, fish's -C). An SSH channel offers no spawn to configure, only
the string an `exec` request carries, so the remote path recreates those same
files on the remote side and execs through them. The integration bodies are
reused verbatim rather than forked.
The bootstrap can't be shell-agnostic: sshd runs it as `$SHELL -c <string>`,
so a POSIX script is parsed by fish and a fish script by zsh. Rather than
contort one expression into parsing identically everywhere, spend a probe
round-trip (`echo __tty7_shell; echo $SHELL` — no substitution, assignment or
grouping, so it is valid in all of them) and then emit the dialect we know we
are talking to. The probe is memoized on the connection key, so extra tabs to
an open host cost nothing.
The probe's negative answer is load-bearing: a remote whose login shell is
unrecognized — or that isn't POSIX at all, where `$SHELL` echoes back
unexpanded — falls through to the plain shell request it always used.
Every arm ends by exec'ing the user's own shell, including the failure paths,
so a remote with a read-only $TMPDIR loses the integration and not the session.
zsh only gets ZDOTDIR pointed at the throwaway dir once all four redirectors
are confirmed written; a half-populated dir would silently cost the user their
dotfiles. The dir removes itself on the first precmd, by which point every
startup file has been read.
Add a per-profile switch, on by default and defaulting to on for profiles
saved before it existed, for remotes we *can* integrate but shouldn't.
Reordering used to be a swap on release: gpui's floating drag card followed
the cursor and `on_drop` moved the tab to whichever slot it landed on, with
the list itself frozen for the whole drag. Replace it with a reorder the list
performs live, and add repo group blocks as a second draggable surface.
The new `ui::reorder` module owns the geometry. A drag freezes every slot's
measured bounds and the grab point; each frame the surface reads the pointer,
asks which slot the held item now belongs in, and renders the list in that
order. The held item stays in the list (dimmed, painted over its neighbours)
and tracks the cursor pixel for pixel; the items it displaces slide 140ms.
Slot selection uses half-overlap — the held item's trailing edge past a
neighbour's centre going forward, its leading edge going back — so a tall
group block and a short one swap symmetrically.
Commit no longer goes through `on_drop`, which only fires when the pointer is
over that element at release: every frame the surface records the whole-tab
permutation a release would produce, and the root applies it when the drag
ends, wherever the cursor is. `apply_tab_order` becomes the single commit
path, replacing `move_tab`/`move_group`; it also lays every group out
contiguously, which fixes group-order corruption when a group's tabs were
non-contiguous in the tab vector.