Commit Graph
370 Commits
Author SHA1 Message Date
thomas dc8faec5d9 Merge remote-tracking branch 'origin/main' into fix/pane-cwd-from-process-table
# Conflicts:
#	CHANGELOG.md
2026-07-27 10:40:53 +08:00
l0ng-ai c5a26a7794 fix(daemon): track a pane's cwd from the process table when OSC 7 is absent
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
2026-07-27 10:20:06 +08:00
l0ng-ai f90dfd7c35 Merge pull request #205 from l0ng-ai/feat/theme-interaction-state
feat(theme): derive interaction state and status colors from the theme
2026-07-27 09:53:11 +08:00
l0ng-ai 7731eec5f7 Merge pull request #204 from l0ng-ai/fix/stale-hover-row-crash
fix(terminal): stop a stale hover cell from indexing a shrunken grid
2026-07-27 09:50:50 +08:00
thomas 6f91c9a640 Merge remote-tracking branch 'origin/main' into feat/theme-interaction-state 2026-07-27 09:48:20 +08:00
thomas 28d451ec2c Merge remote-tracking branch 'origin/main' into fix/stale-hover-row-crash 2026-07-27 09:46:09 +08:00
thomasandClaude Opus 5 dddd07a360 fix(ui): give the segmented control the surface it actually paints on
`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
2026-07-27 09:42:02 +08:00
thomasandClaude Opus 5 d3549bce23 test(terminal): pin the geometry the resize test starts from
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
2026-07-27 09:41:54 +08:00
l0ng-ai 0d0acb5828 Merge pull request #202 from l0ng-ai/feat/windows-titlebar-anchor
feat(chrome): make the title bar's line whole off macOS
2026-07-27 09:40:07 +08:00
thomasandClaude Opus 5 4107829bf8 fix(theme): finish the sweep — plain-button states and midtone grounds
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
2026-07-27 09:39:26 +08:00
thomasandClaude Opus 5 b229fd18f0 fix(terminal): refuse a stale row in smart select, and drop the stale link
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
2026-07-27 09:35:26 +08:00
thomasandClaude Opus 5 c20c4e8c66 fix(chrome): zoom on Linux when a stand-in title bar is double-clicked
`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
2026-07-27 09:35:23 +08:00
l0ng-ai dcb8ba0c77 feat(theme): derive interaction state and status colors from the theme
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.
2026-07-27 09:07:29 +08:00
l0ng-ai c31826ea57 test(crash): match the panic location without a path separator
`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.
2026-07-27 08:35:48 +08:00
l0ng-ai 6edfb28200 fix(terminal): stop a stale hover cell from indexing a shrunken grid
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
2026-07-27 08:21:36 +08:00
thomasandClaude Opus 5 a67cf2b2ad feat(chrome): make the title bar's line whole off macOS
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
2026-07-26 23:08:18 +08:00
l0ng-ai 313c1c3c8b Merge pull request #195 from l0ng-ai/fix/windows-font-fallbacks
fix(fonts): name a CJK and emoji fallback the host platform actually ships
2026-07-26 21:03:33 +08:00
l0ng-ai 8bdcaf4290 fix(fonts): name a CJK and emoji fallback the host platform actually ships
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.
2026-07-26 20:59:11 +08:00
l0ng-ai 1504f92896 Merge pull request #193 from l0ng-ai/feat/detail-panel-scrollbar
feat(ui): give the right panel and tab rail an overlay scrollbar
2026-07-26 20:58:55 +08:00
l0ng-ai a0d153c546 feat(ui): give the right panel and tab rail an overlay scrollbar
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.
2026-07-26 20:46:00 +08:00
l0ng-ai 87273d980b Merge pull request #192 from l0ng-ai/fix/windows-ctrl-click-links
fix(links): open links on Ctrl+click on Windows and Linux
2026-07-26 20:45:45 +08:00
l0ng-ai 7aeca8adf7 fix(links): open links on Ctrl+click on Windows and Linux
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
2026-07-26 20:36:05 +08:00
l0ng-ai 4dd61be56f Merge pull request #191 from l0ng-ai/fix/option-as-meta-under-ime
fix(input): keep Option chords out of the IME when Option is Meta
2026-07-26 20:35:43 +08:00
l0ng-ai ecb5ab1ed7 fix(input): keep Option chords out of the IME when Option is Meta
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
2026-07-26 20:31:32 +08:00
l0ng-ai fdd9b1adbb Merge pull request #190 from l0ng-ai/feat/icon-set-humanist
feat(icons): redraw the bundled icon set to one humanist spec
2026-07-26 20:19:38 +08:00
l0ng-ai a15f28eb73 feat(icons): redraw the bundled icon set to one humanist spec
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.
2026-07-26 20:15:15 +08:00
l0ng-ai 4d09a4ec24 Merge pull request #188 from l0ng-ai/fix/windows-detail-panel-top
fix(right-panel): put the window controls back in the window's corner
2026-07-26 20:09:20 +08:00
l0ng-ai 1eb50fbdba fix(files): move the dotfile switch into the tree's right-click menu
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.
2026-07-26 20:01:04 +08:00
l0ng-ai 6185d625cc fix(right-panel): stop drawing the panel toggle as selected
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.
2026-07-26 20:01:04 +08:00
l0ng-ai a69beaded9 fix(right-panel): put the window controls back in the window's corner
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).
2026-07-26 20:01:04 +08:00
l0ng-ai 0226a1c192 Merge pull request #181 from l0ng-ai/fix/linux-follow-system-refcell
fix(linux): stop `theme_follow_system` from panicking every launch
2026-07-26 20:00:28 +08:00
l0ng-ai 985a292513 fix(linux): stop theme_follow_system from panicking every launch
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
2026-07-26 19:55:38 +08:00
l0ng-ai da10381258 Merge pull request #194 from l0ng-ai/fix/alacritty-keyboard-mode-stack
fix(deps): pin a patched alacritty to survive deep keyboard-mode pushes
2026-07-26 19:33:00 +08:00
l0ng-ai 311d9285ff fix(deps): pin a patched alacritty to survive deep keyboard-mode pushes
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.
2026-07-26 19:11:39 +08:00
l0ng-ai 3bbeb4f0a2 Merge pull request #184 from yoyooyooo/fix/enable-kitty-keyboard
fix(input): enable Kitty keyboard negotiation
2026-07-26 19:04:44 +08:00
jackie e72837a39c fix(input): enable Kitty keyboard negotiation 2026-07-26 11:43:48 +08:00
l0ng-ai 40003ff423 chore(release): v26.7.4 v26.7.4 2026-07-26 10:27:55 +08:00
l0ng-ai 78d86b9949 Merge pull request #175 from l0ng-ai/feat/ux-overhaul
feat(ux): rebuild the menu bar, command palette, and Settings IA
2026-07-26 10:12:52 +08:00
l0ng-ai 6c10f6c768 Merge pull request #176 from l0ng-ai/fix/chrome-icon-weight
fix(chrome): soften idle chrome-tile glyphs to the sidebar's ink weight
2026-07-26 10:09:38 +08:00
l0ng-ai 89b6fc5ae5 Merge pull request #174 from l0ng-ai/feat/grok-hooks
feat(agents): wire the rich status channel into Grok Build
2026-07-26 10:09:21 +08:00
l0ng-ai 8bcf6e06f9 Merge pull request #172 from l0ng-ai/fix/release-draft-gate
fix(release): assemble the release as a draft once every platform is green
2026-07-26 10:09:04 +08:00
l0ng-ai e6de537124 fix(palette): label the sidebar and right-panel toggles from this window's state
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.
2026-07-26 10:08:50 +08:00
l0ng-ai 6018fc1034 Merge pull request #180 from l0ng-ai/fix/locale-derive-from-system
fix(macos): derive the seeded UTF-8 locale from the system locale
2026-07-26 09:41:09 +08:00
l0ng-ai 90763385e2 fix(macos): derive the seeded UTF-8 locale from the system locale
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
2026-07-26 09:31:03 +08:00
l0ng-ai f613d04398 Merge pull request #173 from yoyooyooo/fix/macos-utf8-locale
fix(macos): seed UTF-8 locale for GUI-launched shells
2026-07-26 08:54:43 +08:00
l0ng-ai 297fdfcb0f fix(chrome): soften idle chrome-tile glyphs to the sidebar's ink weight
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.
2026-07-25 22:02:40 +08:00
l0ng-ai 4ac7d30178 feat(ux): rebuild the menu bar, command palette, and Settings IA
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.
2026-07-25 20:19:00 +08:00
l0ng-ai eced0af754 feat(agents): give Grok its brand avatar
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.
2026-07-25 20:17:24 +08:00
l0ng-ai c76ef87d02 feat(agents): wire the rich status channel into Grok Build
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.
2026-07-25 19:17:05 +08:00
jackie d95229411d fix(config): respect explicit locale overrides 2026-07-25 18:17:31 +08:00