Commit Graph
256 Commits
Author SHA1 Message Date
l0ng-ai b139568ca3 fix(daemon): report every prompt-boundary crossing in one chunk
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.
2026-07-27 13:38:47 +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 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 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 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 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 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 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 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
jackie e72837a39c fix(input): enable Kitty keyboard negotiation 2026-07-26 11:43:48 +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 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 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 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
jackie 8b2b4645c6 fix(macos): seed UTF-8 locale for GUI-launched shells 2026-07-25 18:09:11 +08:00
l0ng-ai 5d814c0d70 feat(config): default the tab bar to the left sidebar 2026-07-25 16:22:26 +08:00
l0ng-ai efd2213cf9 Merge branch 'main' into fix/prompt-ctrl-chords
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.
2026-07-25 15:25:35 +08:00
l0ng-ai 7d7d3ce882 fix(windows): start fresh workspaces with a shell, and say where a closed one went
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.
2026-07-25 15:07:47 +08:00
thomasandClaude Opus 5 443f04f3c6 fix(prompt): stop swallowing ^J, and let ^R go to the shell on request
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>
2026-07-25 15:07:31 +08:00
l0ng-ai f2bbd8b93e Merge branch 'main' into feat/multi-window 2026-07-25 14:54:01 +08:00
l0ng-ai 7789dc4c36 Merge branch 'main' into feat/multi-window
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.
2026-07-25 14:12:58 +08:00
l0ng-ai e41afaf857 feat(windows): one window per workspace
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.
2026-07-25 14:10:44 +08:00
l0ng-ai 3798efb51e fix(ssh): make ToggleSftp toggle and the SFTP poll own its lifetime
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.
2026-07-25 11:55:56 +08:00
l0ng-ai 1446b92d0b Merge pull request #166 from l0ng-ai/feat/ssh-actions-in-detail-panel
feat(ssh): fold port forwarding and SFTP into the detail panel
2026-07-25 11:48:30 +08:00
l0ng-ai 44752de6e5 Merge pull request #164 from l0ng-ai/feat/bracketed-submit
perf(input): submit a multi-line command as one bracketed paste
2026-07-25 11:48:07 +08:00
l0ng-ai 7bf35897a2 fix(panel): retire the remote browser when the panel closes
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.
2026-07-25 11:43:44 +08:00
l0ng-ai 0ac9f8aebf fix(input): normalize CR before the bracketed submit
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.
2026-07-25 11:43:36 +08:00
l0ng-ai 0ebe68a453 feat(ssh): fold port forwarding and SFTP into the detail panel
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.
2026-07-25 11:25:06 +08:00
l0ng-ai a7bbe96b16 fix(settings): keep the stock glyphs on the Settings page
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.
2026-07-25 11:19:04 +08:00
l0ng-ai 433a545444 perf(input): submit a multi-line command as one bracketed paste
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.
2026-07-25 10:48:55 +08:00
l0ng-ai fd3de8a3ed Merge pull request #162 from l0ng-ai/fix/windows-titlebar-chrome
fix(windows): titlebar chrome — ⋯ menu rhythm + clickable sidebar controls
2026-07-25 10:34:56 +08:00
l0ng-ai ace2b416ac style: rustfmt the re-nested sidebar add button
The occlude() wrapper pushed the chrome_tile call one level deeper, past
rustfmt's width for the single-line Button builder.
2026-07-25 10:30:13 +08:00
l0ng-ai 1ff907f4cf feat(icons): draw the detail panel and chrome glyphs to one spec
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.
2026-07-25 10:27:23 +08:00
thomasandClaude Opus 4.8 a00abe1c09 fix(windows): un-swallow the sidebar collapse/new-tab buttons
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>
2026-07-25 10:12:52 +08:00
thomasandClaude Opus 4.8 7d190a8298 fix(windows): restore the ⋯ menu's native window-control rhythm
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>
2026-07-25 10:12:52 +08:00
l0ng-ai 73588663a0 fix(chrome): give every icon tile the same soft hover fill
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.
2026-07-24 18:25:02 +08:00
l0ng-ai 09abfd5741 Merge pull request #160 from l0ng-ai/perf/file-tree-async
perf(file-tree): list directories off the render thread
2026-07-24 18:12:56 +08:00
l0ng-ai 8a4168ca5c fix(file-tree): re-walk on the dotfile toggle, retire dropped watches
Two things the async rewrite let slip.

