`segmented` paints its own opaque track — deliberately, since every rung
above it was derived against that ground. But it hardcoded the window
surface, and one of its sixteen call sites is the managed-forward form in
the right panel, which is a sunk `sidebar` rail: the track landed as a
faintly darker box cut out of the column around it, and its hover /
selected / label rungs were measured against a ground that was not there.
Split off `segmented_on`, which takes the `Surface`; `segmented` keeps the
window default the settings sheet wants.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD
Two gaps in the interaction-state change.
`Theme::input` reaches the *outline* button path, which reads the field
live, but not the plain one: gpui-component derives `tokens.button_hover`
and `tokens.button_active` from `input` once, inside the `apply_config`
that `Theme::change` runs — from the stock `#2f2f2f`, before any of
`apply_theme`'s overrides exist. A snapshot never sees the fix, so a plain
`Button` still hovered and pressed in that grey and `Button::selected`
(the terminal search bar's `Aa` / `.*` toggles, the last two left in the
app) filled from `tokens.secondary_active` the same way — ~1.03:1 on
Dracula, i.e. issue #197 again, one snapshot removed from the field that
fixed it. Point those state rungs at the window ladder. `tokens.button`,
the resting fill, is deliberately left alone.
`legible_ink` picked its extreme with `is_dark`, whose 0.5 luminance
threshold is the wrong question: on a midtone ground (luminance
0.18…0.5) it says "dark" while black outreaches white, so a status seed
was driven toward white and clamped there *below* the floor with its hue
gone — precisely when a user most needs to tell an error from a warning.
Take the reachable extreme instead, as `legible_foreground` already does.
Every built-in sits far enough from the midpoint that this picks what
`is_dark` did, so the derived palettes are unchanged; only an imported
scheme could reach the divergent range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD
`title_bar_drag` reached for `window.titlebar_double_click()`, which gpui
only implements on macOS — the platform trait method is an empty default
everywhere else. On Windows that costs nothing (the row is a drag area,
so it is HTCAPTION and the OS has already toggled the window before the
app sees anything), but on Linux the row swallowed the double-click and
nothing happened, on all three rows the helper drives: the rail's top
zone and the code and diff overlays' headers.
`zoom_window()` is the maximise toggle there, and what gpui-component's
own `TitleBar` calls on Linux for exactly this reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD
Selected options were indistinguishable from their neighbours on every
bundled theme, worst on Dracula where a segmented control's active
segment measured 1.03:1 against its siblings (issue #197). The cause was
not Dracula: the app had no state model. Interaction state was a set of
fixed `mix(bg, fg, t)` ratios scattered across `neutrals()` and
`.opacity()` multipliers at call sites, and the fields nobody had
noticed — `input`, `ring`, `switch`, `switch_thumb`, `danger`,
`warning`, `success`, `link` — silently kept gpui-component's stock
values, greys and Tailwind hues with no relationship to the active
theme.
Introduce the state model the app was missing.
`Surface` is the interaction-state ladder for one painting surface, and
every rung is derived to hit a *contrast ratio* against that surface
rather than a fixed blend ratio. A fixed blend makes the perceived step
depend on the seed: selected-vs-resting ranged from 1.20:1 (Catppuccin
Latte) to 1.47:1 (Dracula). Every theme now lands on 1.70-1.72:1. The
target is 1.70 because that is where the already-signed-off Dracula
highlight sits, so the change is a no-op on the theme the look was tuned
against and a lift for the rest.
Ladders are per surface. Menu rows paint on `popover`, not the window
background, which is why the context-menu highlight measured 1.20:1
against the panel it actually sat on while nominally being the fill that
reads fine on the terminal ground.
A fill alone never communicates selection, so `Surface` carries the
label colours too: take a surface, take both channels. The app had
already learned this three times locally — the tab chip, the chrome
tiles and the sidebar each grew a hand-written fill-plus-text pair —
while every site that had not been hand-fixed shipped a fill and nothing
else. `ink_on` keeps a selected label readable on its own fill; without
it, raising a fill toward the foreground drags the label under the
resting ones around it (4.14:1 on Latte).
`Semantics` derives danger/warning/success/info/link from each theme's
own ANSI-16 instead of stock Tailwind, so a danger marker and an error
line of shell output are finally the same red. Conditioning drives a
seed toward black or white rather than toward the foreground: routing
through Rosé Pine Dawn's purple-grey foreground collapsed its red and
yellow into two indistinguishable mauves.
Switch toggles read inverted on every dark theme because the knob fell
back through two unset fields to a stock near-black. The knob now takes
the light end of the theme's axis, the unchecked track comes off the
ladder, and the checked track takes the accent — hue is the only axis
left once the knob has claimed lightness, which is why every system
switch is coloured.
`segmented` is hand-rolled. The stock `ButtonGroup::outline()` path gave
the selected segment the same border and the same label colour as its
siblings, leaving one fill as the entire signal, and exposes no way to
vary weight.
Nine tests pin the properties rather than the current taste, so retuning
the four contrast constants does not force a test edit but abandoning
the model does.
macOS fills the window's leading corner with the traffic lights and
`TITLE_BAR_LEAD` reserves them 80px. Everywhere else that corner held
nothing: the caption row's only contents are the rail's "+" and collapse
at the rail's right edge and the corner chrome at the window's, so the
left third of the row read as unfinished rather than restrained — while
Windows treats the top-left as the app's identity slot.
Three parts, all of them about that row:
- `window_mark()` draws the "duo" mark (the app icon's own art) at the
head of the rail on `CONTENT_INSET`, the line the search box and every
row label below it start on, and follows the rail's controls into the
title strip when the sidebar collapses. It is drawn, never clicked: no
hover capsule, and deliberately no `occlude()`, so the drag region
underneath still takes the press and the strip stays grabbable.
- The rail's stand-in row now reserves the same hairline the real
`TitleBar` draws inside its own height. Without it the bar centred
content on 19.5 and the rail on 20, and the mark hopped half a pixel
as collapsing the rail handed it from one to the other.
- With the detail panel open off macOS the bar is hoisted above
`[terminal | panel]` so the window controls can reach the corner, which
left the code and diff overlays — anchored to the terminal column —
starting 40px down, with headers drawn to *be* the title bar landing a
row low. They now hang on the row that owns the bar, inset by the
panel's width. Covering the caption row that way needs the headers to
carry its gestures, which neither ever did with the panel open or
closed: `title_bar_drag()` gives both (and the rail's row, which grew
the same wiring by hand) drag-to-move and double-click-to-zoom, and
their controls are `occlude()`d so HTCAPTION stops eating the clicks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD
The default `font_fallbacks` list was macOS-only -- Menlo, Hasklug Nerd
Font Mono, Maple Mono NF CN, Apple Color Emoji. Fallbacks resolve by
family name against installed fonts, so off macOS the whole chain matched
nothing and every glyph the primary lacked was left to the platform's own
cascade. Bundled Hack maps 1548 codepoints and zero ideographs, so on
Windows that was every Chinese character in every pane, and every emoji.
The fall-through is not only a matter of which face you get. `element.rs`
pins each wide cell to `2 x cell_width`, and Hack advances 0.60205em, so
a two-column slot is 1.2041em -- while every stock CJK face (Microsoft
YaHei, PingFang SC, Noto Sans CJK) advances 1.0em. `force_width`
left-aligns, so the ideograph hugs the left of its slot and the remaining
0.2em shows up as a gap on the right of every character. Measured on
Windows at font_size 15: left bearing 1.49px, right bearing 4.90px.
Branch the defaults per platform, keeping Maple Mono NF CN first
everywhere -- 0.6em Latin, 1.2em CJK, the one exact two-cell fit against
Hack (bearings 3.06px / 3.62px, ink centered). It stays referenced by
name only, never bundled, at ~20MB per weight.
Changing `Config::default` alone would reach nobody who already has a
`config.json`, which is every existing user. So `fallback_chain` appends
the platform's stock faces the same way it already pins Hack: a fallback
is consulted only after everything ahead of it has missed, so appending
can never displace a face the user chose, and the file is never rewritten.
Verified by driving two builds against one config naming only absent
macOS faces: before, the CJK line differed from an explicit Microsoft
YaHei chain by 3571 pixels (the cascade picked something else); after, it
is pixel-identical.
The detail panel (Info / Outline / Changes / Files), the remote SFTP
listing and the tab rail all scrolled with gpui's `overflow_y_scroll()`,
which paints nothing: a deep file tree or a long tab list gave no hint
that there was more content, or where in it you were (issue #185).
Hang gpui-component's `Scrollbar` over each of those containers through
one shared helper (`ui::scrollbar`). The handle stays app-owned so
`scroll_to_item` keeps working — that rules out gpui-component's
`overflow_y_scrollbar()`, which mints its own handle internally.
Theme the bar from the active theme's own background→foreground mix
ladder rather than the stock fixed greys, and keep the track transparent
so the thumb floats instead of laying a slab down the panel edge.
Show mode follows the platform: auto-hide on macOS (and anywhere else the
OS uses overlay scrollbars), permanently visible on Windows and Linux.
gpui-component's own default picks `Hover` there, which only reveals the
bar within 16px of the edge — that is the "no scrollbar at all" the issue
reports from Windows.
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
Option-as-Meta had no effect for anyone typing with a CJK input source.
The whole setting was dead for them: with Pinyin selected, macOS reports
Option chords as printable text (Option+B composes the special
character), so gpui routed them to the IME before the key handler ran.
The IME committed the composed character and swallowed the event --
on_key_down never ran, and reshape_option_keystroke never got a say.
Switching to ABC made it work again, which is why this looked
intermittent. Verified on the wire: with Pinyin active Option+F/Option+B
put c692 / e288ab on the PTY where ESC f / ESC b belong.
The routing decision lives in gpui's macOS backend and is asked once per
view, with no keystroke in hand, so it could not answer "IME for text,
but not for this chord". gpui now comes from our fork, whose one commit
passes the keystroke to prefers_ime_for_printable_keys; the default
implementation ignores it, so no existing handler changes behavior. The
terminal answers per key: an Option chord with the setting on stays on
the dispatch path, everything else still prefers the IME.
Gated on the setting deliberately. With Option-as-Meta off the chord is
text input and the IME is the right owner -- it is what makes dead keys
(Option+E then E -> e-acute) compose at all.
The fork is wired in with [patch] on the source rather than by editing
the gpui pins, because gpui-component declares its own gpui from the
upstream URL and a pin swap would put two incompatible copies of gpui in
the tree. Fetching a repo that size needs the git CLI; cargo's built-in
libgit2 transfer times out partway through.
Fixes#177
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
Enabling `kitty_keyboard` (#184) made an upstream `alacritty_terminal` bug
reachable from any foreground program. `push_keyboard_mode` caps its stack by
removing from `title_stack` instead of `keyboard_mode_stack` -- a copy-paste
slip from `push_title` that compiles because both are `Vec`s and the removed
value only feeds a `trace!`.
Two consequences. Each overflowing push silently drops a saved window title, so
a later XTPOPTITLE restores the wrong one. And once the title stack is empty,
`Vec::remove(0)` panics: 4097 unpopped `CSI > 1 u` pushes -- roughly 20KB of
output -- kill the `tty7-remote-reader` thread and freeze the pane. The depth
cap never trimmed `keyboard_mode_stack` at all, so it was doing nothing.
Hostile output is not required. A TUI that pushes without popping (per redraw,
per keypress) reaches 4096 on its own in a long session.
Pin our fork of Zed's fork instead: `tty7` is Zed's `fcf32fe` plus the one-word
fix. No `[patch]` section is needed -- tty7 is the only crate in the tree that
depends on `alacritty_terminal`, so a plain URL/rev swap cannot split it into
two incompatible copies the way the gpui pin would.
The regression test guards the pin rather than our own code: it pushes past the
4096 cap and then queries, using the reply as a liveness probe. Verified to fail
against the unpatched rev.
Still present on alacritty master as of 852e971. Drop the fork once it lands
upstream.
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.
The GUI-launch locale fallback exported a literal `LC_CTYPE=UTF-8`. That
name is a BSD libc alias with no glibc equivalent, and the stock
`ssh_config` ships `SendEnv LANG LC_*`, so it rode along to every host we
ssh into. There `LC_CTYPE` outranks the `LANG` the host sets for itself
and then fails to load, dropping the remote shell to the C locale --
re-creating the mangled non-ASCII output the fallback exists to prevent.
Derive the locale from the system locale instead, the way Terminal.app
and iTerm2 do: reduce the CFLocale identifier to its POSIX `lang_REGION`
stem, and fall back to `C.UTF-8` then `en_US.UTF-8`. Every candidate is
checked against `/usr/share/locale` before it is exported, so tty7 never
hands a shell a name the C library cannot load -- including on machines
whose region combination has no installed locale (`en_CN` is an ordinary
macOS setting that resolves to no locale at all).
Still `LC_CTYPE` only, and still only when no locale is inherited or
configured, so message/date/number localization and explicit user
overrides are untouched.
Fixes#178
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.
Grok Build exposes a Claude Code-shaped hook surface, so tty7 can now
install into it and give grok panes live session status and
resume-after-restart, not just a brand chip.
- Owned hook file at ~/.grok/hooks/tty7.json (grok loads every JSON file
there; global hooks need no folder-trust grant), so the user's own
hooks are never touched.
- Read camelCase payload keys: grok's envelope sends sessionId, and
without it restore loses the id --resume needs.
- Relabel events that arrive through grok's Claude-compat scan of
~/.claude/settings.json, keyed on the GROK_HOOK_EVENT var its hook
runner injects — otherwise a grok pane reports Claude Code, and having
both integrations installed emits every turn under two identities.
- Resume via `grok --resume <id>`, stripping the flags that would fight
the injected id (--resume/--load/--continue/--session-id/--fork-session)
or relocate the session (--worktree/--worktree-ref).
Notification is subscribed with a matcher for elicitation_dialog only.
Grok dispatches its permission_prompt notification before the permission
system decides, so it fires on essentially every tool call, auto-approved
ones included; escalating that to the amber "needs you" state would flash
the pane and fire a desktop notification on every tool a turn runs.
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.
The unbracketed fallback stripped a CRLF clipboard's stray \r, but the
bracketed branch passed it inside the markers, leaving the line break to
whatever the far side does with a CR in a paste — a blank line under zsh,
a literal ^M under a shell that doesn't translate it. Normalize every
line break to one \n in the same pass that strips ESC, so both branches
start from the same shape.
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.
Replaying each embedded newline as its own CR made the shell's line
editor run a full prompt cycle per line -- preexec, the user's precmd
chain (git-status prompts, conda), a syntax-highlight pass over the
whole buffer, plus our own OSC 133 D follow-up work. A pasted block
crawled down the screen line by line, as if the command were being
retyped: instant to paste (the local editor never touches the PTY),
then visibly slow to run.
When the shell has bracketed paste enabled, send the buffer as one
paste followed by a single CR. zle keeps the embedded newlines in its
buffer, so backslash / open-quote continuation and heredocs still parse
as one unit -- the PS2 assembly the per-line replay existed for now
happens inside the buffer instead of on the wire -- and the block costs
one prompt cycle whatever its line count. Shells without the mode keep
the per-line fallback.
This is the shape core::agent_prompt::submit_bytes has always used for
agent prompts; the shell path was the odd one out.
Two fixes ride along, both from the same helper:
- ESC is stripped on the submit path (it already was on the clipboard
paste path), so clipboard text can't smuggle its own ESC[201~ end
marker or hand zle an editor command.
- A CRLF clipboard no longer turns \r\n into \r\r -- a stray blank
Enter submitted mid-command.
An empty buffer skips the markers: zsh's bracketed-paste-magic, which
oh-my-zsh turns on, errors on a paste with nothing between them.
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.