Tab at a path position in a native-SSH pane had nothing to offer: path
candidates come off the local filesystem, and a remote pane deliberately
passes no local cwd, so `complete` returned `None`. A no-match hands the
line to the shell (#136), which costs the user the inline editor until
the next prompt — so in practice every path Tab in an SSH session dropped
them back to the raw shell for the rest of that line.
Ask the far side instead. The pane already holds an authenticated russh
connection, and `SftpList` — the same request the SFTP panel browses with
— lists a remote directory over it. No shell involvement: nothing is
echoed into the scrollback, no prompt hooks need suppressing, and there
is no background process to cancel when the user submits.
The listing is a network round-trip, so it can't answer the keystroke
synchronously. Results land on the main thread and only then behave as a
local Tab would; a line that moved on in the meantime drops the answer,
and an empty one falls back to the shell handoff, so a directory we
cannot read is no worse off than before.
completion.rs stays pure: it splits the word into a request and turns the
answer into candidates, both unit-tested. Candidate rules mirror the
local path engine exactly — hidden entries need a dot prefix, `cd` takes
directories only, symlinks follow to their target's kind, same ordering
and cap. Positions a listing cannot help (command position, `~` without
the remote's $HOME, no absolute remote cwd) decline and let the shell
have the Tab.
Only native-SSH panes qualify: a foreground `ssh` typed into a local
shell and a WSL pane have no tty7-owned connection to ask.
The OSC sniffer folded a whole chunk's 133 marks into a single shell
state and `apply_signals` sent one `Prompt` frame for it. That loses
information the client depends on: it counts `at_prompt` false -> true
edges as prompt *cycles*, which is how it tells a fresh prompt from a
same-prompt redraw (a prompt framework re-emitting the PS1-embedded
`133;B` on reset-prompt bumps the report seq but must not count).
A chunk carrying a whole command cycle (`C` ... `D`) therefore reported
only the final at-prompt state, and the edge vanished. Over SSH that is
routine rather than exotic: a fast command's `C`, its output and its `D`
leave the remote together and arrive in one read.
Report one state per crossing instead. Marks on the same side of the
boundary still fold into their latest state, so the ordinary `D`/`A`/`B`
prompt draw costs exactly one frame as before.
Every config-dir file is read by a loader that treats any parse error as
"there is no file" and falls back to defaults. serde_json rejects the
U+FEFF a BOM puts before the opening brace, so a BOM never surfaced as a
broken config — it surfaced as an absent one, and the app booted on
defaults with nothing to explain it.
Windows makes that easy to hit by accident: PowerShell's `>`, `Out-File`
and `Set-Content -Encoding utf8` all write a BOM, so editing config.json
from a shell was enough to lose every setting.
Strip a leading BOM in the three loaders whose files people hand-edit:
config.json, session.json (which dropped every workspace the same way),
and themes/*.yaml. read_to_string decodes the marker to one U+FEFF char,
so this strips the char, not the three raw bytes — and only the first
one, since a second is content the parser should still reject.
window.json and update.json are left alone: they are machine-written
state a relaunch rebuilds, never hand-edited.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD
The combining-mark cases carry exactly one mark — `e` + U+0301, and
❤ + U+FE0F. Thai routinely stacks two on one consonant: `ที่` is U+0E17
with an above-base vowel and a tone mark both sitting on it, and
Devanagari, Arabic and Hebrew stack the same way.
Nothing pinned that. Truncating the mark iterator to its first element
leaves all thirteen `segment_row` tests green — the cluster still forms,
still spans one column, and still leaves its neighbour intact. Only the
trailing codepoint goes missing, which in Thai is the one carrying the
tone: ไม่ (not) and ไม้ (wood) both collapse to ไม.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Cell::zerowidth` answers out of alacritty's one lazily boxed `extra`,
which also holds SGR 58 and OSC 8 — so a cell carrying nothing but an
underline color or a hyperlink reports `Some(&[])`, not `None`.
`snapshot_cell` mapped that straight into `RenderCell::marks`, and an
empty list is still `Some`: every SGR-58 or OSC-8 cell then failed
`is_blank`, broke the run it sat in, and took a `RowSeg::Cluster` of its
own — one `shape_line` per character for a whole `ls --hyperlink`
listing, with the powerline fast path skipped on the way past.
Filter the empties at the source, where the `Option` is built.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD
`Config::load` turns any parse error into *defaults*, so a config that
fails to deserialize doesn't fall back field-by-field — it comes back with
`confirm_window_close: true` and nothing said. The struct has no
`deny_unknown_fields` today; this pins that, since the opt-out is exactly
the setting whose silent reversal nobody would notice.
Also index the Windows/Linux spelling of the chord: the prompt is reached
by Ctrl-W off macOS, and search only knew "cmd-w".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD
`/proc/<pid>/cwd` still resolves after the directory is unlinked, and the
kernel spells that `<path> (deleted)` — a label, not a path. That was
harmless while the reading only backfilled `List` for a pane with no cwd
at all; now that the poll broadcasts it, one `rm -rf` from another pane
(or a `git clean`) leaves every new tab, split, git probe and path
completion pointed at a directory that cannot be opened, and the pane's
last good directory has already been overwritten.
Treat an unstat-able reading as no reading, so the foreground group's
answer is discarded and the shell pid gets its turn — the same shape the
rest of the chain already uses for a pid it cannot inspect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD
`probed_cwd_keeps_the_shells_spelling_for_a_symlinked_path` built its
fixture with `std::os::windows::fs::symlink_dir`, which needs
`SeCreateSymbolicLinkPrivilege` — a privilege an unelevated shell without
Developer Mode does not hold. The test therefore failed on any normal
Windows checkout with `ERROR_PRIVILEGE_NOT_HELD` (os error 1314) before
reaching a single assertion, observed here on Windows 11.
Nothing is lost by making it `cfg(unix)`: `foreground_cwd` answers `None`
off macOS/Linux, so no Windows pane ever reaches that comparison. What
every platform *does* run — a reading identical to the one already
reported, resolved with no filesystem access at all — gets its own test
instead, because a re-send there would push a `Cwd` frame and the git
probe behind it twice a second for the life of an idle pane.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD
An emoji written as base + U+FE0F rendered wrong twice over. Both halves
came from the variation selector arriving as a zero-width combining mark
after the column budget was already spent.
Width, in the `alacritty_terminal` pin (bumped to the fork's b79e704):
`input` reserves columns one `char` at a time, so a base whose East
Asian Width is Neutral -- U+2764 in `❤️`, U+1F5C2 in `🗂️`, U+26A0 in
`⚠️` -- kept the single column it was given, its glyph bled over the
next cell, and every column after it on the line shifted left by one.
The fork re-scores the sequence with `UnicodeWidthStr`, which is where
UTS #51's width-2 rule lives, and widens the cell to match.
Presentation, here: `snapshot_cell` copied only `cell.c` into
`RenderCell`, so `cell.zerowidth()` was dropped before the shaper ever
saw it. `❤` and `❤\u{FE0F}` reached gpui as the same string and picked
the same text-presentation face -- a black heart where every other
terminal shows a red one. `RenderCell` now carries the marks and a new
`RowSeg::Cluster` shapes them with their base. That restores every
combining mark, not just the selectors: `e` + U+0301 was being dropped
the same way.
A marked cell never joins a batched run. Marks add characters without
adding columns, which is exactly the correspondence `force_width` uses
to pin one glyph per column in a `Run` or `Wide` segment.
Fixes#203.
A pane learned its working directory from OSC 7 alone, which only shells
tty7 manages to inject its integration into ever emit. A shell that execs
into another one from its rc file (`exec fish` at the end of .zshrc), a
nested shell started by hand, or any shell with no integration at all
emits none — and since a pane's cwd is seeded with its spawn directory,
such a pane does not report *no* directory, it reports a permanently
stale one. New tabs and splits, the git probe and path completion all
follow it to the wrong place, with nothing on screen to say why.
Read the cwd from the process table too, on the same half-second
foreground poll that already detects SSH sessions and coding agents, and
reconcile it with what the pane reports:
- A remote pane is left alone. The local process table can only see the
ssh client's own directory, which is the confusion apply_remote_context
clears the cwd to avoid in the first place.
- No reading is "nothing to read", never "no cwd", so it cannot clear one.
- When both name the same directory the shell's spelling wins. $PWD keeps
the symlinked route the user walked in through, and that is the path a
new tab should open in; only a genuine disagreement moves the pane.
The three platform cwd readers move from DaemonPane methods to free
functions so the reader thread can call them; the method stays as a thin
delegate for List.
Fixes#187
Closing the last window pops "Close Window?" every time. The prompt was
only ever teaching, not protection — Cmd-Q, the tray's Quit and the
palette's Quit all leave without asking, and nothing is lost either way
since the panes keep running in the daemon. Once the user knows that,
being asked on every quit is friction.
Adds `confirm_window_close` (default true, so nothing changes for
existing configs) and a Window & Tabs toggle. Off makes the last window
close like any other: detach the workspace, quit.
`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
The new hover-invalidation test read 24 rows into the pane because the
harness names that size, but the test window lays itself out first and
`set_grid_size` had already moved the terminal to the measured geometry
— so the "same size changes nothing" call was a size *change*, and it
cleared the cell the assert expected. Set the starting geometry from the
test instead of inheriting whatever the window measured. Unix-only
module, so this only shows up in CI.
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
Two gaps left by the hover fix, both the same crash class.
`grid_smart_range` indexes `grid[click.line]` the moment it starts —
`hyperlink_run`, then `logical_line_at` — with the row a double-click
carried in from the frame that dispatched it. That row can outlive its
grid the same way a hovered cell can: a split, a window drag, or the
reader thread applying the daemon's replayed attach geometry between
paints. `Grid`'s `Index<Line>` only `debug_assert`s the bound, so a
release build walks off the storage, and the click arrives in a gpui
`extern "C"` callback where the panic aborts instead of unwinding.
Guard the row up front, exactly as `TerminalView::grid_line` does.
`set_grid_size` forgot the hovered *cell* but kept the link it had
resolved. That link is stored in grid coordinates, so after the reflow
it underlines whatever text now sits at those coordinates and holds the
pointing-hand cursor over it until the pointer moves again — clamping's
wrong-but-silent hover, one step removed. Drop it with the cell.
Both halves get a regression test; the smart-select one panics on the
storage `debug_assert` without the guard.
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.
`panic!` records the location with the platform's own separator, so the
assertion on `src/core/crash.rs` failed the Windows job while the hook
itself worked fine there.
Hovering a pane records the cell under the pointer so ⌘-hover can
underline links. Nothing invalidated that cell when the grid shrank
under it — a vertical split, un-maximizing a pane, a smaller window —
so the remembered row could name a line the grid no longer has.
The next modifier press then walked into it: ModifiersChanged →
set_link_modifier → every pane in every tab → link_span_at, which
indexed the grid with the stale row. Grid's Index<Line> only
debug_asserts the bound, so a release build underflows the storage
index and panics on the slice check. That panic happens inside gpui's
extern "C" input callback, where it cannot unwind — the process
aborts, and the OS crash report keeps the abort rather than the panic.
Validate the row against the grid before indexing (one choke point,
shared by hover and click), and drop the remembered cell on resize.
Two more ways the same class of crash reached users:
- `new_terminal` unwrapped a failed daemon spawn with `expect`, so a
daemon that died took the app down with the next ⌘D/⌘T instead of
reporting the failure. It returns a Result now; split, new tab,
worktree tab and reopen-closed-tab surface a notification, and
session restore drops the leaves it can't rebuild.
- A spawn that fails because nothing is listening on the socket now
starts a daemon and retries once, the way a disconnect mid-Spawn
already did. `connect()` had to stop interpolating the io::Error
into a string for that classification to see it.
And so the next one is diagnosable at all: a panic hook writes the
message, location and backtrace to `crash.log` in the config dir. The
GUI has no logger and no stderr when launched from Finder, and the
crash report loses the message to the abort.
Fixes#201
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).