Commit Graph
130 Commits
Author SHA1 Message Date
l0ng-aiandClaude 24cf458e3b feat(mobile): a phone app to watch and drive tty7 panes (#983)
* feat(mobile): a gateway that lets a paired phone reach this machine over iroh

The first half of the mobile app: everything on the desktop side, plus the
client library the app will link.

- tty7-mobile-proto: the phone<->gateway wire protocol. One stream per purpose
  (pair, control, pane), framed like the daemon's frames, with raw terminal
  bytes kept out of JSON. No tty7-core, so it cross-compiles for iOS/Android.
- tty7-gateway: dials nothing, accepts phones over iroh (hole punching, relay
  fallback, end-to-end encrypted), checks the peer's key against the paired
  device list, and bridges to the daemon. Panes are observed, not attached,
  and keystrokes go in through SendInput, so a phone never resizes a pane or
  takes it away from the desktop window. `pair` prints a one-time QR code.
- tty7-mobile-client: the phone side (pair, tree, pane streams, direct/relay
  and RTT for the link), plus a `probe` example that stands in for a phone.

Verified against a real, isolated daemon: pair, tree, and typing into a pane
over a direct path, ~0.8 ms median echo on loopback.

* feat(mobile): the Tauri app — pair, browse the machine, drive a pane

The phone half, as a Tauri 2 app in mobile/ (its own cargo workspace, so the
desktop build and CI never compile a WebView stack).

- Rust side: the phone's iroh endpoint and key, paired machines, and every
  live stream, behind eight commands. Terminal output crosses to the WebView
  as raw ArrayBuffers on a Tauri channel, batched per frame, in order with the
  pane's JSON events.
- Frontend: vanilla TS + xterm.js. Paired machines and pairing; one machine's
  workspaces, tabs and panes with agents that need you pinned on top and the
  link's direct/relay path and RTT in the header; a terminal that fits the
  desktop pane's width, with an esc/tab/ctrl/arrows key bar. Coming back from
  the background re-watches and re-opens, relying on the daemon's replay.

Verified as a macOS build against a live gateway and an isolated daemon:
paired from the app, tree rendered on a direct path, keystrokes and key-bar
arrows reached the pane. iOS/Android builds need Xcode / the Android NDK,
neither of which is on this machine; mobile/README.md has the steps.

* fix(gateway): one serve per machine, and say how to start a missing server

- `serve` takes an exclusive lock on <config dir>/mobile/serve.lock. A second
  gateway on the same key is a second endpoint answering to one address, so a
  phone reached whichever the network picked. It is now refused, naming the
  pid that holds the lock.
- With no tty7 server running, phones and the terminal both hear "tty7 isn't
  running on <host> — open tty7 there, or run `tty7 server start`" instead of
  "lost the tty7 server: No such file or directory". `serve` checks once at
  startup, and still starts, since tty7 may be opened after it.

* feat(mobile): redesign the app as a native-feeling, minimal UI

The first cut read as a web page of bordered boxes. This rebuilds it the way a
phone app moves and reads, in the desktop tty7's own look:

- Screens push and pop with View Transitions; large titles fold into the bar.
- Grouped inset lists on a tinted canvas, following the system Light/Dark with
  the desktop presets, accent and ANSI palettes. Hack for code and terminal.
- Panes are listed by tab, one card per workspace, with the desktop's agent
  avatars and status badges (Waiting hollow, Working blinking) and its words.
- Pairing is its own screen, with steps, a Paste button and inline errors.
- A machine that stays silent for 10 s says so and offers to pair again; an
  offline notice says what to check.
- The terminal header shows live/offline in words. A pane too wide to read is
  shown at a readable size and pans to follow the cursor, with a toggle to fit
  the whole width. The key bar has drawn icons, puts left/right first, and
  has a keyboard toggle.

PRODUCT.md and DESIGN.md record the product facts and the design system.

* feat(mobile): open a new tab from the phone

Each workspace on a machine's screen gets a "New tab" action. It starts a
shell at the end of that workspace, in the directory its last tab is in, and
opens it straight away.

- Protocol: a one-shot `Open::NewTab { workspace_id, cwd, size }` stream,
  answered with `Ok` and a `TabCreated { tab_id, pane_id }`, or `Denied`.
  It is additive, so the protocol version stays. A gateway from before this
  drops the stream unanswered, and the app says to update it.
- Gateway: takes the same two steps as `tty7 tab new`, spawning a shell owned
  by the workspace and then TabCreate. The shell starts at the grid the phone
  asked for, because no desktop window is showing it yet. Sizes are clamped.
- App: a `tab_new` command, with the size worked out from the screen at the
  readable font size.
- probe: `newtab <workspace-id> [cwd]`.

* feat(mobile): reach the machines the desktop is linked to over SSH

A machine's screen now lists, under its own workspaces, every machine its
tty7 holds an SSH link to, with that machine's workspaces. Panes there open,
take input and get new tabs like local ones. "Needs you" gathers panes from
all of them.

- The gateway routes through the local server over links it already holds,
  the same way `tty7 -m <machine>` does. It never dials a down link, because
  that would guess at credentials the phone does not have. A down link
  shows as "Link down", with a note to reconnect it on the desktop.
- Protocol, additive: `Tree.remotes`, and an optional `machine` (the link
  key) on `Open::Pane` and `Open::NewTab`. A local pane is asked for exactly
  as before, so older gateways still understand it.
- Rebuilding a route target from a link key moves from the CLI into
  tty7-core as `RouteInfo::target` / `RouteInfo::host`, so the CLI and the
  gateway share one rule. The CLI now calls it.

* perf(gateway): read linked machines in parallel, never waiting on a slow one

Linked machines were read one after another on every tree poll. One slow
SSH link, whose requests can take 10 s, stalled the whole tree for every
phone, local workspaces included. It also held a lock that queued pane
opens, input and new tabs on every other link.

- Each linked machine is read on its own thread (`poller::Poller`). A poll
  waits at most 250 ms. A machine that has not answered is reported as it
  last was, and its read lands for the next poll. Only one read is in flight
  per machine, however many phones are watching. A machine that drops off and
  comes back cannot receive a stale read, because each slot has a generation.
- Routed control connections are locked per machine, not all together.
- A link that is up but has not answered its first read is sent as
  `RemoteView.pending` (additive). The app shows "Reading…" with skeleton
  rows instead of claiming the machine has no workspaces.

* fix(mobile): keep reaching the computer after the gateway restarts

Every `serve` bound a random port, so a restart left each phone holding
addresses that no longer answered. Where the n0 relays are unreachable, which
is common behind the Great Firewall, the phone had no other way to find the
gateway until it was paired again.

- `serve` listens on the same UDP port every time. It picks one on first run,
  keeps it in `<config dir>/mobile/port`, and moves only if the port is taken.
  IPv6 binding may fail, as in iroh's own defaults.
- Both ends add mDNS lookup (`iroh-mdns-address-lookup`, service `_tty7._udp`).
  On the same network a phone finds the gateway by key when its addresses are
  stale, such as after a new DHCP lease or a new IPv6 prefix. The gateway
  advertises and the phone only listens. If multicast is refused, each side
  starts without it and says so, rather than failing.
- iOS: `Info.ios.plist` declares the local-network use and the Bonjour
  service, without which iOS blocks multicast.
- An ignored test (`--test mdns`) connects by key alone over mDNS, for a
  machine that allows multicast.

* feat(mobile): run the gateway in the desktop daemon, paired from Settings

Phone access no longer needs `tty7-gateway serve` in a terminal.
Settings → Mobile switches it on, and the local daemon runs the gateway from
then on, with every window closed as well. That is where the panes a phone
reaches live anyway.

- Settings → Mobile, in all three locales:
  - an "Allow phone access" switch;
  - a status line (running, starting, off, or why it failed);
  - "Show code", which draws the QR code and the tty7pair: code with a
    Copy button, and closes on its own once a phone uses it;
  - the paired phones, each with Unpair.
  The section is in search, including by its config key `mobile_access`.
- The daemon (`tty7-app --daemon`) runs a supervisor (`core::mobile`). It
  watches `mobile_access` in config.json, re-reading only when the file
  changes, and starts or stops the gateway. A failed start is retried every
  30 s and logged once.
- tty7-gateway grows a `service` module: `start()` returns a stoppable
  handle, and `pair_code()` makes a code. The CLI's `serve` and `pair` are
  thin wrappers over them now. The gateway logs through `log`, so the
  daemon's output lands in its log file, and it reports itself in
  `mobile/status.json`. `State::serving()` checks the lock, which a crash
  cannot leave stale. A gateway that cannot take the lock leaves the
  status alone, because it belongs to the one holding the lock.
- iroh is linked into the GUI binary only. tty7-server stays the lean static
  binary pushed to remote machines.

* feat(mobile): serve phones whichever daemon is running

A daemon started by `tty7 server start` is the lean tty7-server, which
carries no gateway. With phone access on, the Settings status stayed on
"Starting…" and no phone could connect.

The GUI now checks, 5 s after it starts and whenever phone access is
switched on. If nothing is serving, it starts `tty7-app --mobile-gateway`,
detached the same way as the daemon (`spawn::detach_helper`). The helper
serves until the switch goes off, and exits at once if another process
already holds the gateway lock. When the daemon's own gateway is up, no
helper is started. The helper logs under its own role, "mobile".

* refactor(mobile): the daemon owns the gateway, as a child, whoever started it

There were two ways of running the gateway: a thread inside `tty7-app
--daemon`, and a detached helper the GUI started when the daemon could not.
That is now one way.

Every daemon, whether `tty7-app --daemon` or `tty7-server`, runs
`tty7_core::daemon::mobile::supervise` from `run_with`. While
`mobile_access` is on it keeps the gateway running as a child process, and
stops it when the switch goes off.

- Which program: `tty7-app` runs itself as `--mobile-gateway`. `tty7-server`
  runs a `tty7-gateway serve --exit-with-stdin` from beside it or on PATH,
  and links nothing new. Without one, it writes the reason into
  `mobile/status.json` for Settings to show, instead of "Starting…" forever.
- Lifetime: the child's stdin is a pipe from the daemon, and the gateway
  exits when it closes. A stopped, crashed or handed-off daemon (the pipe
  is close-on-exec) takes its gateway with it, and the next daemon starts
  one from its own binary. No gateway from an older build survives an
  update.
- Isolation: iroh no longer runs inside the process that holds every pane.
- `Status` moves to tty7-core, the one definition that the daemon, the
  gateway and the GUI all share.
- Gone: the GUI's helper check (`core::mobile` in the app) and the in-daemon
  gateway thread.

* fix(mobile): stop and reap the gateway before a daemon handoff

Found by running a real `tty7 server restart`. The old gateway did exit,
because the new image's supervisor started its own gateway and the old one
lost the lock. But it was left as a zombie: the image after the exec never
reaps a child it did not start, so each handoff leaked a process entry.

`hand_over` now calls `daemon::mobile::stop_for_handoff` before the exec,
which closes the gateway's stdin and waits for it. A flag keeps the
supervisor from starting another in the moments before the exec, and
`handoff_failed` clears the flag if the exec never happens, so the daemon
goes on serving.

Checked with two handoffs in a row (tty7-app → tty7-server → tty7-server):
- each old gateway was reaped, with no zombies system-wide;
- exactly one fresh gateway was running after each handoff;
- the pane's shell and its environment survived;
- the probe phone kept working.

* feat(mobile): the switch shows whether phones can reach you, not a status row

Settings → Mobile had an "Allow phone access" switch that showed intent and a
separate Status row that showed reality. That is one thing shown twice, and
the switch could sit on while nothing was running.

- The switch is the state. Switching on holds it at "Starting…", disabled,
  until a gateway is actually serving. If the daemon reports a failure, or
  nothing comes up within 15 s, the switch goes back off, `mobile_access`
  is reverted, and a notification says why.
- If the page opens on a failure the daemon is still retrying, the switch
  shows off with the reason in its description, and switching it on
  retries.
- The Status row is removed, with its four strings. There are two new
  strings for the failure, in en, zh and ja.

Also fixes the Settings window never showing notifications. It is a `Root`
like the workspace window but did not draw the notification layer, so any
toast pushed from Settings was queued and never shown. That included the
existing "Set as Default Terminal" result.

Checked in a dev instance. On a tty7-server daemon with no tty7-gateway:
Starting… first, then off, with "Phone access could not start: … no
tty7-gateway beside it or on PATH". On a tty7-app daemon: on, with Show
code enabled and no toast.

* feat(mobile): drop the "Needs you" section

The machine screen gathered every pane whose agent was waiting or done into
a "Needs you" section above the workspaces. Done lasts until the next prompt,
so the section grew with every finished agent and mostly held panes that
needed nothing.

Panes now appear once, in their own workspace. Each keeps its status badge
and words ("Needs input", "Working", "Done") and the agent's message.
PRODUCT.md and the design sidecar are updated to match.

* fix(mobile): say when typing doesn't reach the pane

The gateway's input thread gave up silently on a failed send_input, and
the app's input task did the same on a failed write, so the phone kept
showing a live pane while keystrokes went nowhere. Both now report the
failure as a pane error. Keys after it are dropped rather than ending
the stream, which the phone would read as the pane closing.

* feat(mobile): a compose box, paste and Shift+Tab on the terminal

Replying to an agent meant typing into xterm a character at a time,
without autocorrect, dictation or a usable IME. The compose box is a
real text field: Send types the text and then Enter, as its own write,
and several lines go in as one bracketed paste when the program asked
for it. Drafts survive leaving the pane and a send that failed. An
agent's pane opens on the box with the keyboard down.

The key bar gains Shift+Tab (Claude Code's mode switch) and a paste key.
A failed keystroke now takes the pane offline with a Reconnect banner
instead of being swallowed.

* feat(mobile): reconnect on its own, and select text to copy

A dropped pane or machine stream is retried with backoff (1s, 2s, 4s …
15s) and at once when the network comes back, rather than waiting for a
tap on Reconnect. The pane keeps its last screen up until the new
replay starts, and output or errors from a replaced stream are ignored.

Touch selection does not work in xterm, so a copy key lays the whole
buffer out as plain text over the pane, wrapped to the phone and joined
where the terminal wrapped, for the phone's own selection and Copy.

* feat(daemon): size leases, and Take Back on the desktop

An observer can now run a pane at its own size (ClientMsg::Lease, feature
size-lease). The pty and every observer go to that size. The controller
keeps its grid, its resizes are remembered rather than applied, and the
pane goes back to the last of them when the lease ends: the observer lets
go, its connection closes, or the controller takes it back.

A controller hears about leases only after asking (Watch), since an older
client cannot decode DaemonMsg::Lease. The desktop asks on every attach,
spawn and relink where the daemon advertises the feature, locally or
through the host hello for remote workspaces, and shows the pane as in
use on the phone with a Take Back button.

* feat(mobile): take a pane over at the phone's size

The terminal's phone button asks the gateway to run the pane at the
phone's grid (PaneRequest::TakeOver), which it turns into a size lease on
the observer connection, named after the paired device. The grid follows
the keyboard and rotation; leaving the pane, or the connection dropping,
gives it back. When the desktop takes it back the app says so and offers
to take it over again, never doing it on its own. A daemon too old for
leases is reported, and the pane keeps working.

* fix(mobile): link SystemConfiguration and install a rustls provider on iOS

The first iOS build failed to link: netdev and system-configuration, pulled
in by iroh, need SystemConfiguration.framework, which the generated Xcode
project does not list. bundle.iOS.frameworks adds it on `tauri ios init`.

Once linked, the app panicked at launch: iroh builds its reqwest client with
`rustls-no-provider`, so a process-wide crypto provider must be installed
before any client is built. Install ring's, which is already in the tree.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(mobile): keep the terminal's scrollbar on screen while panning

Panning a pane wider than the phone scrolled xterm's own box sideways with
the text. The box was only the view's width, so its vertical scrollbar
panned off with the columns and its scrollback stopped taking touches past
the first screen. The box now spans every column, and the bar is shifted
to the visible right edge as the view pans.

The bar is also drawn like the indicator WebKit shows for the pan, thin,
rounded and translucent, instead of VS Code's 14px square slider, so the
two axes match.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(mobile): pair by scanning the QR code, and Enter and quick-answer keys

Pairing took a pasted `tty7pair:` code, which on a real phone means getting
text off the desktop somehow. A Scan button next to Paste now reads the QR
code tty7 shows, through tauri-plugin-barcode-scanner, and pairs straight
away. The camera runs behind the WebView with our own viewfinder and Cancel,
since the plugin's full-screen view has no way out. The plugin is registered
on phones only, so the desktop dev build is unchanged.

The key bar gains Enter, so an agent's highlighted choice can be confirmed
without raising the keyboard, and 1 2 3 y n for numbered choices and y/n
prompts.

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(mobile): sign for the App Store and declare exempt encryption

Sets the development team so `tauri ios init` writes it into the Xcode
project, and marks the app's encryption (standard TLS/QUIC only) as exempt
so TestFlight uploads skip the export-compliance question.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(mobile): count the iOS safe areas once

The WKWebView's scroll view inset its content by the safe areas, and the
page, laid out with viewport-fit=cover, padded by env(safe-area-inset-*)
as well. The viewport came out 778pt tall on an 874pt screen: everything
sat a status bar's height too low, with a blank band under it. The scroll
view's automatic inset adjustment is now off, so the page runs edge to
edge and its CSS alone keeps clear of the status bar and home indicator.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(mobile): the Terminal Mobile redesign

The app takes the desktop's neutral greys, light and dark, with ink rather
than a system blue for what is pressed or chosen.

- Machines: pairing's "+" moves to a floating bar at the bottom beside a
  search field. Each machine shows its link, round trip and tab count as
  last seen.
- A machine: tabs grouped by workspace with a count; round agent glyphs;
  a dot on the right for running or waiting on you. The per-group New tab
  buttons give way to one "+" in the same floating bar, beside tab search.
- New tab: a sheet to pick Claude Code, Codex or a shell, and the
  workspace. An agent's command is typed into the new tab once it is live.
- A pane: the bar keeps only back, the title with its state, and a menu
  for selecting text, the fit and the phone's size. Under it, one row of
  equal keys, a page at a time, and a message box that is always there;
  its round button sends, or when empty hands the keyboard to the
  terminal.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(mobile): the tty7 mark as the iOS app icon

The phone showed Tauri's default icon: `tauri ios init` filled the Xcode
project with it. The committed icons/ios set was drawn on the macOS grid,
a rounded tile inset on transparency, which iOS would shrink inside its own
mask with a pale border. icons/app-icon-ios.svg is the same Duo mark full
bleed, rendered without alpha as the App Store requires.

Claude-Session: https://claude.ai/code/session_01HVosmYyVSH3BrsLYx3tcSS

* fix(mobile): typing, scrolling and pairing on a real phone

- Typing into a pane: an input method's text never reached it, since iOS
  never commits a candidate into xterm's hidden textarea. Tapping the
  terminal now focuses a plain field of our own, whose committed text goes
  to the pane as it is committed. Backspace on the empty field, Enter, Tab
  and the arrows go as the terminal's keys.
- xterm sends nothing itself any more (disableStdin). That also stops the
  phone answering a program's colour and device queries: the desktop
  answers those, and the phone's late second answer landed in the shell as
  typed text.
- Scrolling the scrollback: xterm 6 has no working touch scrolling. A
  mostly vertical swipe now scrolls the buffer a row at a time and coasts;
  a sideways one is left to the native pan.
- Pairing: the steps name the desktop's Settings -> Mobile, Allow phone
  access and Show code, not a command-line gateway, and so do the notices
  when a machine cannot be reached.

Claude-Session: https://claude.ai/code/session_01HVosmYyVSH3BrsLYx3tcSS

* fix(mobile): room for the keyboard, and smoother vertical scrolling

- The keyboard: the WebView runs edge to edge and is not resized for it,
  so it covered the dock and the pane's last lines. The app now takes the
  size of the visual viewport, drops the home indicator's gap while the
  keyboard is up, and keeps the cursor's line in sight.
- Scrolling: the terminal draws with xterm's WebGL renderer, which a
  scroll does not make lay every row out again. A swipe is applied once a
  frame, and moves the view by pixels: xterm scrolls whole rows, and the
  rest of a row is a GPU shift of the drawn screen, put on together with
  the rows it goes with.

Claude-Session: https://claude.ai/code/session_01HVosmYyVSH3BrsLYx3tcSS

* feat(mobile): settings, message history, and a tighter home screen

- Settings, from beside the Machines title: appearance (automatic, light,
  dark; the status bar and keyboard follow through the window's interface
  style), terminal text size, how a pane wider than the phone first shows,
  clearing the message history, and the version.
- The message box keeps what it sends on the phone. As a message is
  written, past ones that match take the key row's place; with the box
  empty, a History button opens them all, searchable. A line that asks for
  a password is sent but not kept.
- ^R on the third key page, for the shell's own history search.
- A top-level screen's bar floats over the list, clear until the title
  scrolls under it, so the large title sits just under the status bar.

Claude-Session: https://claude.ai/code/session_01HVosmYyVSH3BrsLYx3tcSS

* fix(mobile): a fitted pane fills the view, and its scrollbar drags

- A pane shorter than the view (a wide one, fitted) no longer leaves the
  bottom of the screen blank: the terminal here runs as many rows as fill
  the view, the extra ones holding the pane's earlier lines, and what is
  left over goes above so the prompt stays next to the keys. The pane on
  the desktop keeps its size.
- A drag that starts on the scrollbar is left to xterm. The swipe handler
  took it too, the other way round, and the two cancelled out.

Claude-Session: https://claude.ai/code/session_01HVosmYyVSH3BrsLYx3tcSS

* fix(mobile): errors say what to do in the app, not on the command line

The messages a phone shows, and the two Settings → Mobile can, named the
gateway process, the CLI's `tty7 server start`, the transport and a lock
file's path. They now speak of tty7 on the computer and of pairing: open
it there, update it, pair again, quit the other copy.

Claude-Session: https://claude.ai/code/session_01HVosmYyVSH3BrsLYx3tcSS

* style: rustfmt the gateway and mobile client

Claude-Session: https://claude.ai/code/session_01HVosmYyVSH3BrsLYx3tcSS

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-29 19:09:32 +08:00
l0ng-ai 12ec0c92a9 fix(sidebar): stop ungrouping tabs when a git probe fails (#1011)
A tab's auto group comes from probing its cwd for a repository. Any
failure of that probe - git failing to spawn, the macOS /usr/bin/git shim
dying while Xcode is mid-switch, a remote link dropping - came back as
"not a repository", overwrote the tab's remembered group, and was never
re-checked while the pane sat idle. Tabs in perfectly good repositories
dropped into Ungrouped and stayed there.

- Read root, home and branch in-process on this machine with
  gix-discover instead of three git processes. Only the line counts
  still come from `git diff --numstat`, so they keep matching the diff
  views; a detached HEAD is still named by git for the same reason.
- Remote hosts keep asking their own git (a new control request would
  force a dialect bump), but the probe now tells "not a repository"
  (exit 128, or the directory is gone) apart from a failure.
- A failed probe leaves the cache as it was, so a tab keeps its
  remembered group, and an unanswered cwd is retried every 10s.
2026-09-29 16:55:03 +08:00
l0ng-ai 241b91d70b chore(release): v26.9.4 2026-09-29 12:58:21 +08:00
l0ng-ai 4e97498e2c fix(editor): ⌘R / ⌘⌥F no longer crash the app
Bumps gpui-component to d4ad0c10: opening the replace row from the
shortcut read the editor back while it was being updated, and panicked.
2026-09-29 10:35:51 +08:00
l0ng-ai c5cdf5382b feat(editor): redesigned find bar with a replace shortcut
Bumps gpui-component to e9dd0538:
- The find bar is one filled pill (search glyph, field, case, replace,
  "3 of 6" count, up/down, close) with no border or rule, and it no
  longer stacks its own padding on the editor's.
- The current match is amber, the other hits a neutral wash, so Enter
  visibly moves between them in any theme.
- ⌘⌥F / ⌘R (Ctrl+H elsewhere) opens find with the replace row.

The breadcrumb row drops to 22px and sets the symbol in the code font.
2026-09-29 10:00:08 +08:00
l0ng-ai 7f66f69674 fix(ssh): pin russh to our fork with the zlib truncation fixes (#997) (#1006)
A host with `Compression yes` (every one imported from an ssh config that
sets it) negotiated zlib@openssh.com, and the pinned ayamir/russh rev cut
every packet that inflated past 2x its compressed size. The zlib stream
desynced right after auth: the session showed as connected and never
printed a byte.

- Move the [patch.crates-io] pin to l0ng-ai/russh (branch tty7): ayamir's
  gssapi-with-mic commit plus Eugeny/russh 58886f4d and 24e2c374.
- Add an end-to-end test that runs a zlib session against an in-process
  russh server and checks every byte of a compressible + incompressible
  payload arrives (0 of 270336 bytes on the old pin).
- Re-importing an ssh config now updates `algorithms` on hosts that already
  exist, so dropping `Compression yes` and re-importing takes effect.
2026-09-28 23:07:49 +08:00
l0ng-ai 7016fdb7ed feat(editor): IDE-level code editor — multi-cursor, LSP, git gutter, symbols, split (#1002)
* fix(keymap): let the code editor's multi-cursor chords beat the pane keys

gpui-component now binds secondary-d, secondary-shift-l and
secondary-alt-up/down in the Input context for multiple cursors. A
context-free binding ranks as deep as the focused context and ties go to
the one added last, so tty7's SplitRight and FocusPaneUp/Down took those
keys inside the editor. Re-add the editor bindings after tty7's table; other
text fields have no handler for them and fall through to the pane keys.

* feat(editor): show the selection count in the status bar

With several cursors the Ln/Col readout (the primary caret's) is followed
by "(N selections)".

* feat(editor): git change markers in the gutter

Diff each buffer against its file's index version (what VS Code's quick
diff and the SCM panel's Changes compare with) and hand the hunks to the
editor as gutter markers: added, modified, and a wedge where lines were
deleted. The base is read with `git show :./name` through the buffer's own
host, so remote workspaces work too; SFTP buffers and untracked files get
no markers. It is re-read when the repository's SCM epoch moves, on save
and when the path changes; the diff itself is a line-level Myers diff run
off the UI thread shortly after each edit.

Next/previous change and Revert Change (one undo step, also in the
right-click menu) are commands; clicking a marker opens a peek of the
staged lines with a Revert button. The editor watch now also follows the
repository the gutter found, so a stage or checkout refreshes the markers.
The `editor_git_gutter` config (default on) is flipped by the
ToggleEditorGitGutter command.

* feat(editor): go to symbol, breadcrumbs, and back/forward navigation

Go to Symbol (Cmd-Shift-O in the editor) lists the file's outline on a new
Symbols tab of Search Everywhere: indented by nesting while browsing, ranked
flat with the containing symbols as a subtitle while searching. Arrowing
through the rows previews each symbol; Escape puts the caret and the scroll
back, Return keeps it.

The outline comes from the tree the highlighter already parsed, with a small
query per language (Rust, Go, Python, JavaScript, TypeScript/TSX, C, C++,
Java, Markdown, Ruby, shell). Tty7App::editor_set_document_symbols lets a
language server's documentSymbol answer replace it per buffer.

A breadcrumb row over the text shows the file's path from its project root
and the symbols around the caret; the symbols open Go to Symbol.

Back and forward (Ctrl-- / Ctrl-Shift-- on macOS, Alt-Left/Right elsewhere)
walk a per-tab history. Jumps are noticed by sampling the caret whenever the
editor draws: a change of file, or a move of ten lines or more without an
edit, records where the caret was. Quick open, go to line, file links and
anything that goes through open_file_in_editor_at are therefore captured
without hooks of their own; a place in a closed file reopens it.

* feat(editor): bind next/previous change to Alt+F5 in the code editor

Bind EditorNextChange / EditorPrevChange to alt-f5 / shift-alt-f5 in the
Input context only, after tty7's own table, so a terminal never loses the
function key. Mark the hunks stale right after a revert, before the
input's Change event lands, and cover the whole loop (markers, stepping,
revert as one undo) with a window test.

* feat(editor): line commands in the editor's right-click menu

Toggle Comment, Move Line Up/Down, Duplicate Line, Delete Line and Go to
Matching Bracket, dispatched to gpui-component's new editing actions so
each row shows its chord. Strings in en, zh and ja.

* feat(editor): language servers for the code editor

A new ui::lsp module runs language servers for local files the editor
opens: rust-analyzer, typescript-language-server, pyright (or pylsp),
gopls and clangd, from a registry table. Each server is keyed by
(server, project root), found from markers such as a Cargo workspace,
package.json, go.work/go.mod or pyproject.toml, and shared by every buffer
under that root. A server missing from PATH (which tty7 already fills from
the login shell) means no LSP for that language and a one-line hint in the
status bar.

The JSON-RPC client frames Content-Length messages over the server's stdio
on dedicated threads, holds everything back until initialize is answered,
cancels requests nobody waits for any more, and answers the server's own
requests (configuration, capability registration, applyEdit). Documents
use full-text sync, debounced, and flushed before every request. A server
whose last file closed shuts down after a grace period, every server exits
with the app, and one that crashes is restarted a few times before it is
left down.

Features: diagnostics as underlines with hover messages and an error and
warning count in the status bar; completion (snippets flattened to plain
text); hover; go to definition by secondary-click or F12, across files
through open_file_in_editor_at; code actions on the editor's own menu
(cmd-.), resolved and executed as the server needs; Format Document
(shift-alt-f); and Rename Symbol (F2) in the editor's bar, applied to open
buffers and to files on disk. The key bindings sit in the Input context so
F2 and F12 stay with terminal programs.

Positions are converted at the boundary: servers count UTF-16 units,
gpui-component counts chars, and the rope counts bytes.

The new editor_lsp setting (default on) turns it all off.

* fix(editor): clear the change markers when the file loses its base

A file that becomes untracked or leaves its repository kept the markers
of its last diff; clear them when the base goes away. Guard the base read
against a panic, which would otherwise leave the fetch flagged forever,
and test the read against a real repository: the base is the staged
version, and untracked files have none.

* test(keymap): the editor's navigation chords win inside the editor, not in a terminal

* fix(keymap): let the code editor's line commands beat the app's chords

The editor binds toggle comment, move/copy/delete line, insert line, select
line and go-to-bracket on its CodeEditor key context. A context-free app
binding ranks as deep as the focused context and wins the tie by being
added later, so ⌘/ (shortcut sheet), ⌘↵ (fullscreen), ⌘⇧↵ (maximize) and,
off macOS, ⌥↑/⌥↓ (pane focus) took those keys inside the editor. Re-add
them in fixed_bindings on CodeEditor, which only a multi-line code editor
declares, so plain text fields and the terminal keep the app's keys.

* fix(keymap): route cmd-K cmd-D to the code editor's skip-occurrence

The chord starts with ClearScrollback's key on macOS, and gpui drops a
pending chord that ranks below a complete match, so the fork's binding
never got its second key. Re-add it after tty7's table, in the CodeEditor
context only, so other text fields don't wait on cmd-K.

* feat(editor): history follows edits, and paging is not a jump

Places in the back/forward history now move with lines inserted or deleted
above them, read from the editor's line-edit log on every change. The caret
move a Page Up/Down, paste, undo or redo makes is heard through the
keystroke that caused it and is not recorded as a jump.

* feat(editor): Problems list, keyboard change peek, Editor settings

- Problems: every error, warning and note the language servers published
  for the open files, grouped by file, at the foot of the code panel.
  The status bar's counts and ToggleEditorProblems (Cmd/Ctrl+Shift+M)
  open it; a row opens its file at the line and column. Read through a
  new LspStore::diagnostics_snapshot, mapped to editor columns.
- EditorPeekChange (Alt+F3 in the editor) peeks the change under the
  caret, or goes to the next one. The peek now takes the keyboard from
  a click too: Enter reverts, Escape closes and hands focus back.
- Settings > General gains an Editor group: git change markers,
  language servers, soft wrap and rendered Markdown, searchable and
  resettable like every other row.

* feat(lsp): outline, references, workspace symbols, signature help

- documentSymbol feeds the editor's outline (breadcrumbs, Go to Symbol)
  through editor_set_document_symbols, refreshed 500 ms after typing
  pauses. Flat answers are nested by range; an empty answer from a server
  still indexing leaves the tree's outline in place.
- Find All References (shift-F12) and a definition with several answers
  open a Locations tab in the search. Arrowing through it previews a place
  in the file in front, and Return goes there, into any file.
- Go to Symbol in Workspace (secondary-T inside the editor) asks the
  server's workspace/symbol as the query is typed, into the same list.
- Completion items are resolved for their documentation and auto-import
  edits, which are applied on accept (with the gpui-component fork).
- Signature help opens on the server's trigger characters (or on '(' and
  ','), stays current while typing in the call, and closes when the server
  says the cursor has left it or on Escape.
- Diagnostics are replaced wholesale on publish; between publishes the
  fork now carries them through edits instead of dropping them.
- workspace/configuration answers from per-server settings, with an empty
  object for a section tty7 sets nothing for. rust-analyzer gets
  checkOnSave with cargo check, also as initializationOptions, and every
  server gets didChangeConfiguration after initialized.

* feat(editor): text commands in the palette and keymap

Transform to Upper/Lower/Title Case, Trim Trailing Whitespace, Join Lines
and Remove Surrounding Brackets, as tty7 actions (bindable on the
Keybindings page, unbound by default) and as palette rows offered while a
buffer is open. Both run gpui-component's editing action on the active
buffer. Join Lines gets VS Code's ctrl-j as a fixed CodeEditor binding on
macOS, where the terminal keeps it as a line feed. Strings in en, zh, ja.

* feat(editor): split the editor into two groups

Cmd-\ (Ctrl-\ off macOS, bound only inside the editor so the terminal keeps
SIGQUIT) opens the file in front in a second group on the right. Each group
has its own file in front and, for different files, its own caret and
scroll; Cmd-Alt-Left/Right (Ctrl-Alt off macOS) or a click moves the focus
between them, and a group whose last file closes goes away. The split is
saved with the tab's other editor state.

The focused group is always TabCode's own files/active and the other waits
beside it, so everything that acts on the file in front - saving, go to
line, language servers, change markers, multiple cursors, history - follows
the focus unchanged. A file shown in both groups is drawn once, in the
focused one; the other shows a placeholder that brings the focus over. A
second InputState kept in sync would have needed every hook attached twice.

Also: palette rows and View menu items for Go to Symbol, Back, Forward and
Split; Cmd-Shift-O closes Go to Symbol when it is open; the status bar no
longer repeats the path the breadcrumbs show (a rendered Markdown file keeps
its breadcrumb path); change markers are kept current in both groups.

* test(keymap): the editor group chords win inside the editor

* fix(lsp): close a window's documents when the window closes

Documents were only closed by sync_window, which runs when a window's
buffer set changes. Closing a window never ran it, so no didClose was
sent, the idle shutdown never started, and a language server lived on
until quit (forever, with the app retired to the tray).

* fix(editor): restore a split whose left group had no recorded files

The recorder writes files: [] with a split when the left group held only
untitled or remote buffers, but restore returned early on an empty left
list and dropped the right group too.

* fix(editor): forget a swept orphan buffer's navigation state

The orphan sweep dropped clean buffers without calling forget_buffer, so
each one's outline cache (a full copy of its text), LSP symbols and
edit-log cursor stayed in EditorNav for the life of the window.

* fix(lsp): owner-only sync, stale-edit checks, request timeouts, re-enable

- Only the buffer that owns a document may use its server. The same file
  open in another window used to send its own text as didChange on F12,
  rename, references or signature help, swapping the document under the
  owner. LspStore::context now takes the requester and refuses a
  non-owner quietly, before anything is sent.
- apply_workspace_edit checks every open buffer an edit touches against
  a baseline: the texts when a rename was asked for or the code-action
  menu was filled, or the text the server last heard for its own
  workspace/applyEdit (which then answers applied: false). A buffer typed
  in, opened or closed since means nothing is applied.
- A request made after the server's output closed fails at once instead
  of waiting forever. Requests time out after 10 s (initialize after 60 s,
  shutdown after 2 s) and cancel themselves on the server.
- Turning editor_lsp back on asks every window for its open files again,
  which starts their servers. Windows register how to be asked; closed
  ones are forgotten.

* test(nav): spell out LineEdit's new at_line_start field

gpui-component's LineEdit now records an insertion at column 0, which
moves the whole line down. The literal edits in this test are mid-line.

* fix(editor): a restore merges into the tab, and commands follow the group focus

Session restore used to assign the recorded groups over whatever the tab
held when its files finished loading, so a file opened or a split made in
the meantime was lost. TabCode::restore_groups keeps a split made meanwhile
as it is, and otherwise lays out the recorded groups and puts the files
opened meanwhile back into the focused group, the last of them in front. A
restore the reader has moved on from no longer takes the keyboard, and never
hides a panel they opened.

The group focus only followed the keyboard when the editor drew. Keys are
safe - gpui draws a window whose focus moved before dispatching the next
key - but a command from the menu bar or a context menu is dispatched
without a draw, and acted on the group that had the focus before. A
capture-phase listener for every editor command on the editor's element now
settles the group first, wherever the command's handler sits.

* fix(editor): leave cmd-T to New Tab; offer workspace symbols from the palette

Inside the code editor cmd-T was rebound to Go to Symbol in Workspace,
which made tty7's most-used chord mean two things depending on focus.
It is New Tab everywhere again. Workspace symbols stay reachable as an
Editor palette row and as a rebindable, unbound action.

* build: pin gpui-component to the fork's editor work (0e7541fb)

* fix(search): let the editor's pickers stand alone in Search Everywhere

Go to Symbol, a language server's places and Go to Symbol in Workspace
opened on hidden tabs, so the window-wide scope row (All, Terminals,
Sessions, Hosts, Commands) sat over them with nothing selected, and Tab
swapped the list for the terminals. They now show a heading in place of
the row (References / Definitions / Symbols / Workspace Symbols, with a
count), Tab stays put, and the footer drops the scope hint.

* fix(search): Go to File stands alone like the editor's pickers

Go to File (cmd-O) is reached only by its chord, yet it sat under the
scope row with nothing lit and offered Tab to leave for the terminals.
Every tab outside the row now stands alone the same way: its name in
place of the row, Tab stays put, no scope hint in the footer.

* feat(search): give the editor its own scope row — Files, Symbols, Workspace Symbols

Go to File, Go to Symbol and Go to Symbol in Workspace were three
separate pickers, each on its own chord. They now share a second scope
row, the editor's, next to the window's: cmd-O and cmd-shift-O open on
it and Tab walks it. Only tabs that can answer show: Symbols needs a
file in front, Workspace Symbols a language server that searches the
project. A row of one shows as a heading. Each tab is opened the way its
chord opens it, so it arrives set up; references and definitions still
stand alone.

* feat(search): fold Workspace Symbols into Symbols

Symbols and Workspace Symbols answered the same question at two scopes.
Symbols now does both: with nothing typed it is the file's outline; once
a query is typed, the front file's language server is asked across the
project and its answers are listed after the file's own, each under a
heading when both have rows (the front file's own hits are dropped from
the project's). The separate tab, action and palette row are gone.

* fix(search): call the language server's project results Project, not Workspace

LSP's workspace/symbol searches the server's project root, which has
nothing to do with a tty7 workspace (a group of tabs). The Symbols tab's
second section said Workspace, reading as if it searched those. It says
Project now, and the tty7-facing names follow (project_symbols,
set_project_symbols, lsp_project_symbol_query); only code that speaks
the protocol keeps its word.

* ci(host-boundary): allow the language servers' local reads

ui::lsp only ever holds local buffers (OpenFile::local refuses any other
host) and runs its servers on this machine, so the files it reads to
measure a column, apply a rename to disk or find a project root are on
this disk. Each call is allowlisted with that reason.

* test(editor): spell test paths so they hold on Windows

The language-server tests used /p/... paths, which are not absolute on
Windows and so have no file:// URI; they now build platform paths
(lsp::test_path) or spell the URI out. The gutter and split tests
canonicalized their temp dirs to get past macOS's /private symlink,
which on Windows yields the \\?\ form no editor path is ever in; they
share a helper that canonicalizes everywhere but Windows.

* test(editor): resolve temp dirs the way the editor does

On the Windows runner the temp dir is an 8.3 short name (RUNNER~1),
which the editor's load expands through Host::canonicalize. The split
tests now resolve their fixtures through that same call instead of
guessing per platform.
2026-09-28 20:47:07 +08:00
dependabot[bot] 2938a41a1c deps: bump the cargo-minor-patch group with 6 updates (#998)
Bumps the cargo-minor-patch group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [encoding_rs](https://github.com/hsivonen/encoding_rs) | `0.8.35` | `0.8.42` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.26.0` | `1.26.1` |
| [plist](https://github.com/ebarnard/rust-plist) | `1.10.0` | `1.10.1` |
| [clap](https://github.com/clap-rs/clap) | `4.6.6` | `4.6.7` |
| [toml_edit](https://github.com/toml-rs/toml) | `0.25.13+spec-1.1.0` | `0.25.15+spec-1.1.0` |
| [ureq](https://github.com/algesten/ureq) | `3.4.0` | `3.4.2` |


Updates `encoding_rs` from 0.8.35 to 0.8.42
- [Commits](https://github.com/hsivonen/encoding_rs/compare/v0.8.35...v0.8.42)

Updates `uuid` from 1.26.0 to 1.26.1
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.26.0...v1.26.1)

Updates `plist` from 1.10.0 to 1.10.1
- [Release notes](https://github.com/ebarnard/rust-plist/releases)
- [Changelog](https://github.com/ebarnard/rust-plist/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ebarnard/rust-plist/compare/v1.10.0...v1.10.1)

Updates `clap` from 4.6.6 to 4.6.7
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/main/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.6...clap_complete-v4.6.7)

Updates `toml_edit` from 0.25.13+spec-1.1.0 to 0.25.15+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/v0.25.13...v0.25.15)

Updates `ureq` from 3.4.0 to 3.4.2
- [Changelog](https://github.com/algesten/ureq/blob/main/CHANGELOG.md)
- [Commits](https://github.com/algesten/ureq/compare/3.4.0...3.4.2)

---
updated-dependencies:
- dependency-name: encoding_rs
  dependency-version: 0.8.42
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-patch
- dependency-name: uuid
  dependency-version: 1.26.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-patch
- dependency-name: plist
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-patch
- dependency-name: clap
  dependency-version: 4.6.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-patch
- dependency-name: toml_edit
  dependency-version: 0.25.15+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-patch
- dependency-name: ureq
  dependency-version: 3.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 20:46:43 +08:00
l0ng-ai ee817bae01 feat(ui): v5 modal cards, and an IME crash fix in the code editor (#993)
* fix(deps): gpui-component keeps highlight boundaries on char boundaries

Typing Chinese through the IME in the code editor could panic the app:
after a sync parse ran past its 2ms budget, the stale injection layers
put a style boundary inside the committed glyph and the text system split
the line mid-character. Debug builds miss the budget often enough to hit
it; the fork now snaps every style range to the current text.

* feat(ui): bring the modal cards onto the v5 design

- Confirmations take v5's alert shape: title and detail as one
  paragraph, answers beneath, no header row or footer rule, 360px wide
  and set lower, at eye height.
- Dialog buttons take the field's 7px corner and 14px padding; a
  secondary answer rests on the well's faint fill instead of bare text.
- The scrim dims rather than blacks out: 45% dark, 14% light.
- The New Workspace form joins the other cards: 12px corner, borderless
  wells for its fields, an esc cap and the shared 40px footer.
2026-09-28 13:35:53 +08:00
l0ng-ai f647a19746 feat(editor): draw the editor's right-click menus like the rest of the window (#992)
* feat(editor): draw the editor's right-click menus like the rest of the window

The text's menu was gpui-component's native OS menu: another font and
material, no shortcuts, and Go to Definition / Show Code Actions that no
language server ever enables. It is now a PopupMenu with Attach to Agent
(the selected lines ride along as #L3-9), Undo/Redo, the clipboard,
Find, Go to Line, and the file's own items: Open in Browser for web
files or Open with Default App, Reveal, Copy Path, Copy Relative Path.

The file tabs get a menu too: Close, Close Others, Close to the Right,
and the same file items. Closing several asks once about unsaved edits.

* chore(deps): gpui-component moves the caret on right-click under a host menu
2026-09-28 13:35:15 +08:00
l0ng-ai bc08fc49f4 fix(deps): gpui-component paints text selection under the glyphs (#991) 2026-09-28 11:41:11 +08:00
l0ng-ai 23e4ea79f5 chore(deps): gpui-component with v5 menus
Picks up l0ng-ai/gpui-component bce0d8ec: popup and context menus take
the v5 popover look — a neutral hover step, 28px rows, a hairline ring
and v5's long shadow.

Claude-Session: https://claude.ai/code/session_01Aeskbok7Ah7dvKWnaaJFEg
2026-09-28 08:45:02 +08:00
l0ng-ai bf5149bea0 fix(editor): stop losing edits, share buffers, add file strip, quick open and go to line (#984)
* fix(host): save local files atomically via a temp file and rename

LocalHost::write_file truncated the target in place, so a crash, a full
disk or a killed process mid-save destroyed the user's file. It now
writes a hidden sibling temp file, syncs it, keeps the old file's mode
and renames it over the target, removing the temp file on any error.

It still writes in place where a rename would change something visible:
a non-regular target (symlink, directory, FIFO), a read-only file, and on
Unix a hard-linked file or one owned by another user, or when the temp
file cannot be created (e.g. a read-only directory).

* feat(editor): add editor_text for encodings, line endings, indentation and EditorConfig

A pure module the code editor will use when loading and saving files:
decode detects BOMs, binary files, UTF-8, GB18030 and a lossless
Windows-1252 fallback and normalises CRLF; encode restores the exact
bytes and names the first unrepresentable character; detect_indent
infers tabs or a 2/4/8 space width with language defaults; and
editorconfig_for resolves .editorconfig sections with save-time rules.

* feat(editor): share buffers across tabs, guard unsaved work, add a file strip

- One buffer per file per window; tabs list which buffers they show. The
  same file open in two tabs is no longer two diverging copies.
- Closing a tab, its last pane, the window, or quitting asks about unsaved
  files (Save / Cancel / Discard) instead of dropping them. Bulk closes skip
  tabs with unsaved files; a tab that vanishes any other way hands its
  unsaved buffers to the tab in front.
- File tree rename/delete now retarget or flag the open buffer, so a save
  no longer recreates the old path.
- Saves check the file's mtime first and ask before overwriting a change
  made elsewhere; this is the only detection SFTP buffers get.
- Dirty is a comparison with the saved text, so undoing back clears it.
- Reloads replace only the changed span as an ordinary edit, keeping undo.
- Load/save go through editor_text: encoding, BOM and CRLF round-trip,
  indentation is detected, .editorconfig is honoured.
- Header shows a strip of open files; New File, Save As (native panel
  locally, a path bar remotely), Go to Line (Ctrl+G), and the status bar
  shows indentation, encoding and a clickable line ending.
- Open files are remembered per tab across restarts.

* feat(search): quick open a file by name from a Files tab

Search Everywhere gains a Files tab that finds any file in the active
tab's project by fuzzy name and opens it in the built-in editor, with
`name:line[:col]` jumping to that spot. The list comes from one walk of
the project through the host (Host::search with an empty query), so it
works the same on local, SSH and WSL workspaces and skips what the tree
hides: dotfiles, .git and gitignored paths. The walk is capped at 50k
entries / 20k directories, kept between openings and revalidated in the
background each time the search opens.

Files join the All tab once a query finds them. Go to File... is bound to
Cmd+O on macOS (Cmd+P is already Search Everywhere) and ships unbound
elsewhere, where every obvious chord is taken or owed to the shell.

* chore(editor): allowlist the editor session file, tidy lints

* fix(editor): keep restored file order, drop stale close waits, carry files through tab merges

- Background arrivals (restore, merge, rescue) append to the strip in order
  instead of inserting beside the active file, which reversed them.
- A cancelled Save As, a dismissed path bar, or a dropped buffer cancels any
  close that was waiting on that save.
- Merging a tab into another carries its open files along.
- A shell exiting closes its tab without a prompt it could not honour;
  unsaved buffers move to the tab in front.
- Tabs rebuilt under the same id (server restart) restore their files.

* fix(host): only fall back to an in-place write when the rename is refused

On Windows every failure of the atomic save fell back to fs::write,
including a failure while staging the temp file. A full disk would then
truncate the original in place, the very loss the temp file prevents.
Staging errors now return as-is; only a refused rename (a file held open
elsewhere) takes the in-place path.
2026-09-28 00:32:18 +08:00
l0ng-ai fd2c4f7d4e feat(panel): Search and GitHub tabs in the right panel (#978)
* feat(panel): add Search and GitHub tabs to the right panel

The right panel grows from three tabs to five. Five word labels do not
fit the panel's 280px resting width, so the tab row now draws a glyph
per tab and names it in a tooltip.

Both new panes are placeholders here; the content search and the
GitHub issues/PR browser land on top of this.

* feat(panel): find in files in the right panel's Search tab

The Search tab replaces its placeholder with a content search over the
active tab's project -- the same roots the Files tab shows -- on the host
that project lives on. Hits arrive as you type (debounced, with a
generation counter so a stale answer never lands), grouped by file with a
count, each line excerpted with its matches highlighted. Clicking a hit
opens the built-in editor at that line and column; Enter searches again.
Match-case, whole-word and regex toggles sit at the end of the field, and
the tab focuses its field whenever it is brought forward.

Host::search_content is new on the Host trait, implemented once in
host::content_search (ignore walk + regex) and run by LocalHost directly
and by tty7-server over a new SearchContent control request. The walk
honours .gitignore with or without a repository, skips dot-entries, binary
files and files over 1 MB, and reports a capped search as truncated. The
request is gated on a new `content-search` hello feature, so a server that
predates it is never sent it; the panel says the server needs updating
instead of showing no results. Conformance cases cover local and the
stdio server alike.

* feat(panel): browse GitHub issues and pull requests in the right panel

The GitHub tab follows the focused pane's repository: its root is resolved
the way the Source Control tab does, its remotes are read through the Host
(the tree may be on another machine), and the github.com remote is bound,
upstream over origin in a fork, with a menu to pick another.

The list switches between issues and pull requests, open and closed, 50 rows
a page with Load more; rows carry a state glyph distinct by shape, labels
(click one to filter by it) and relative times. A row opens the detail in
place: title, state, author, labels, description and comments as Markdown,
and for a pull request its branches, size and changed files. A file opens in
the diff overlay through a new supplied-patch DiffSource, so GitHub's patch
renders exactly like a local one without a git probe.

Read-only, and sign-in reuses the GitHub CLI: GH_TOKEN, GITHUB_TOKEN, then
`gh auth token`, found on PATH or at the Homebrew locations a Finder launch
cannot see. Signed out, public repositories still work; 401, 403, 404 and
rate limits are told apart and explained. Requests go out from this machine
over the installer's ureq stack and proxy settings, on threads of their own,
cached per repository with background revalidation. Remote images in issue
text become links instead of loading, and non-web link targets are disarmed.

The Info tab gains a GitHub row that opens the branch on the remote it
tracks, or the repository for a branch never pushed.

* docs: list ShowRightPanelGitHub with the other panel actions

* feat(panel): one-line GitHub rows, a pill for the current tab

- GitHub list rows are one line: state glyph, #number, title. Labels and
  the age of the last update appear on hover, from state rather than a
  group_hover display switch, which gpui cannot paint.
- The current right panel tab sits on the sidebar's selected fill; ink
  alone could not tell five same-weight glyphs apart.
- The GitHub glyph is a 1.8px outline like the other tab icons, not the
  filled mark.
- The detail byline names both times (opened / updated) so it no longer
  reads as disagreeing with the list's update age.

* feat(github): show screenshots pasted into issues

Images GitHub hosts itself (github.com/user-attachments, a repo's
/assets, *.githubusercontent.com) now render in issue and PR text, each
in a paragraph of its own so the text view draws it at its size rather
than at line height. Images from any other host stay links, so opening
an issue still tells no third party that you read it.

gpui held a null HTTP client, so no remote image could load; the app now
installs the update check's reqwest client (same user agent and proxy)
at launch.

* fix(github): load private-repo screenshots, give inline code a neutral fill

- Pasted attachments (github.com/user-attachments/assets/<uuid>) want a
  browser session on a private repository, which an API token is not.
  The detail and comment requests now ask for the full media type, and
  each attachment is swapped for the signed private-user-images URL the
  rendered body_html carries for the same uuid.
- Inline code in rendered Markdown (the GitHub tab and the editor's
  preview) sits on a faint neutral fill instead of the theme accent,
  which is also the selection colour. Needs gpui-component 6af19d91 for
  TextViewStyle::inline_code_background.

* style(panel): tidy the GitHub and Search tabs' top rows

- GitHub drops its heading row on macOS. It existed only to hold the
  refresh tile, and no other tab has one; refresh now sits with the
  repository's other actions, in the repo row and a detail's header.
- Search's Aa / ab / .* toggles are muted while off instead of body ink.
- Search's idle note puts the folder on its own line, spelled ~/…, so
  the narrow column no longer breaks the path at a slash.

* feat(panel): order the right panel's tabs Info, Files, Search, Changes, GitHub

Info stays first as the default and the pane's overview; after it come
two pairs, the project's files (Files, Search) and its version control
from local to remote (Changes, GitHub), where Changes and GitHub were
split by the file tabs before. The palette, the Keybindings list and the
docs follow the same order.

* style(panel): drop the change count from the Changes tab

Beside one glyph of five, the number read as a badge on that tab alone,
and the Changes tab already leads with the same count under its own
heading. right_panel_tabs no longer needs the row's width, which it only
measured to decide whether the count fit.

* style(icons): fit the GitHub glyph to the other tab icons' size

The Lucide mark filled its whole 24px box, edge to edge, where tty7's
own icons keep about 3.5px clear, so at 15px it drew a size larger than
the four tabs beside it. Scale it to 0.9 about the centre, and raise the
stroke to 2.0 so it still renders at the others' 1.8.

* style(icons): a simpler GitHub glyph

Drop the Lucide mark's tail and redraw the head and legs on tty7's own
grid: the same ~15px live area and 1.8 stroke as the other tab icons, no
scale transform. The legs keep it reading as the Octocat; a head alone
read as any cat.

* test(github): find gh on PATH in the blank-variable token test

The test placed gh only at /opt/homebrew/bin/gh, which gh_candidates never
offers on Windows, so the Windows CI job panicked at unwrap. Put gh on a PATH
directory spelled with the platform's exe name instead.

* fix(github): close image and link bypasses in the issue Markdown sanitiser

Checked against markdown-rs (the parser TextView uses), several inputs got
past the line-based rewrite:

- is_github_hosted cut the host only at `/`, so
  `https://evil.io?.githubusercontent.com/x.png` (and `#`, `\`, `&#47;`)
  counted as GitHub-hosted and was fetched from evil.io. The host now ends
  at the first of `/?#\` and may hold only DNS characters.
- `<img src>` values were written into `![..](..)` unescaped, so a `)` in
  the value closed the image and opened a second one from any host. Written
  destinations are now percent-encoded.
- `<image>` (which the HTML parser reads as `<img>`) passed as an ordinary
  tag and loaded its src.
- A kept link target was copied without scanning; when the parser ended
  the link elsewhere (open title, unbalanced paren) a `![..](..)` inside it
  came alive. Markup characters in it are now encoded.
- `file&#58;///...` and similar character references passed is_safe_target
  and decoded to a `file:` link. References are decoded before judging.

The rewrite still cannot see every construct the way the parser does
(code spans inside tag attributes, fences the parser rejects, multi-line
link definitions), so the detail view now also checks the parsed tree: a
block containing a non-GitHub image, an unsafe link or definition, or raw
`<img>` is drawn as its plain source instead.

* fix(github): hide gh's console, bound Retry-After, and reject URL authorities with ?#\

- run gh through proc::output_within with hide_console, so a Windows GUI
  launch does not flash a console window and stdout is drained while gh runs.
- saturating_add a hostile Retry-After instead of overflowing i64.
- parse_github_url no longer accepts `https://evil.io#@github.com/o/r`.

* fix(search): no panic on an unbounded time budget, and read files through the size cap

ContentLimits arrive off the wire on a server; Instant + u64::MAX ms
panicked. A file that grew between the size check and the read was read
whole; it is now read through a take() at the cap.

* fix(panel): keep Load more on an empty filtered page, and drop another host's hits

- /issues pages filtered to one kind can come back empty while later pages
  hold matches; the GitHub list said "No issues" and hid Load more. It now
  reads on through up to five such pages and keeps Load more offered.
- While a new search runs, the previous hits stay on screen; if they came
  from another host, a click opened their path on the active host. They are
  now kept only when the host is the same.
2026-09-27 19:04:42 +08:00
l0ng-ai ed939bbc26 feat(search): browse every agent's past sessions, locally and on remote workspaces (#977)
* feat(search): list more agents' past sessions, and fork, copy or hide one

The Sessions tab listed Claude Code and Codex only, and a row could only
be resumed.

- Past sessions of Gemini CLI, Qwen Code, Pi, Oh My Pi, Kimi Code,
  Copilot CLI, Droid, Qoder CLI and CodeBuddy are read from where each
  keeps them (honouring QWEN_HOME, COPILOT_HOME, KIMI_CODE_HOME, …), with
  the name the agent or user gave the session, else the first prompt.
  Context blocks agents prepend (<system-reminder> and kin) no longer
  hide a prompt.
- Cmd/Ctrl-E on a session row opens its actions: Resume, Fork Session
  (agents that can fork, with the configured launch flags), Copy Session
  ID, and Remove from List. Removing keeps the search open, drops the row
  at once and remembers it in `hidden_agent_sessions`; the agent's own
  history is not touched.

OpenCode and Cursor keep sessions in SQLite and are not read yet.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM

* feat(search): list remote workspaces' sessions, and OpenCode and Cursor

The Sessions tab only ever read this computer, and left out the two
agents that keep their history in SQLite.

- agent_history moves into tty7-core, and the scan goes through the
  Host: a local workspace reads this machine in-process, a remote one
  asks its server (new ControlRequest::AgentSessions; CONTROL_VERSION
  11 -> 12, so each remote host takes one Update Server). Resumed or
  forked sessions open on that machine, in the directory they ran in.
  The last answer is kept per host so the tab does not open empty.
- OpenCode: top-level, unarchived sessions from opencode*.db (or
  $OPENCODE_DB); a placeholder title gives way to the first prompt.
- Cursor CLI: chats under ~/.cursor/chats (or $CURSOR_CONFIG_DIR), named
  from meta.json or store.db. Cursor files a chat only under the md5 of
  its directory, so it is placed by matching open tabs' and other
  sessions' directories; chats nothing matches are left out, since they
  could not be resumed anywhere.
- SQLite is bundled (rusqlite), opened read-only, falling back to an
  immutable read when the writer's WAL cannot be shared.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM

* test(search): wait for the real scan before seeding sessions

The search's own scan runs on a real thread. On CI it landed after the
test seeded its rows and replaced them, so the edit gesture found no
row. The test now waits for the scan first. Assertion messages no
longer print session ids (CodeQL rust/cleartext-logging).

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM

* fix(search): harden the past-session scan and note it in the changelog

- Honour CLAUDE_CONFIG_DIR for Claude Code's projects, as the hooks
  installer already does.
- Read a Codex rollout's head as bytes: the 2 MiB cap can split a
  multi-byte character, and read_line then dropped the whole session.
- Escape `%` and `#` (not only `?`) in the immutable SQLite URI fallback,
  so a database under such a directory still opens.
- Refuse a session id starting with `-`: ids now come from file and
  directory names on disk, and one would be read as a flag by the
  resume or fork command.
- Update the unreleased Sessions changelog entry for the new agents,
  remote workspaces, the Cmd-E actions and the v12 dialect bump.

* fix(search): spell the immutable SQLite fallback as file:///C:/ on Windows

SQLite reads file:C:/x as a relative path, so the fallback open (and its
test) failed on Windows. An empty authority and a leading slash name the
file on every platform.

* test(search): keep ? out of the fixture directory name on Windows

Windows file names cannot hold a '?', so the fixture's create_dir_all
failed there before the fallback was ever opened.
2026-09-27 18:18:05 +08:00
l0ng-ai 572bfc014b feat(ui): v4 redesign, including a rebuilt settings window (#973)
* feat(ui): restyle the right panel after the v4 design

- Tab row: 12.5/16rem word tabs 22px in and 18px apart, the current one in
  body ink at medium weight; no hover pill, no underline bar, no hairline
  under the row.
- Info: Session, Processes and Ports are spaced 16px apart with no rules;
  28px medium muted headings, 28px Session rows on a 76px label floor with
  values in body ink, 26px process rows with a tree elbow for children, and
  an explicit empty line for Ports.
- Files: the search sits in a 28px filled well; tree rows are 26px with a
  disclosure chevron column, ignored entries dim their icon instead of
  going italic, and a folder's change dot is 5px.
- docs/design-system.md updated to match.

* feat(switcher): restyle the workspace switcher after the v4 design

- Card: 112px from the top, 12px corners, 48px search row with an esc
  keycap, 420px body split 340px / preview, 40px footer.
- Workspace rows are 52px: a 26px initial disc carrying the link state as
  a ringed dot (live green, faint when offline, amber while connecting,
  red on failure), a medium name with its stable number, a machine ·
  path · time line, and the tab count over the state word.
- Preview rows are 44px with the sidebar's 18px brand disc, an all-muted
  branch · diff line, a Current label and a 5px dot that blinks with the
  sidebar while an agent is working.
- Footer: ghost New workspace button and keycap hints for navigate, open
  and new window; the unused click-for-new-window string is dropped.

* feat(scm): restyle the Changes tab after the v4 design

- Pinned block keeps 8/10/14 rhythm; branch name medium, 26px sync tile.
- Commit message box rests at 56px with a 7px radius.
- Split commit control: inverted neutral fill when committable, faint
  fill otherwise; 6px radius and an inset 0.5px seam.
- Change groups sit 16px apart under 22px sentence-case medium headers;
  file names take width first and directories right-align, eliding
  from the start.
- History: 32px header, 26px rows inset with rounded hover, 1px lines
  and 7px beads (HEAD filled, others hollow), neutral inks on a
  single-lane page, age column always shown, faint HEAD pill.

* feat(ui): restyle the rail and palette after the v4 design

- Default Light/Dark take warm neutrals (#fcfcfb/#1c1c1e, #18181a/#ececed);
  Git added/modified seeds follow v4 green and amber.
- The left rail gets its own tinted fill again (Neutrals.rail, 3% toward
  the ink) with its own surface ladder; the right panel keeps the content
  fill. Captions and hairlines are floored on the rail too.
- Title bar is 48px; the bar over the terminal centres the active tab's
  title in caption ink when tabs live in the rail.
- Rail header: 26px new-tab and collapse tiles, then the workspace chip
  and search field (28px, 7px radius).
- Groups sit 16px apart under a 22px caption heading with
  'branch · +a −d' in tabular numerals.
- Rows are 30px (42px with a branch line), 16px avatars, medium weight
  when current, branch cut from the front, and a trailing 5px status dot
  (blinks while working, hollow while waiting, unread count as a pill).
- docs/design-system.md updated.

* docs(design-system): note the commit button's inverted neutral fill

* fix(panel): align the right panel's insets with the v4 design

- Rows pad 8px inside lists inset 12px, so text sits on a 20px column in
  every tab and hover fills start 12px in with a 6px radius. Headings,
  empty states and the Ports line move to the same column.
- Tab labels 18px apart; the panel row's chrome tiles are 26px, 4px
  apart, 12px from the edge. Default panel width 280.
- Info: label column floor keeps values at x=88; Ports add tile 22px.
- Changes: 8px top gap on macOS, pinned block on 14px edges with the
  branch at 22, 12px sync glyph, 8px group chevron, 10px status cell,
  1px between rows.
- History: compact gutter for single-lane pages, filtered rows on the
  text column, 10px row gap, 24px age floor, header on 20px insets,
  4/12 padding when expanded (heights re-counted in commits).
- Files: search well at 12px with an 11px glyph, 10px before the tree,
  16px indent step, 16px bottom padding.

* feat(diff): restyle the diff overlay and commit detail after the v4 design

Carry the v4 language into the diff overlay and the commit detail view:
0.5px hairlines at 8% ink, 26px row pills with a 6px radius, the rem type
ladder from right_panel.rs, neutral chips instead of accent washes, the
shared git_badge for status letters, and tabular figures on counts.
Layout, spacing, type and colour only; no behaviour or i18n changes.

* feat(ui): restyle the dialogs, notices and home page after the v4 design

- New ui::dialog module holds the shared modal chrome, taken from the
  workspace switcher: a 12px card, a 48px title row with an esc keycap,
  18px insets, a 40px hairline footer, 28px borderless field wells on the
  faint fill, 11.5px medium muted labels, and 18px keycaps.
- Buttons: the primary is the inverted neutral fill, the Commit button's
  paint, instead of the accent. Secondary buttons are transparent with the
  surface's hover rung. Override on a changed host key stays the one red
  button. A disabled button sinks to the faint fill and drops its click
  handler.
- SSH sheet: host and fingerprint lines sit in a mono detail well, and
  keyboard-interactive prompts become field labels. Banners match the
  sheet's width and card shape.
- Worktree prompt: moves to the same card, with the path preview hung off
  the Name field.
- Notice pill: severity moves from a tinted edge to a 6px leading dot.
- Home: shortcut rows are 28px with a hover fill and keycap chords, and the
  remote strip's action uses the secondary button.

* fix(panel): start Info and Changes flush under the tab row

Their first line is text centred in a 28px row, so the extra 8px step put
it visibly lower than the Files tab's search well. Only Files keeps it.

* fix(scm): put the commit detail on the right panel's 20px text column

* feat(palette): restyle the command palette after the v4 design

- Card: the switcher's 12px corner, 112px drop from the top (shorter
  windows still scale it up), 600px max width.
- Search row keeps the list's own field; an esc keycap sits in its
  trailing corner while the field is empty.
- Rows are 32px with an 8px corner, 8px list inset and 10px padding. The
  keyboard row takes the popover's neutral selected step and a medium
  title instead of the accent wash, via a palette row element in place
  of ListItem.
- Section headings: 28px, 11.5/16rem medium caption ink, on the rows'
  text column. Shortcuts are per-key 18px faint keycaps from ui::dialog.
- New 40px footer with the switcher's keycap hints (navigate, open).
- Empty state: headline in body ink, hint in caption ink.

* feat(ui): carry the v4 chrome into panes, the file viewer and SFTP

- theme: additive helpers for a device-pixel hairline, tabular figures
  and an inverted neutral button variant.
- Pane splits rest as a device-pixel hairline in the divider tone; hover
  and drag keep the accent at 1px like the other resize edges.
- File viewer header: medium file name, 5px unsaved dot, 26px/6px close
  tile with its glyph on the content inset, divider hairline under it.
  Status bar: divider hairline, caption size, tabular line/column.
- SFTP browser: file-tree rows (26px, 6px corner, 16px caption glyphs),
  breadcrumb and notes on the 20px text column, a borderless edit well,
  inverted OK button, and ink-on-track transfer progress.
- Forward rows line up with the process and port rows (text at 20px,
  6px corner); Add and Reconnect use the inverted neutral fill.

* docs(design-system): note the v4 palette, pane, viewer and SFTP chrome

* feat(settings): restyle the settings page after the v4 design

- Nav: the rail's tinted fill and surface ladder behind a divider hairline;
  a 28px filled search well; 28px rows in 7px pills, the current one on the
  selected rung at medium weight instead of the accent; match counts in
  muted ink; the modified-only filter toggles like a nav row.
- Pages: the title sits in the 48px title-bar band at 16/16rem; group
  headings are 11.5/16rem medium muted on a 28px line; sections are split
  by a 0.5px divider with 16px either side.
- Rows: labels in body ink at regular weight, descriptions at 12/16rem
  muted, 28px floor with 8px padding; a search hit wears the faint neutral
  fill rather than the accent tint.
- Controls: text fields and dropdowns are 28px filled pills with no
  outline; buttons, segmented tracks and steppers are 26px with a 6px
  radius on the same fill. The one primary action per view (save theme
  draft, connect, install update) is the inverted neutral fill of the
  commit button. Switches and sliders keep the accent.
- SSH: host list header with 26px tiles and a filled search, 22px group
  headings, 42px two-line host rows; the form's labels are a muted,
  right-aligned column level with 28px fields; disclosure headers use a
  chevron on a 28px band.
- Theme cards are filled and unoutlined, taking the selected rung while
  open; the theme panel keeps the content fill with a divider edge and its
  title in the title-bar band. Keycaps are filled with no outline and
  shortcut rows are divided by 0.5px hairlines.
- right_panel::SECTION_GAP is now shared; docs/design-system.md updated.

* feat(ui): spell tab titles out in full in the rail and title bar

The rail's rows and the centred title have room to spare, so they take
the whole label from a new full_tab_label rather than tab_label's
three-segment cut; only the width they have decides what gets elided.

* fix(settings): even out the page rhythm and line up the columns

- Nav header: drop the min_h(ROW_H)/min_h(0) pair on the heading, which
  measured ~46pt taller than it painted and opened a hole under the search.
- Page titles sit under the title-bar band, level with the nav heading,
  instead of jammed against the window's top edge.
- Headings get a 22pt group-header row and hug their rows; rules keep more
  air, so a heading reads as its rows' rather than floating between.
- SSH: the host list gives width before the nav, so the nav no longer
  narrows on that page; its header, search well and detail title run level
  with the nav's; the empty note starts on the host-title column.
- Window & Tabs no longer opens on a stray rule.
- Integrations: status leads the buttons on one line, in the meta ink.
- Terminal: the shell footnote stays close to its rows.

* fix(ui): stop eliding branches that fit, and seat the SCM branch on the text column

- elide_tail_clusters returned "…" plus the whole string when nothing
  needed cutting, so the rail's group header printed …feat/v4-redesign
  with room to spare. Return the text as-is when it fits.
- The group header only reserves the chevron's width when it draws one.
- The Changes tab's branch name no longer stacks a small button's padding
  on the row gap; it starts on the file names' column.

* fix(ui): keep a tab's name in place when an inline rename starts

gpui-component's Input keeps 12px of inner padding even with
appearance(false), so the name jumped sideways as the rail row, the
group header and the top-strip chip swapped their label for the field.

Claude-Session: https://claude.ai/code/session_01Q9vsQSxAZjkwT7nRAiFF1J

* revert(settings): restore the page rhythm from before 08497d57

The title in the title-bar band, full-row headings, SECTION_GAP rules and
the shell footnote's spacing read better than the tightened version. The
bug fixes from that commit stay: the nav gap under the search, the stray
rule on Window & Tabs, the SSH column alignment and nav width, and the
one-line Integrations rows.

Claude-Session: https://claude.ai/code/session_01Q9vsQSxAZjkwT7nRAiFF1J

* revert(settings): restore the pre-v4 settings layout, on the rail's fill

The v4 restyle (541a887a) and the follow-ups crowded the page. Bring
settings.rs back to main's layout and give its sidebar the main window's
tab-rail fill, so the two sidebars read as one surface.

Claude-Session: https://claude.ai/code/session_01Q9vsQSxAZjkwT7nRAiFF1J

* feat(settings): rebuild the settings window after the v4 design

Rewrites the settings page to the Settings design: a sidebar with search,
per-page modified counts and a "Modified only" switch; quiet grouped rows
with an inline Reset; and the design's own controls (switch, segmented,
stepper, slider, text field, dropdown and popover menus) in a new
`settings/kit.rs`.

- Appearance: Light / Dark / System cards and a theme menu per slot with a
  live preview, search, keyboard navigation and swatches. Font menus are
  searchable and draw each family in itself.
- Keyboard shortcuts: back link, search, "Restore N changed", Default/tmux.
  A recorded chord another action already has now asks Replace / Cancel
  instead of taking it over silently.
- SSH: one column of recent hosts, "Show all" by source, search; details and
  a six-field editor open in place. Auth, jump/proxy, forwarding and
  advanced sections are no longer shown; saved values are kept.
- Integrations: machine menu, agent search, install summary, agent icons,
  and a per-row menu (Reinstall, Reveal hook file, Uninstall).
- General gains startup and restore; updates and the server move to About.
- Search results group live rows by page; a Modified view lists changes.

The page code moves out of settings.rs into src/ui/settings/.

* fix(ui): lay truncating names out at their full width

Moves the gpui fork to 5d366e6, which stops a size measured under
truncation from answering the later whole-text measure. Before it, a
truncating name beside other content in a flex_1 column read as just its
ellipsis with the whole column free. Adds a switcher test that fails
without the fork change.

* fix(scm): seat the History chevron on the change groups' column

The History header now draws its chevron in the change groups' own box
and size, so its title starts where Staged Changes and Untracked do.
Folded, the header drops to 24px with even padding instead of the
expanded section's taller band.

* chore: ignore local design mockups and Impeccable state

* fix(macos): show enter and tab shortcuts correctly in menus

Moves the gpui fork to 5c390b9, which maps enter and tab to their native
key equivalents. A menu item bound to secondary-enter, like
ToggleFullscreen, read as ⌘E.
2026-09-27 09:48:25 +08:00
l0ng-ai d843b82286 chore(release): v26.9.3 2026-09-23 19:21:12 +08:00
l0ng-ai 6aa83b4bd9 chore(release): v26.9.2 2026-09-10 11:50:47 +08:00
l0ng-ai 6649cccbc7 Merge pull request #822 from l0ng-ai/fix/notification-poll
fix(notify): stop polling Notification Center from the UI thread
2026-09-09 17:35:45 +08:00
ayamirandl0ng-ai 59dbe83913 feat(macos): add default terminal integration (#818)
* feat(macos): add default terminal integration

* fix(macos): route external opens through the layout pull

Five holes in the LaunchServices path, all on the way from a URL to a tab.

The `ssh:` arm handed the raw URL back to `parse_quick_connect`, which
reads a bare `user@host:port` typed into Quick Connect. Everything a URL
carries past the authority landed in the wrong field: `ssh://h:2200/`
parsed its port as `2200/` and was dropped on the floor, `ssh://h/srv`
became the host `h/srv`, and the percent escapes `url` was added for were
never decoded. Read the authority off the parsed URL instead.

`x-man-page://3/printf` is Apple's sectioned form, and taking the host as
the page name ran `man 3`, which asks the user what page they wanted.
Section and page are now both carried.

A window that is pulling its layout is one `Adopt::IfEmpty` will not adopt
into, so a tab inserted while the pull is out comes back as the whole
workspace — the failure `then_open` already exists to avoid. Both the
script/man path and the SSH path inserted straight into a freshly restored
window, so `then_open` becomes a list of parked requests and carries a
command or an SSH link as well as a folder. A cold `ssh://` link also went
through `open_at` directly, claiming a fresh workspace and leaving the
restored one detached and unannounced; it takes the shared restore now.

`new_tab_running` wrote the command whether or not a tab opened, so a
failed spawn typed a script path and a newline into whatever pane was
focused before — a shell mid-line, or an agent.

Left alone deliberately: an `ssh://` link still connects without a
confirmation, which is a product call rather than a defect.

Claude-Session: https://claude.ai/code/session_01E4EPKzHg1fm9HMmHkUYpER

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-09-09 17:05:58 +08:00
l0ng-ai c9ec23d090 fix(notify): stop polling Notification Center from the UI thread
macOS notifications went through mac-notification-sys with wait_for_click so
a click could reveal the pane. That crate notices a click by parking the
sending thread and adding, per outstanding notification, a repeating 0.5 s
timer on the main run loop that calls deliveredNotifications — a synchronous
XPC round trip. A banner nobody clicks stays in Notification Center, so its
timer never goes away. Sampled with nine outstanding: a fifth of the UI
thread inside that XPC, every window juddering, one more timer per agent
turn.

Drive NSUserNotificationCenter directly with a delegate of our own: the click
arrives through didActivateNotification, the pane rides in the identifier,
and nothing runs on the main thread until the user clicks. notify-rust's
show is no longer called on macOS, since it is that crate and would replace
the delegate; only set_application stays, to name a bare binary.

Claude-Session: https://claude.ai/code/session_01VuYUPiDEhQX6aQ4WQZbEGn
2026-09-09 16:50:42 +08:00
l0ng-ai 644945d137 style(ui): flatten the inline controls and give tooltips a real shortcut slot (#803)
* style(theme): drop the lift under every inline control

Theme::shadow gates exactly one thing -- the shadow_xs an inline control
(button, input, select trigger, checkbox, radio, slider knob) paints under
itself -- and never the drop shadow on a menu, tooltip or popover, which each
draw theirs unconditionally. Left on, every field and button in the window
carried a faint lift that nothing else here has: this chrome separates surfaces
with low-contrast fills and hairlines, so a control sitting a millimetre above
the panel was the one place claiming depth. Panels that really do float keep
their shadow.

Also bumps the gpui-component pin, and records why the switch and slider keep
their accent: both were tried on the neutral ramp the segmented controls use,
and a dark-grey "on" against a light-grey "off" turned out not to be a large
enough step to read while scanning a column of rows.

* style(tooltip): render a chrome tile's chord as a chord

chord_hint pasted a label and its shortcut into one string -- "Hide sidebar
<cmd>B" -- and handed that to Button::tooltip. Inside the card the chord then
wore the label's own size and colour, so the tooltip read as one odd sentence
rather than as a name with a shortcut beside it.

Tooltip already has a key_binding slot that sets a chord apart on the right, a
size down, in muted_foreground. What was missing was a way to hand Button a
built tooltip instead of a string; gpui-component grew tooltip_element for
that. chord_hint becomes chord_tooltip, and key_hint gains a key_stroke sibling
so a caller can reach the Keystroke rather than only its formatted text.

* style(settings): one field width, and a chevron that is not a patch

The right-hand column had three widths, each picked where it was written: text
fields 260, sliders 240, dropdowns 180. Every row still ended on the same right
edge, so on one page the difference read as controls aligned carelessly rather
than as controls of different kinds -- and moving between Appearance and
Terminal, where the mix differs, the column visibly changed width. FIELD_W is
the one number now, at 260, the widest of the three because it is the one with
a requirement behind it: a font name or a shell path has to fit untruncated.

The Program row's shell picker was a ghost button, which fills a rounded
rectangle while its menu is open, sized by hit_target to the 24px
accessibility floor -- exactly the field's inner height, so that fill met the
border top and bottom and looked like a patch stuck over the field's right end.
It now draws with no fill in any state, the way Select draws its own chevron.
Its menu was min_w(200) anchored TopRight on a chevron that sits inside the
field, so it hung off the field's right half with its left edge 110px in from
the field's own; it is now as wide as the field it drops out of.
2026-09-08 08:34:13 +08:00
l0ng-ai d16746a9af fix(deps): restore the lockfile edges #799's merge walked back (#802)
The merge for #799 re-resolved Cargo.lock and pointed twenty consumers at
older copies of dependencies that were already in the tree for other
crates. No `version =` line moved, so the change is invisible to the usual
scan of a lockfile diff, but the graph regressed:

  * 15 crates off `windows-sys 0.61.2` onto `0.60.2`
    (anstyle-query, anstyle-wincon, dirs-sys, errno, miow, muda,
     nu-ansi-term, quinn-udp, rustix, socket2, stacker, tempfile,
     tray-icon, uds_windows, winreg)
  * `winapi-util` off `windows-sys 0.61.2` all the way onto `0.48.0`
  * `gpu-allocator` off `windows 0.62.2` onto `0.58.0`
  * `iana-time-zone` off `windows-core 0.62.2` onto `0.58.0`
  * `dlib` off `libloading 0.8.9` onto `0.7.4`
  * `bindgen` off `itertools 0.13.0` onto `0.11.0`

Nothing in that PR asked for it. Its only dependency change was the gpui
fork rev, and the range it moved over touches one file in
`crates/gpui/src/elements/list.rs` and no manifest, so the resolution was
incidental to the merge rather than required by it.

This points those twenty edges back at the versions they held before, which
is what a fresh resolve picks. Every version already present in the lock
stays present: `windows-sys 0.60.2` is still there for `notify 8.2.0`,
which pins `^0.60.1`, and the older `windows`/`windows-core`/`libloading`/
`itertools` copies still serve their own consumers. So this drops no
duplicate builds; it only stops the newer copies from being compiled
alongside older ones for crates that had already moved on.

Lockfile only. No manifest and no source changes, and `cargo metadata
--locked` accepts the result without wanting to rewrite it.
2026-09-08 08:18:26 +08:00
dependabot[bot] cb710c4d79 deps: bump async_zip from 0.0.18 to 0.0.19 (#764)
Also bump the version requirement in Cargo.toml, which dependabot left at 0.0.18 and which made every --locked job fail.
2026-09-07 22:23:28 +08:00
l0ng-ai 081e191bb0 perf(diff-overlay): draw the patch as a virtualised row list (#799)
The overlay built its whole patch as a nested element tree on every frame:
a card per file, a header per hunk, six elements per line. gpui notifies the
view on each scroll wheel event, so a few hundred lines of diff rebuilt tens
of thousands of elements tens of times a second, and the window stalled.

Flatten the tree into one row per line in a new `diff_list` module and draw it
with `gpui::list`, which builds only the rows on screen. The rows are rebuilt
only when what they are built from changes, so scrolling no longer re-splits
hunks or re-clones every line, and a change to one file splices just the rows
it touched rather than resetting the list and losing the scroll position.

The key that decides a rebuild takes the snapshot each frame was asked about
even when it matched only by contents. A probe that finds nothing new still
lands a fresh `Arc` over an equal snapshot; a key left pointing at the old one
would go on walking the whole patch to prove the two equal, once per wheel
event, which is the cost the key exists to avoid.

A list counts a row it has not laid out yet as zero tall, which left the
scrollbar reading an 800-line patch as one viewport: its thumb filled the
track, and a drag from top to bottom travelled 248px and stopped. The rows
below the fold are counted at the 19px both views already give a line of a
patch, through `ListState::with_size_hint` — added to the gpui fork for this,
`Cargo.lock` following its `tty7` branch to `ece710e3`.

A card cannot survive that flattening — its rows are separate items now — so
the frame it drew is gone, and with it the grey header bars and hunk bands
that made the overlay the one view in the app still speaking gpui-component's
default container language. The rows take the source control panel's own
measurements instead: 26px, 10px inset, 5px radius, colour only under the
pointer. The title bar's view switch loses its border for the same reason.
2026-09-07 22:13:57 +08:00
dependabot[bot] 6474e25a24 deps: bump the cargo-minor-patch group across 1 directory with 3 updates (#776) 2026-09-07 22:09:19 +08:00
l0ng-ai 20b73adb2a chore(release): v26.9.1 2026-09-07 21:00:55 +08:00
l0ng-ai 37be703d5b chore(release): v26.9.0 2026-09-04 17:42:08 +08:00
dependabot[bot] 436e9c4320 deps: bump the cargo-minor-patch group with 2 updates (#724)
Bumps the cargo-minor-patch group with 2 updates: [uuid](https://github.com/uuid-rs/uuid) and [ureq](https://github.com/algesten/ureq).


Updates `uuid` from 1.24.0 to 1.24.1
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.24.0...v1.24.1)

Updates `ureq` from 3.3.0 to 3.4.0
- [Changelog](https://github.com/algesten/ureq/blob/main/CHANGELOG.md)
- [Commits](https://github.com/algesten/ureq/compare/3.3.0...3.4.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.24.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-patch
- dependency-name: ureq
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 16:09:02 +08:00
l0ng-ai 46759b8a01 fix(input-bar): read column widths from unicode-width, not a hand-rolled table (#704)
* fix(input-bar): read column widths from unicode-width, not a hand-rolled table

The input bar scored every character against a hand-written list of code-point
ranges. Anything the list missed counted as one plain column, so `🀄`, `⌚` and
every combining mark pulled the rest of the row a column left, and clicks,
wrapping and the caret all landed off by that much (#701).

The grid gets its widths from `unicode-width` by way of `alacritty_terminal`,
so read the same table. Zero-width characters then need a cell to ride in:
group each base with the marks that follow it, so the shaper sees one run and
composes `é` instead of setting `e` and its accent side by side. An emoji
presentation sequence is re-scored as a string the way the grid re-scores it,
so `❤️` is two columns in the bar as well.

A ZWJ sequence stays two cells on purpose — that is what the grid makes of it,
and composing it here would put the bar a column off from where the text lands.

* fix(input-bar): derive click and wrap geometry from the cells the bar draws

`input_cells` re-scores an emoji presentation sequence to two columns and
hands a stranded combining mark a column of its own, but `input_char_positions`
kept walking the text character by character — so `❤️` was drawn two columns
wide and counted as one. Everything geometric read the short count: a click on
`X` in `❤️X` selected past it, wrapping broke a column early, and vertical
caret motion aimed at the wrong column.

Walk the same cells instead. Only the base of a cell carries the width, so a
click still lands on the base rather than a mark riding on it, and the riders
sit at the column the caret takes after the cell.

A cell now also tints as a unit when a selection covers any character in it —
it is one glyph, so half-highlighting it drew a mark unselected next to its
selected base.
2026-08-20 22:35:32 +08:00
Austin Spragginsandl0ng-ai 2cdc26f357 Wire hooks, resume and detection for Kimi Code CLI (#694)
* feat(agents): wire hooks, resume and detection for Kimi Code

Kimi Code CLI takes its hooks as [[hooks]] entries in the same
config.toml that holds the user's providers and models, so this adds a
third install strategy — a format-preserving TOML merge on toml_edit —
beside the JSON map merge and the owned files. Like Qwen it reports
permission requests first-class, so it gets no Notification hook.
Resume rides `kimi --session <id>`; fork stays unwired, Kimi
documents none.

Closes #693

Signed-off-by: Austin Spraggins <spragginsdesigns@gmail.com>

* fix(agents): harden the Kimi Code TOML hook merge and its resume flags

The TOML merge strategy the Kimi wiring introduces round-trips a shared
config.toml cleanly, but three gaps sat behind it.

`hooks_state` counted only the marked entries that still named an event,
so a hand-edit that dropped the key off one of nine entries left the
remaining eight matching the roster exactly and the file reported
Installed with a broken entry in it. Every marked entry now counts,
which is what the JSON merge already did and what `refresh_hooks` needs
to see.

A `hooks = []` spelled as an empty inline array made install fail
outright -- toml_edit keeps an empty array and an array of tables apart,
but the two say the same thing and neither carries any configuration. It
is now promoted rather than refused. Every other wrong-shaped `hooks`
key -- a string, a table, a non-empty inline array -- still refuses with
the file left byte-for-byte alone.

`Stop` is not the only way a Kimi turn ends: its own event reference says
`Stop` does not fire on interrupts and `Interrupt` fires instead, and a
turn that dies on an error reports `StopFailure`. Without those two an
Esc or a failed turn left the pane on "working" for good and `tty7 wait`
could only ever time out. Both are observation-only events and report
the same end of turn `Stop` does.

On resume, `--agent` and `--agent-file` join the stale flags: Kimi
rejects either next to `--session` at startup, and resuming rebinds the
session agent by itself, so replaying them turned a working resume into
a launch error.

Tests cover the wrong-shaped `hooks` keys, a config.toml that does not
parse on both install and uninstall, a file that does not exist yet, a
second install being byte-for-byte the first, mangled and surplus marked
entries, an uninstall threading between the user's own entries and the
tables after them, and the `--session=<id>`, bare `--session`,
`--continue` and `--agent` spellings on the resume path.

---------

Signed-off-by: Austin Spraggins <spragginsdesigns@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-20 09:51:05 +08:00
l0ng-ai 9f34cd3501 fix(editor): stop scrolled-out text painting over the line numbers
Bump the gpui-component fork to 070d1a2, which clips the editor's scrolling
content to the right of the gutter. Text, selections, indent guides and the
cursor all paint from a bounds origin that horizontal scrolling has already
shifted left, so scrolled-out content kept painting under the line-number
column; the only thing hiding it was the gutter quad painted afterwards,
which works only while `editor.gutter.background` is opaque.

`apply_theme` clears that key to transparent so the panel can sit on a
gradient or image window background without a seam, which is exactly the
case the upstream code does not cover. Note that dependency in the theme,
so the next person to touch it knows the transparent gutter is not free.
2026-08-16 19:05:53 +08:00
l0ng-aiandl0ng-ai 0df604054d fix(daemon): keep a lingering daemon findable and reapable after quit-and-stop (#655)
* fix(daemon): keep a lingering daemon findable and reapable after quit-and-stop

Quit-and-stop could strand a daemon that had already unlinked daemon.sock
and deleted daemon.pid but never finished exiting: libc exit() runs atexit
handlers and static destructors beside dozens of live threads, and a
finalizer that blocks leaves the process holding the singleton lock with no
name on disk. Every later launch then spawns a daemon that stands down
against the lock and times out red, forever.

Three changes, each a fallback for the others:

- on_shutdown keeps the pidfile: once the endpoint is unlinked it is the
  only handle anything has on a process that is not gone yet. A pidfile
  that outlives a clean exit was already handled by recorded_daemon_is_dead
  and the reap path.
- The daemon exits through _exit(2) (after flushing the logger), skipping
  the atexit/destructor window entirely; everything owed to disk is flushed
  explicitly in on_shutdown.
- spawn::stop reaps with the pid it captured before asking the daemon to
  die, instead of re-reading a pidfile an old build's shutdown may have
  wiped mid-stop; reap_recorded_daemon keeps the pidfile when the process
  survives even SIGKILL, so the next attempt still has someone to reap.

* review: fix stale stop() comment, pin the mid-stop pidfile-vanish ordering in the test

The comment at the top of stop() still claimed a clean shutdown removes
the pidfile, which this branch just made untrue; it now states the real
reasons the pid is captured early. The vanishing-pidfile test now asserts
the sweeper's delete actually landed while stop() was waiting, so a
future shrink of PROCESS_EXIT_TIMEOUT cannot silently turn it into a
weaker scenario.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-16 17:35:39 +08:00
l0ng-aiandl0ng-ai 05de7ae33a fix(new-tab): keep the SSH menu inside a menu's shape (#649)
* fix(new-tab): keep the SSH menu inside a menu's shape

The saved-host rows carried names and endpoints long enough to drag the panel
out to the 500px ceiling PopupMenu falls back to, and the row that meant to
elide was clipped mid glyph instead. The menu now stops at 360px, and a row
that runs out of room cuts the endpoint first — the name is what the reader is
picking by, so it keeps whatever is left rather than being squeezed to "..".

The height ceiling moves up to fit the shape everyone actually sees — nine
shells, both headings, six hosts and the two closing rows — so the default menu
arrives whole instead of scrolled with "Local" cut off above, and is capped
again against the window so a short one never gets a menu taller than itself.

The rule above the split hint goes: a separator divides two lists of things to
pick, and the hint is a footnote about the list it follows.

Bumps gpui-component, where a scrollable PopupMenu painted a scrollbar whether
or not it overflowed, custom rows could not elide, and labels had no padding of
their own.

* fix(new-tab): measure the menu ceiling off the viewport, and elide nameless hosts

`window_bounds()` answers how a window should be reopened after it is
closed, so a fullscreen macOS window reports the bounds it would restore
to rather than the screen it currently fills. A terminal spends much of
its life fullscreen, where that reading capped the menu at 80% of a
window nobody is looking at — putting back the scrollbar and the
cut-off `Local` this branch is here to remove. `viewport_size()` is what
every other window-relative size in the app already measures against.

A host saved on its address alone is *named* `user@host:port` and carries
no note, so it took the plain-item path — bare text with nothing to elide
against, on the longest string in the menu and the row least able to cut
it. Every host row is a custom element now, and `menu_row` drops its
right half when the note is empty rather than holding the gap open with
a zero-width child.

Also drops 17 unrelated dependency downgrades that rode along with the
`gpui-component` bump. The lockfile moves only the three `source` lines
it meant to; `cargo check --locked` accepts it.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-15 14:24:22 +08:00
l0ng-ai f1144deb9f fix(ui): restyle the in-app notification to sit in tty7's own visual language (#646)
Bumps the gpui-component pin to pick up the notification restyle: flow-positioned status icon and close button that centre on the first line at any wrap count, a hairline-shadow surface in light theme, and a type ranking expressed in rems so it survives the ui_font_size setting.
2026-08-15 11:03:13 +08:00
l0ng-ai 71c6783fb4 chore(release): v26.8.3 2026-08-12 18:41:37 +08:00
l0ng-ai 2dc6a88af6 merge: main into the Source Control branch
Conflicts were the two streams touching the same seams, resolved by
taking the newer decision on each side:

- main's interface font scale (rems tokens) wins in right_panel.rs; the
  SCM panel keeps its local px steps until it moves onto that scale,
  and the now-unused PANEL_TEXT constants are gone.
- main's l10n_keys! macro (idents only) means the key list carries no
  doc comments any more; our SCM keys fold into it, and PanelUntracked
  stays deleted — its only caller was the panel this branch replaced.
- main's Command::localized palette style carries our Git group; ORDER
  keeps main's visibility and our width.
- main's ansi_seed/clear_ink refactor in presets.rs carries the lane
  colours: lanes() now clears through the same helper semantics uses.
- file_tree keeps both: main's drag-and-drop targets and this branch's
  git decorations per row.
- diff_overlay keeps both: main's sidebar-count write-back on snapshot
  install and this branch's epoch read and untracked preview.
- main's window.prompt SSH-close confirmation supersedes the bespoke
  modal our branch still carried; main's tile-glyph revert stands.
- main's two new guards are satisfied: the fourteen SCM actions carry
  authored names on the Keybindings page (their palette wording, plus
  a new CmdGitToggleGraph), ja translates ScmDetached, and CmdGroupGit
  joins the kept-in-English list — Git is a name.

2571 tests, 0 failures.
2026-08-10 13:25:30 +08:00
l0ng-ai 44f0683d0a fix(sidebar): cut labels on grapheme clusters, not on chars (#450)
The sidebar's elision measures against real glyph widths but slices by
`char`, so it can satisfy every width check and still hand back a torn
cluster. Scanning budgets from 30px to 200px over emoji fixtures, 48
widths produced output no font can render as intended:

    "release-…\u{200d}👩\u{200d}👧"   a joiner with nothing in front of it
    "lon…\u{fe0f}"                    the variation selector lands on the ellipsis
    "abcdef…🇳ghijklmnopqr"           half a flag, which renders as a bare N

A tab title carrying an emoji is not exotic — plenty of TUIs and coding
agents put one there — and the second case is the same U+FE0F this repo
already carries an alacritty patch for.

`elide_keep_edges`, the tail-only fallback, and `short_title`'s 40-glyph
clamp now index grapheme clusters. `elide_path_keep_tail` cuts on `/`
and was already safe. Widths are unchanged: clusters are measured the
same way chars were, so every existing elision test still passes on the
same fixtures.

`unicode-segmentation` is already in the tree via gpui; pinning it here
adds one line to Cargo.lock and no new code.

Tests assert the property rather than the symptom: whatever survives on
either side of the ellipsis has to be a cluster-aligned prefix and
suffix of the input. That catches any tear, not just the three shapes
found here. Written first, confirmed failing on all three cut sites, and
green after.
2026-08-10 10:38:04 +08:00
l0ng-ai 0106430ecd merge: main into the Source Control branch
The one conflict is an import list in `diff_overlay.rs`: this branch added
`SharedString` for the unified view's row labels, main added `Background`
and `Hsla` for the window backdrop work. Both sides are still used, so the
resolution is the union.

Worth recording why this merge happened when it did. `main` moving is not
normally urgent — branch protection dropped its strict check, so a branch
behind main still merges — but a *conflicting* branch is different: GitHub
cannot compute `refs/pull/N/merge`, and every workflow that triggers on
`pull_request` silently stops running. Three pushes in a row registered no
CI at all on #424 while other PRs kept going green, which reads as a GitHub
incident and is really just an unresolved conflict.
2026-08-09 16:20:19 +08:00
ARNOandl0ng-ai 61efe27f2d feat(windows): add native backdrop material presets (Mica / Acrylic /… (#412)
* feat(windows): add native backdrop material presets (Mica / Acrylic / Blur)

Adds a Background material dropdown (Auto / Blur / Mica / Mica Alt /
Acrylic / Off) that maps onto the native Windows backdrop APIs already
provided by the gpui fork — Mica and Mica Alt via
DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE), Acrylic via the new
DWMSBT_TRANSIENTWINDOW material, and Blur via the classic
ACCENT_ENABLE_ACRYLICBLURBEHIND path — with no fork changes required.
* config: introduce WindowBackdrop in tty7-core with lenient kebab-case
  deserialization, defaulting to Auto for existing configs
* theme: resolve the backdrop through a build-number fallback chain
  (Mica/Mica Alt need Windows 11 22H2, Acrylic needs 22H2 natively and
  1809 via classic acrylic, Blur needs 1809; older builds fall back to
  plain translucency) and default the background alpha to
  SYSTEM_MATERIAL_OPACITY (0.82) while a material is active
* settings: replace the blur toggle with a localized backdrop dropdown
  that only lists the presets the current Windows build actually
  supports, and keep the settings panel fully opaque so workspace
  translucency never shows through it
* theme: make the file sidebar and right detail panel follow the window
  opacity so the backdrop material shows through the whole workspace,
  keeping row-level accents opaque for readability
* i18n: add backdrop keys for en, zh-CN and ja-JP, covered by the
  translation completeness test

* feat(theme): let the sidebar and right panel follow the window opacity

* update GPUI

* fix(windows): gate the sidebar translucency to translucent windows and sync the opacity slider

fix(windows): gate the sidebar translucency compensation to active materials

* fix(windows): derive the material opacity default from the resolved appearance

* fix(theme): keep WindowBackdrop semantics consistent on non-Windows

f

* fix(theme): stop Windows-only materials from pinning the blur on other platforms

* docs(changelog): document the Windows backdrop material settings

* refactor(theme): share the default window-opacity derivation

* fix(ui): keep gradient presets behind the settings panel and scope its fallbacks

* fix(ui): keep the settings theme picker legible and the backdrop label honest

f

* fix(theme): let every backdrop variant defer to the local blur toggle on non-Windows

* fix(settings): restore the backdrop dropdown selection on locale refresh

* fix(ui): keep the opened-file editor surface opaque under window translucency

* fix(settings): rebuild backdrop options after selection

* fix(settings): ignore synced windows backdrop overrides on other platforms

* fix(settings): preserve synced windows backdrop on non-windows reset

* fix(diff): keep the full-window overlay background opaque

* fix(windows): keep Auto opaque and stop the backdrop from misreporting itself

Ten findings from a review of the backdrop-material work, all in the
Windows-only paths.

The root one: `material_active` treated `Auto` as a material whenever the
legacy blur toggle happened to be on. `Auto` is the default in every config
written before this setting existed, and plenty of them carry
`window_blur: true` from the switch that no longer renders on Windows, so an
untouched install would drop from opaque to 0.82 alpha - with its file
sidebar and right panel at 0.15 - on first launch after the update, with no
visible control to undo it. Only an explicit pick in the dropdown now buys
the translucent defaults. The switch comes back on Windows while the
backdrop is `Auto`, since that is exactly when the legacy flag still decides
something.

The rest:

- Mica and Mica Alt fell back to `Blurred` with no lower bound, asking for a
  blur that does not exist below 1809 - and build 0, which is what a failed
  `RtlGetVersion` reports. They now degrade to plain translucency like
  `Blur` and `Acrylic` already did.
- Acrylic is no longer offered below 22H2, where it resolves to the very
  same classic WCA blur as `Blur`. A test now asserts that no two offered
  presets render identically on any build.
- `reload_from_config` re-applied the theme and the opacity slider but not
  the backdrop dropdown, so an external config change switched the window's
  material while the control kept naming the old one.
- The settings, opened-file and diff overlays were made opaque so the OS
  backdrop cannot show through their text; that also hid the theme
  background image, which used to show through them. They paint their own
  copy of it now, and the fill they share moved into
  `theme::overlay_background`.
- The SFTP transfers tray painted `workspace_surface_color` inside the right
  panel, which already paints it, stacking the same translucent surface
  twice into a darker band with a hard seam.
- `apply_theme` re-issued `set_background_appearance` on every `Config`
  mutation in every window. With a DWM material that now costs a
  `SetWindowPos(SWP_FRAMECHANGED)` frame recalc, so dragging the opacity
  slider recalculated the frame once per mouse sample; it is skipped when
  the appearance is unchanged.

* fix(ui): dim the overlay background image, and stop telling Windows it is macOS

Two defects found while driving the previous commit's changes in the app.

The overlays repaint the theme background image over their own opaque fill,
so it survives them being made opaque - but nothing dimmed it. Before those
overlays were opaque the image reached the eye through their translucent
fill; painting it at full strength put the settings text straight on top of
the wallpaper and made the panel unreadable at any image opacity above about
half. They now paint the image and then the workspace's own fill over it,
which is exactly the strength the image had through these overlays before,
and which needs no new constant to say so. Shared as
`app::overlay_surface_layers`, empty when the theme has no image so a
themeless window paints no second pass of anything.

The Windows-only blur row reused `SettingsBlurDesc`, whose text ends in
"(macOS)". It gets its own key in all three locales, describing the job the
flag actually still has on Windows: feeding the `Auto` material.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-09 15:14:54 +08:00
l0ng-ai b4e7add65d chore(release): v26.8.2 2026-08-09 09:59:26 +08:00
l0ng-ai 9914a939b4 chore(deps): regenerate the lock for tty7-core's smallvec
The dependency was declared but the lock was never refreshed, so every
worktree building against it produced the same one-line diff.
2026-08-09 00:27:31 +08:00
l0ng-ai 51c35aac1f fix(terminal): resize ConPTY panes with conhost's semantics, in stream order (#415)
ConPTY emits no repaint after a resize; conhost silently re-anchors its
layout and keeps painting with absolute cursor addresses computed against
it. Measured live: growing the window keeps rows and cursor pinned and
opens blank rows below, and shrinking scrolls the last written row to the
new bottom. The grid resized the alacritty way instead, so after a
maximize every absolute-CUP paint landed mid-screen inside the old output.
The vendored alacritty_terminal now has a conpty_resize mode mirroring
conhost's model (fork rev 1276f12); every Windows pane opts in.

Separately, a resize during a burst of output reflowed ahead of the
backlog (up to the gate's 16 MiB of old-width bytes). The daemon now
echoes a Size frame to the controller at the exact stream position where
the PTY geometry changes, and a client that probes the new resize-echo
feature defers its reflow to that marker. Remote routes and older daemons
keep the reflow-at-request-time path.
2026-08-08 22:40:04 +08:00
Hongwei Qin bb72be338d fix(windows): respect system proxy for remote server downloads (#364)
The GUI update check already uses reqwest, which reads the Windows
system proxy from the registry by default. The remote server
installer / bundled-server fallback uses ureq, which only reads
HTTP_PROXY/HTTPS_PROXY environment variables unless the
win-system-proxy feature is enabled.

Enable ureqs win-system-proxy feature so that release downloads
inside the daemon also honor the Windows system proxy set by tools
like Clash (System Proxy mode), v2rayN, etc. This is a no-op on
non-Windows platforms.

Fixes the inconsistency where the update check could reach GitHub
through the proxy but the actual download would time out trying to
connect directly.
2026-08-07 11:57:14 +08:00
Hongwei Qin 4a8a4bcbaa feat(proxy): macOS system proxy, Windows SOCKS parsing, manual override (#367)
Resolve an HTTP/SOCKS proxy for tty7's own update checks and release downloads, from (in order) a new `http_proxy` config field, the platform system proxy — Windows registry / macOS SCDynamicStore — and the HTTP_PROXY/HTTPS_PROXY/ALL_PROXY environment variables.

Programs running in a pane are deliberately unaffected: they inherit their proxy from their own environment, as in any other terminal.

Fixes #365.
2026-08-07 11:35:21 +08:00
Hongwei QinandHongwei Qin a7de7db2c4 feat(windows,macos): clickable toasts, richer context, and i18n (#373)
Desktop notifications now carry the pane they came from: clicking one reveals
that pane's window, tab and split. Windows shows a WinRT toast with an
`Activated` handler, macOS uses mac-notification-sys' click response, and both
route through the existing tray dispatch channel. Linux keeps the plain
notify-rust path.

Titles gained context — an agent name or the machine, then the workspace — and
bodies name the command or agent alongside the duration, all of it translated.

Notification text is sanitized on every path: it comes off the terminal, and a
stray control byte used to make the Windows toast XML fail to parse and lose the
notification outright.

Co-authored-by: Hongwei Qin <exqinhongwei@outlook.com>
2026-08-07 11:27:07 +08:00
l0ng-ai e1531cdea6 revert(windows): drop the taskbar status dot (#377)
The per-window taskbar overlay badge (#355, for #199) is removed, and with
it the in-flight follow-up that was making its green "finished a turn"
state reachable: the feature is not wanted. Nothing shipped — the badge
only ever existed in Unreleased — so this is a plain removal rather than a
deprecation, and its CHANGELOG entry goes with it instead of gaining a
"Removed" counterpart.

What goes: `ui::taskbar` and its `ITaskbarList3::SetOverlayIcon` poll, the
`taskbar_status_icon` config flag and its Settings → Window & Tabs row and
strings, `Tty7App::taskbar_signals`, `TerminalView::shell_busy` /
`RemoteTerminal::shell_busy` (the overlay was their only caller), the
`raw-window-handle` dependency and the `Win32_UI_WindowsAndMessaging`
feature it needed, and the feature docs in both languages. A stale
`taskbar_status_icon` left in someone's `config.json` is ignored, as any
unknown key is.

The tray badge and the in-window status dots are untouched; they were
always the ones the taskbar was mirroring.
2026-08-06 21:40:25 +08:00
27bb1864df feat(windows): taskbar status overlay per window (#355)
* feat(windows): taskbar status overlay per window (#199)

Stamp a colored status dot on each window's taskbar button using the
same palette as the in-window agent dots:
- blue while a shell command or agent is working,
- amber when an agent is waiting on the user,
- green when work finishes while the window is unfocused (cleared on activation).

Adds a `taskbar_status_icon` setting (default on, Windows only) and a
Settings -> Window & Tabs row. The overlay is updated by a foreground
poll that aggregates agent status and shell busy state across each
window's panes, diffing against the current taskbar badge and only
calling ITaskbarList3::SetOverlayIcon when the badge changes.

Includes unit tests for overlay priority and the done-while-unfocused
edge tracking.

* fix(taskbar): retry a failed overlay instead of caching it as drawn

Four fixes on top of the overlay:

- A failed SetOverlayIcon was still recorded in `shown`, so a badge the
  taskbar never took was remembered as drawn and never retried. Stamp now
  reports success, and a failure drops the interface so the next tick
  re-creates it — which is also what an Explorer restart needs.
- `create_failed` was a permanent latch: one CoCreateInstance failure
  killed the badge for the whole process, though Explorer may simply not
  be up yet when the first window opens. Use the tray's attempts/cooldown
  backoff instead, which this module otherwise copies.
- The overlay's accessibility description was hard-coded English in an
  app that localizes everything else. Reuse the panel and tray strings.
- Render the dot at 32px, not 16. SetOverlayIcon wants 16x16 at 96 dpi,
  so at 150%/200% scaling the shell upscaled a 16px icon; `tray::icon`
  already renders at 32 off macOS for the same reason.

Also drops the Win32_Graphics_Gdi feature: CreateIcon, DestroyIcon and
HICON all live in Win32_UI_WindowsAndMessaging, and the build and the
taskbar tests pass without it.

Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-06 10:12:41 +08:00
603bca171e feat(updater): add windows updates and cross-platform nightly support (#330)
* feat(updater): add windows online updates

* feat(updater): support online updates for windows portable zip builds

f

* feat(updater): support online updates for nightly build

* fix(updater): strengthen post-download update verification

* feat(updater): support explicit stable and nightly channel switching

* fix(i18n): localize update settings ui

* fix(settings): prevent slider value labels from wrapping

* feat(updater): drop the nightly channel, refuse all-users Windows installs

Follow-up to the Windows updater work on this branch, applying maintainer
review.

Nightly is a build channel, not an update channel. The updater consults
`/releases/latest` again and nothing else, so it behaves on Windows exactly
as it already does on macOS: a Nightly build is offered the stable release
that supersedes it and graduates out of the prerelease, and no rolling
prerelease can become a source of code that gets executed on a user's
machine. Removed with it: the `UpdateChannel` enum and its version-string
inference, the `tags/nightly` query, the cross-channel version-ordering
bypass, the Settings → About channel row, the rolling-tag
`update-manifest.json` and the i18n keys that only served them.
`parse_version` and `is_update_available` are byte-identical to main again.

Nightly builds are untouched, and still carry tty7-updater plus the macOS
update archive — a Nightly user needs a working helper to reach the stable
release that replaces their build.

An all-users Windows installation is no longer updated in place. Running the
release Setup silently as the signed-in user cannot replace
`C:\Program Files\tty7`: Inno resolves `{autopf}` to `%LocalAppData%\Programs`
and installs a second copy beside the real one, or re-launches itself
elevated and puts a bare UAC prompt for an unsigned executable in `%TEMP%` in
front of a user whose GUI just vanished. tty7 declines both and points at the
release page. Detection reads Inno's own `HKLM` state for the frozen AppId and
independently probes whether the directory accepts writes, so a relocated or
pruned installation is caught too; the decision is a pure function with unit
tests, and it is re-checked before the download as well as during it.

Release and Nightly now verify the Windows packages they just built, mirroring
the macOS update-archive step: the install marker, tty7-updater.exe, the ZIP
layout the updater will accept and the PE versions it will demand. Every fact
the updater checks on the user's machine after downloading is checked here
instead, so a packaging mistake fails the build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:27:37 +08:00
618855cf4a fix(windows): brand toast notifications with a tty7 AUMID (#340)
* fix(windows): brand toast notifications with a tty7 AUMID (#339)

* fix(windows): only write the toast shortcut where it is ours to write

The AUMID shortcut was rewritten on every launch, which broke two cases
the review caught on a real machine.

An elevated install owns `%ProgramData%\...\tty7.lnk`, so writing a
per-user copy listed "tty7" twice in the Start Menu and left an orphan
pointing at a deleted exe once the uninstaller had removed only its own.
And `cargo run` repointed the installed shortcut at `target\debug`,
permanently, for anyone who both installs tty7 and builds it.

So decide before writing. An all-users shortcut settles the question by
itself — branded if the installer stamped our AUMID on it, otherwise we
stay on the PowerShell identity, because the alternative is littering a
Start Menu we cannot clean up. Otherwise we refresh the single per-user
`tty7.lnk` Inno's default install owns anyway, and only when it is not
already ours, and never from a cargo build directory. A dev build still
brands the process for taskbar grouping, and still gets branded toasts
when an install left a stamped shortcut behind — Windows asks that the
AUMID be registered, not that it point at the process using it.

Reading a shortcut back needs `IShellLinkW::GetPath`, hence the
`Win32_Storage_FileSystem` feature; `SLGP_RAWPATH` keeps it from chasing
a moved target over the network.

Also close the window this opened. The shell indexes a new `.lnk`
asynchronously and, for an AUMID it has not seen, `Toast::show()`
reports success and drops the toast — measured, it does not return an
error. A shortcut we wrote seconds ago is therefore not yet proof of
anything, so toasts keep the PowerShell identity for half a minute after
we write one: ugly beats invisible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 21:09:07 +08:00