The search walk bakes the dotfile setting in — hidden and ignored entries
never reach the hits — so the eye toggle did nothing while a query was up:
`retarget` only compared the query string, and the query hadn't moved. It
now compares the setting too, and flipping it re-walks.

`file_tree_refresh_roots` returning early on an unchanged root set also
skipped the watcher rebuild, which spans *every* tab's roots — so closing a
tab left its root watched for the rest of the session. The cache-drop still
hangs off the per-tab check; the rebuild now compares the union itself.
2026-07-24 18:09:05 +08:00
l0ng-ai 182298a223 perf(file-tree): list directories off the render thread
The tree did its filesystem work inside the paint. `ensure_loaded` ran a
`read_dir` plus a `.gitignore` chain compile for every root and every
expanded directory that wasn't cached, and a non-empty search box replaced
that with a breadth-first walk of up to 2000 directories — re-run on every
keystroke, because the search input notifies per `InputEvent::Change`. On
a cold cache or a large repo that is a visibly stalled frame.

Render now only reads caches. A miss becomes a queued load that runs on
the background executor and lands with a `cx.notify()`, so a
just-expanded directory fills in on the next frame rather than blocking
this one — the same trade every editor makes.

- `TreeLoader` owns the filesystem half, including its own `.gitignore`
  matchers (`Arc`, not `Rc`, so they can cross threads). It is seeded from
  the UI thread's compiled matchers and hands back the ones it compiled,
  so the cache still warms. The ignore semantics are unchanged: deepest
  match wins, `!` whitelists un-ignore, `.git` is always ignored.
- `Loads` tracks what is in flight and what a watcher event superseded
  while it flew, so a repaint can't spawn a second load for the same
  directory and a listing that predates a change can't install itself.
- The search is debounced 200ms, walks on the background executor, and
  carries a generation so a slow walk can't overwrite a newer query's
  answer. Previous hits stay on screen while the next walk flies rather
  than blanking per keystroke.
- `file_tree_refresh_roots` returns early when the root set is unchanged.
  It is called from render whenever the roots are empty, and it clears the
  caches and notifies — so a tab that can produce no roots spun the
  render loop.

Watcher events deliberately do *not* restart an open search. Doing so
starves it: the watcher's own debounce delivers a batch about as often as
the search debounce waits, so under sustained churn (a build writing into
`target/`, which the watcher reports because it knows nothing about
gitignore) every walk bowed out before reading a single directory and the
list stayed empty. A snapshot that is stale until the next keystroke is
the better failure.
2026-07-24 17:57:44 +08:00
l0ng-ai 9a4d818b78 refactor(editor): drop the LSP client entirely
Opening a `.rs` file in the code panel silently spawned rust-analyzer,
which then indexed the whole workspace — hundreds of megabytes of RAM and
a busy core — with no setting to turn it off. A terminal emulator should
not do that to its user on a click, and rather than add a flag to disable
something nobody asked for, the integration goes.

Removed: the JSON-RPC client and reader thread (`ui::lsp`), the per-server
registry, the completion / hover / definition providers installed on the
buffer, document sync (didOpen/didChange/didSave/didClose), diagnostics,
Go to Definition (F12), Find References (⇧F12) and its drawer, and the
status bar's server indicator. With them go the `lsp-types`, `ropey` and
`url` dependencies — all three were used only by this code (they remain in
the lock file as transitive deps of gpui-component and gpui, which is
expected).

Kept, and deliberately so:

- **Syntax highlighting**, which is tree-sitter, not LSP: gpui-component's
  `tree-sitter-languages` feature, `InputState::code_editor(language)` and
  `language_for_path` are all untouched. It is static, in-process, and
  costs nothing beyond parsing the open buffer.
- ⌘S save, dirty tracking, the external-change watcher and its conflict
  banner, markdown preview, soft wrap, and open-from-the-file-tree.

The module header now records *why* there is no language server, so the
next person to reach for one finds the reasoning instead of a gap.

Net −975 lines.
2026-07-24 17:56:02 +08:00
l0ng-ai 564f52e76f test(lsp): use a host-shaped absolute path in the URI round-trip
`Url::from_file_path` rejects a POSIX literal on Windows, so the test
panicked there instead of exercising the percent-encoding it is about.
2026-07-24 17:19:21 +08:00
l0ng-ai f6ba623af0 fix(procinfo): the Windows process table reads Proc::parent
`winproc::Proc` names the parent link `parent`, not `ppid` — the
Windows arm of `process_table` had never been compiled, so it took a CI
run to find it.
2026-07-24 17:12:57 +08:00