Commit Graph
348 Commits
Author SHA1 Message Date
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
jackie 8b2b4645c6 fix(macos): seed UTF-8 locale for GUI-launched shells 2026-07-25 18:09:11 +08:00
l0ng-ai 3c36632cbe fix(release): assemble the release as a draft once every platform is green
The four platform jobs each ran softprops/action-gh-release, so the first
one to finish published a release carrying only its own assets. That
release immediately became /releases/latest, which the in-app update check
polls — users were prompted to download a version whose assets were still
being built, and macOS users in particular could open the page minutes
before a .dmg existed. A permanently failed platform left the gap forever.

Build jobs now hand their bundles to a single draft-release job via
upload-artifact. It runs only after all four succeed, and assembles a
draft: drafts are invisible to /releases/latest, so nothing is advertised
until the release skill has verified the six assets, written the notes,
and published it by hand.

This mirrors the shape nightly.yml already used.
2026-07-25 16:33:37 +08:00
l0ng-ai 673bdc9200 Merge pull request #171 from l0ng-ai/feat/default-left-sidebar
feat(config): default the tab bar to the left sidebar
2026-07-25 16:26:47 +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 268bf7d7e5 chore(release): v26.7.3 v26.7.3 2026-07-25 15:38:52 +08:00
l0ng-ai 7b645f2dec Merge pull request #170 from l0ng-ai/fix/prompt-ctrl-chords
fix(prompt): stop swallowing ^J, and let ^R go to the shell on request
2026-07-25 15:29:56 +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 32eee7eecc Merge pull request #169 from l0ng-ai/feat/multi-window
feat(windows): one window per workspace
2026-07-25 15:12:16 +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 2860d4af6f Merge pull request #167 from l0ng-ai/fix/sftp-toggle-and-poll-lifetime
fix(ssh): make ToggleSftp toggle and the SFTP poll own its lifetime
2026-07-25 14:38:20 +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 f9c0e9a065 Merge pull request #165 from l0ng-ai/fix/settings-keeps-stock-glyphs
fix(settings): keep the stock glyphs on the Settings page
2026-07-25 11:47:43 +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