Commit Graph
1615 Commits
Author SHA1 Message Date
l0ng-ai 80ab718ac3 fix(notify): reveal the pane from toasts clicked in notification history
Windows only reports a click on a toast that has left the screen through
COM: the shell activates the ToastActivatorCLSID named on the shortcut
that carries the AUMID. tty7 had no such class, so clicks from Action
Center did nothing.

- Stamp a fixed ToastActivatorCLSID on the Start Menu shortcut (runtime
  and installer); a shortcut lacking it is rewritten.
- Register the class per-user as a LocalServer32 (never from a cargo
  build directory) and as a running class object at GUI startup.
- Implement INotificationActivationCallback; the toast's launch string
  carries the pid-checked pane identifier, so a click on a toast from an
  earlier process only brings the window forward.
- Queue tray actions that arrive before the dispatch loop exists, which
  is the cold-start case when COM launches tty7 to deliver the click.
- Deduplicate a popup click that arrives through both COM and the
  in-process Activated event.
- The uninstaller removes the CLSID registration.

Fixes #1179
2026-10-15 15:18:19 +08:00
8c5ea523b3 fix(titlebar): the title bar's lead moves with the traffic lights (#1146)
* fix(titlebar): the title bar's lead moves with the traffic lights

#1139 moved the lights from 9pt to 16.5pt in, but the 80pt `TITLE_BAR_LEAD` the tab strip, editor header and search band start at stayed put, so the gap after the zoom button shrank from about 17pt to 9.5pt. On macOS the lead is now derived from the lights: their inset, their 54pt span (three 14pt buttons on a 20pt pitch), and the inset again, 87pt. The strip-width calculation that hard-coded 80 uses the constant, the search-band tests use it instead of 80, and a test checks the lights end, with their inset, before the lead. Off macOS nothing changes.

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

* fix(titlebar): TitleBar pads the strip by the lead, not its own 80pt

gpui-component's TitleBar hard-codes an 80pt left padding on macOS, so
raising TITLE_BAR_LEAD to the traffic lights' 87pt left the tab strip at 80
while the strip width, search band and document headers assumed 87. Set the
padding from TITLE_BAR_LEAD so they all start at the same x.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-10-13 19:54:31 +08:00
Adam HitchcockandClaude Opus 5.5 d2d3793f50 test(daemon): a dead pane's refusal may come after it is reaped (#1170)
send_input_to_a_dead_pane_answers_an_error raced the reaper: the session
ending after Exited lets the server drop the dead pane, so the input can
find no pane at all and answer "no such pane". Both are refusals.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:25:45 +08:00
l0ng-ai 523d834eb8 fix(switcher): defer host-menu disconnect so it cannot abort the app (#1162)
The host menu's Disconnect ran RemoteLinks::disconnect inside an update
of the window's own Tty7App. The disconnect detaches the panes of every
window on that machine by reading each window's app, this one included,
which panicked on the double lease. The panic happened inside the
mouse-up platform callback, where it cannot unwind, so the process
aborted and every window went down with it.

Defer the disconnect until the update is over, and add a regression test
that reproduces the original panic without the fix.
2026-10-13 00:36:26 +08:00
l0ng-ai f995a06902 fix(daemon): keep mobile input working with many open panes (#1159) 2026-10-12 23:17:20 +08:00
l0ng-ai 5c45d17513 fix(sidebar): retry negative repository probes for idle panes (#1157) 2026-10-12 23:17:14 +08:00
l0ng-ai 0a55c33b72 chore(release): v26.10.0 v26.10.0 2026-10-12 15:03:16 +08:00
l0ng-ai 559672614f fix(updater): skip mobile releases when checking desktop updates (#1156) 2026-10-12 15:02:42 +08:00
l0ng-ai 009e1d5158 fix: retain daemon diagnostics and preserve live sessions during recovery (#1153) 2026-10-12 14:12:54 +08:00
wickandwenlingang f1847b70b2 fix(terminal): a double click on the prompt selects the word under it (#1143)
With the inline prompt editor live, every click on the input row went to
the editor, including clicks on the prompt the shell drew ahead of it.
A click left of the input start was mapped to the editor's first
character, so double-clicking the branch in ` tty7 git:(main)` ran a
word select over the editor's own (often empty) text and selected
nothing, and its first click had already thrown the caret to the start
of the line.

A click on the prompt now bypasses the editor and goes to the grid's
selection like a click on any other output: double click selects the
word, triple click the line, and the caret stays where it was. Clicks
on the typed text are unchanged.

Co-authored-by: wenlingang <lingang.wen@longbridge.sg>
2026-10-10 21:53:53 +08:00
Adam HitchcockandClaude Opus 5.5 a39d97d5c5 fix(daemon): a pane keeps its private modes across a handoff (#1145)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:53:49 +08:00
Adam HitchcockandClaude Opus 5.5 2afebab175 refactor(terminal): copy-unwrap review follow-ups (#1149)
Copy Raw is a keymap action (default cmd-alt-c) instead of a chord
hard-coded in the key handler, and `raw` threads through
copy_contextual into the one copy path, which now owns the
Claude-pane policy. copy_unwrap.rs depends only on alacritty_terminal:
the boundary rule splits into a Block that tracks fences and tool
output and a pure `sep` with early returns. The fixture tests run
through a real Term at the fixture's width instead of a hand-built
row model.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:53:45 +08:00
l0ng-ai 1cefcc6a46 fix(fonts): turn stroke thickening off in gpui, not through AppleFontSmoothing (#1135)
* fix(fonts): turn stroke thickening off in gpui, not through AppleFontSmoothing

font_thicken: false pinned AppleFontSmoothing to 0 in the process's
argument domain. That never left the process, but AppKit reads the same
preference, so tty7's native menus and alerts lost their smoothing too,
most visibly dark text in light mode. The terminal's own dark text looked
unchanged, since gpui never thickens it in the first place.

The gpui fork now exposes set_glyph_dilation, which reaches its
rasterizer alone; apply_font_thicken calls that and writes no
preference. A test checks that turning thickening off leaves
AppleFontSmoothing alone (red on the old implementation).

Fixes #1119

* test(fonts): keep the font_thicken test out of config_reload_tests' doc comment
2026-10-09 11:30:39 +08:00
l0ng-ai e166f82ced fix(sftp): say why SFTP would not start instead of "Timeout" (#1133)
* fix(sftp): say why SFTP would not start instead of "Timeout"

Every way an SFTP channel can fail before the protocol starts ended the
same way: the library's 10s request timeout, shown in the Files panel as
"sftp init failed: Timeout". Each has its own cause and fix on the server:

- sshd without a `Subsystem sftp` line answers the request with
  CHANNEL_FAILURE. russh's request_subsystem only sends the request, so
  the refusal went unread and the INIT was sent into a channel nobody
  was listening on.
- A `Subsystem` line naming an sftp-server that is not installed: the
  channel is accepted and then closes without a byte.
- A shell startup file that prints on non-interactive logins puts its
  text where the version reply belongs; read as a packet length it asks
  for gigabytes that never arrive.

The subsystem reply is now read (bounded at 20s), and the first bytes
back are checked to be the header of an SSH_FXP_VERSION packet before
the stream is handed to the library. Each case is reported at once with
what to change on the server; text ahead of SFTP is quoted.

Fixes #1126

* fix(sftp): let the probe's reason win over the library's end-of-stream error
2026-10-09 11:30:34 +08:00
l0ng-ai 273741319a fix(daemon): make a full-screen program repaint when its first paint left the ring (#1136)
A long-running full-screen program paints its whole screen once, after
?1049h, and only differences from then on. Once the replay ring has
dropped that first paint, a reattaching client is put into the alternate
screen and handed nothing but differences from a screen it never saw,
painted onto an empty one.

On attach and observe, when the pane is in the alternate screen, the ring
no longer carries the switch into it, a program owns the pane and the
pane is alive, step the pty one row away and back 50 ms later. The
program hears a size change and repaints in full. Only the pty moves:
the ring keeps its size and no client or lease sees a Size, and the size
put back is read when it is put back. One nudge per pane per 2 s.

Fixes #1122
2026-10-09 11:26:20 +08:00
l0ng-ai b54bf5d5a3 fix(terminal): report the wheel at the pointer and keep the IME anchored to the caret (#1134)
A wheel report carried the cell of the last button press: only a click,
or motion for programs that ask for every move, ever updated it. tmux asks
for button events only (1002), so after clicking its window list every
notch over the panes was reported as being over the window list. The
wheel now resolves the cell under the pointer from the grid geometry of
the last frame.

On Linux the IME candidate window only moves when the caret rectangle is
pushed to the platform, and gpui pushes it on focus and around a preedit,
never because the caret moved. A TUI moves the caret on its own, so the
candidate window stayed where the last composition or the focus-in left
it. The pane now invalidates the IME coordinates whenever its anchor
moves while focused.

Fixes #1131
2026-10-09 11:15:46 +08:00
2f79dccd5b feat(terminal): copy rejoins the lines Claude Code wrapped itself (#1141)
* feat(terminal): copy rejoins the lines Claude Code wrapped itself

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

* fix(terminal): keep copy_unwrap fixtures LF on Windows checkouts

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

* refactor(settings): build the join-wrapped row with its Selection & Clipboard siblings

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-10-09 11:10:27 +08:00
l0ng-ai b50daebe0b fix(windows): keep ClearType text in an opaque window (#1132)
gpui only rasterizes subpixel (ClearType) glyphs when the window's
background appearance is Opaque. tty7 never asked for Opaque: with no
backdrop material the window was always Transparent (or the WCA blur),
even at the default opacity of 1.0 where nothing behind it can show
through. Every glyph on Windows therefore fell back to grayscale
antialiasing, regardless of the system ClearType setting.

On Windows, hand the window Opaque whenever the fill is fully opaque
and the resolved backdrop is plain translucency or the WCA blur. DWM
materials (Mica / Mica Alt / Acrylic) are left alone. Lowering the
opacity switches back to Transparent at runtime as before.

Fixes #1120
2026-10-09 11:09:07 +08:00
Adam HitchcockandClaude Opus 5.5 d9955df56b fix(settings): light-theme controls take the page's hue instead of white (#1137)
On a light theme Settings drew its dropdowns, buttons, menus, chips and switch knobs pure white, which stands out on a tinted page such as a cream theme. They are now the page colour lifted a step and made opaque, so they keep the theme's hue; a white theme still gets white controls, and dark themes are unchanged. The change is the `raised` colour in `settings/kit.rs`. `cargo test --bin tty7-app settings` gives 58 passed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:49:43 +08:00
Adam HitchcockandClaude Opus 5.5 45e5bde4ed fix(diff): a new or deleted file draws unified in the split view (#1138)
A new or deleted file has one side only, so in the split view half its card was empty. Such a file now draws unified whatever the view, and the others still split. A drag takes its mode from the row it starts on, so selecting and copying in one of these files works in either view. `file_mode` in `diff_list.rs` picks the mode, with tests; `cargo test --bin tty7-app diff` gives 90 passed, 1 ignored.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:49:39 +08:00
Adam HitchcockandClaude Opus 5.5 8229c28298 fix(titlebar): the traffic lights sit as far in from the left as from the top (#1139)
The lights sat 9pt in from the left edge, the inset of a plain 28pt title bar, which crowds them into the corner of the 48pt bar. AppKit's own toolbar windows inset them as far from the left as from the top: measured on macOS 26, the close button sits 19pt in and 19pt down in a 52pt unified toolbar, and 12pt in and 13pt down in a 40pt compact one. `traffic_light_position` now uses the top gap it already derives (16.5pt) for `x` as well, and its test asserts the two match. The lights still end well inside the tab strip's 80pt lead. `cargo test --bin tty7-app traffic` passes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:49:35 +08:00
l0ng-ai a05b371aee fix(ssh): a link whose peer stops answering is noticed and redialled (#1129)
* fix(ssh): a link whose peer stops answering is noticed and redialled

A connection whose peer went silent with the TCP socket still established
(a stalled path, a host that rebooted under it) was never noticed: nothing
is sent on an idle link, russh waits on a channel confirmation forever, and
the cache kept handing the connection out. The machine read "connected",
the GUI sat on "Reconnecting…" indefinitely, routed CLI calls hung, and
Retry Now dialled nothing new.

- Every connection now gets an SSH keepalive (15s, 3 misses) unless its
  profile sets its own interval or turns it off with 0.
- Channel opens wait at most 20s. A connection that does not answer in time
  is marked dead, so the next open dials a fresh one; what already runs on
  it is left alone. The remote probes' exec requests are bounded too.
- When direct-streamlocal is refused on a connection whose server came from
  the per-connection note, the note is dropped and the server proved again
  (which starts it) before falling back to a per-link stdio server. The note
  outlives a server that exited, or a host that rebooted, while the link
  stayed up.

* fix(ssh): a slow forward target or a failed re-prove no longer costs the link

A direct-tcpip open is answered only once the server's own connect to the
target settles, which for a target that drops SYNs outlasts the 20s open
bound on a healthy link. Marking the connection dead there stopped every
forward on it and sent the next pane to dial and authenticate afresh.
direct-tcpip is unbounded again; the keepalive catches a dead peer.

A re-prove of a stale server note that fails now falls back to the
per-link --stdio server instead of failing the link.
2026-10-08 18:07:55 +08:00
l0ng-ai 9b2c571fe1 fix(settings): put the password and key passphrase back on the SSH host editor (#1130)
* fix(settings): put the password and key passphrase back on the SSH host editor

The rebuilt settings window drew the host form from scratch and left out
the credential rows. Everything behind them survived — the form still
read the keychain on open, counted a typed secret as a change, and
filed it on save — but with no box on screen there was no way to give a
host a password short of connecting and waiting to be asked.

The editor now shows a masked Password row (with a reveal toggle) after
Port, and a Key passphrase row after IdentityFile. Each appears only when
the host's auth method would actually use it, the same split the
connection path makes: Password for Auto/Password, the passphrase for
Auto/PublicKey once IdentityFile names a key that exists on this machine.

* test(settings): the SSH editor's credential rows follow the auth method
2026-10-08 18:06:21 +08:00
l0ng-ai ad7efe5889 fix(switcher): make the error band's Try Again and Dismiss do something (#1127)
The band falls back to the reconnect supervisor's last error, which
neither button cleared: Dismiss left it on the next frame, and Try Again
started a second connect of its own while the stale reason kept hiding
the Connecting state.

- Try Again on a supervisor failure asks the supervisor to dial now
  (backoff reset) instead of racing it with another connect.
- While a connect is in flight the band says Connecting, not the old
  reason.
- Dismiss silences that exact failure on the supervisor; a different
  one is shown again, and the link coming up forgets it.
- A dismissed parked-route notice stays quiet instead of resurfacing as
  the error band, and the band offers no Try Again on a parked route.
- A failed Close on an orphan pane shows why on its own line.
2026-10-08 17:57:19 +08:00
ZuoXinyuandDu Leilei d72b9fa267 fix(ui): raise the tab-number badges on the tab chord's modifier (#1128)
The badges were revealed on a bare secondary modifier — ⌘ on macOS, Ctrl
everywhere else — whatever `ActivateTab1`…`ActivateTab9` were bound to. Off
macOS the nine ship on Alt, so holding Alt raised nothing while Ctrl, which
switches no tab there, lit up every number; and a tab row rebound onto Alt, the
case reported, still had to be held with Ctrl to see the hint.

The hold is read from `ActivateTab1` now, so it follows the binding and is
matched exactly. An unbound action, a chord with no modifier, a key that is not
the digit the badge draws, and the tmux preset's `prefix 1` raise no badges at
all rather than promising numbers no key answers.

Co-authored-by: Du Leilei <duleilei@sailyond.com>
2026-10-08 17:49:50 +08:00
l0ng-ai 7b375787f0 fix(sync): a tab missing from the window is never closed on the machine (#1125)
A Full sync used to read every machine tab the window was not showing as
a close. Missing is not closed: a window is missing tabs whenever it is
emptied (Restart Server clears it for the whole handoff), a rebuild fails,
or state outlives a visit. Each of those turned into a plain TabClose for
every tab, nothing remembered, shells left running as orphans, and the
now-empty workspace was then removed as disposable. Restart Server did
exactly this to a workspace of sixteen agents: any delta during the
handoff synced the emptied window.

#554, #579, #672 and #716 each fenced off one way of reaching that diff.
This removes the inference instead. close_tab_inner, the one path every
user close goes through, records the intent; a sync closes only tabs with
a recorded intent and holds the rest where they are.
2026-10-08 17:14:23 +08:00
l0ng-ai e80bde755b chore(mobile): 0.1.6 (#1123) mobile-v0.1.6 2026-10-07 22:50:59 +08:00
kalpakprodandkalpak e7e53e69c5 fix(tabs): a title that only swaps its status mark no longer repaints the window (#1114)
An agent that animates its title (Claude Code turns a circle one quadrant
at a time) sends a new OSC title every few hundred milliseconds. Each one
rebuilt the whole window for a tab that reads the same, because
TabView::label() strips the mark before drawing it.

Two places now read "same title" the way label() does (trimmed, status
mark stripped, via tab_view::same_title_ignoring_status_mark):

- settle_title treats a frame that only changes the mark as a Revert, so
  it queues no 400 ms wait and ends in no cx.notify() on the pane.
- on_layout_delta asks the machine mirror, before applying a PaneFacts
  delta, whether the pane's record differs from the mirror's by anything
  but such a title, and skips apply_layout_delta (whose only effect for
  PaneFacts is the notify of the root view) when it does not. The mirror
  is still updated and sync_window still runs.

A title whose words change lands as before, so a ticking counter in a
title repaints as often as it did.

Co-authored-by: kalpak <307643502+kalpakprod@users.noreply.github.com>
2026-10-07 22:08:58 +08:00
l0ng-ai ac4b356859 fix(terminal): restore kitty keyboard flags the replay ring dropped (#1074) (#1103)
A program that asks for the kitty keyboard protocol pushes its flags once,
at startup. A long session pushes that push out of the 8 MB replay ring, and
the mode tracker that re-sends what the ring lost only knew DEC private
modes, so a client that reattached rebuilt the pane with no keyboard flags
and sent Escape as a bare ESC to a program still parsing the kitty form.

TerminalModes now keeps the keyboard flag stacks the way alacritty does: one
per screen, swapped on ?1049, reloaded from the top on every push, pop and
swap, with CSI = changing only the flags in effect. They cannot be restored
as the state they ended in, because the pushes and pops the ring still holds
act on the stack relative to where it stood, so the ring folds every evicted
byte into a head tracker and the replay prelude rebuilds the stacks as they
stood at the ring's front.
2026-10-07 21:52:31 +08:00
l0ng-ai 83ef561a36 fix(mobile): keep the connection across a screen lock (#1104)
Back from the background the app now asks the machine whether the
connection still answers (Session::answers, up to 3s) and keeps its
streams when it does, instead of closing the pane and replaying it every
time. A dead connection is closed and dropped so the next dial starts
fresh rather than waiting out the idle timeout on it.

A dropped connection is retried quietly under the screen that is up; the
"Reconnecting" banner comes after 3s, or at once if a keystroke was lost.

iOS asks for the background task grace period on leaving, so a short
lock comes back to the same connection. Android runs a foreground
service (specialUse, with a "Connected to <host>" notification and a
partial wake lock) while a machine or pane screen is open.
2026-10-07 21:33:21 +08:00
l0ng-ai 4140214c9f fix(daemon): a phone keeps updating on a pane with a long history (#1105)
* fix(daemon): credit a replay back, so a phone keeps updating on a long pane

An observer was charged for the whole replay ring when it subscribed, but
the writer only credited Output, Image and ClipboardWrite back, never the
Snapshot frames the replay is made of. The charge stayed for good. A full
ring (8 MiB) is the observer's whole budget, so the first byte of live
output pruned it, and with the refusal sender still holding the channel
open nothing told the gateway: the phone showed the screen it opened on
and never updated until the pane was reopened.

Charging and crediting now share one measure, gated_len, and the replay
reports what it queued. A controller's gate starts at its replay too, set
under the state lock instead of reset after it.

* fix(daemon): room for a full replay, and an observer dropped is let go

Crediting the replay was not enough on a busy pane. The observer budget
was exactly RING_CAP, so while a full ring's replay was still being
written, one byte of live output pushed the gate past it and the observer
was dropped. With a 1 ms ticker in the pane this happened on every open.
The budget is now two rings: the replay, and as much again of live output.

A dropped observer also went silent. stream_observer kept a clone of the
channel's sender for refusals, so dropping the pane's sender never closed
the stream; the client sat on a connection nothing would be sent down
again, and a lease it held was never released. Refusals now go through the
pane (tell_observer), the pane holds the only sender, and a dropped
observer is sent a last Error saying why before its stream ends. The phone
reads that Error as a reason to reconnect.
2026-10-07 21:32:49 +08:00
0x38e 0ff68b5172 fix(sidebar): keep button size and position when toggled (#1099)
* fix(sidebar): keep button size and position when toggled

* fix(sidebar): use unfilled icons when collapsed

* docs(changelog): link the sidebar fixes to PR 1099
2026-10-07 21:28:27 +08:00
63bf3b885c perf(github): revalidate the GitHub panel's reads by ETag (#1117)
* perf(github): revalidate the GitHub panel's reads by ETag

HttpTransport keeps each plain GET's last answer and ETag and re-asks
with If-None-Match; a 304 replays the stored answer. A signed-in 304 is
not counted against the rate limit, so revalidations and polls stop
draining the hourly allowance the panel shares with gh. get_full is not
cached because its signed attachment URLs expire.

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

* fix(github): replay the answer that went with the ETag that was sent

The tag and the stored answer were read under two separate locks. If the
cache started over (or the entry was replaced) between them, a 304 found
no answer, fell through to classify() and surfaced as HTTP 304 in the
panel. Read both as one snapshot and replay that on a 304.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-10-07 21:22:53 +08:00
77760989a0 perf(github): a pull request's detail fetches its independent requests in parallel (#1115)
* perf(github): a pull request's detail fetches its independent requests in parallel

The detail sent its seven requests one after another. Requests that do not
depend on each other now overlap on scoped threads: issue and comments, then
the pull request (whose head sha the checks need) alongside reviews and files,
so a pull request's detail is three round trips. `checks` sends check-runs and
status together. Same request count.

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

* test(github): hold each request until the overlap shows, not for 30 ms

A fixed 30 ms per request only shows three in flight if the threads all
start within that window, which a loaded CI runner does not promise. Each
request now waits until three are in flight (a second at most), so the
test passes on any machine and still fails when the requests go one by
one.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-10-07 21:22:41 +08:00
l0ng-ai e9d6e6641e feat(mobile): name machines as the OS does, and recognise a re-paired computer (#1097)
A phone listed two "Mac"s with nothing to tell them apart: the installed
tty7 and a dev build on the same computer, each with its own key. The one
whose key nothing answered to any more just sat on "Connecting…".

- The gateway names itself by macOS's computer name ("thomas's Mac mini")
  rather than gethostname(), which is whatever the router's DHCP handed out
  ("Mac.lan") and changes with the network. Elsewhere it is unchanged.
- Pairing codes carry an optional machine fingerprint: a SHA-256 of the
  OS's machine id (gethostuuid, /etc/machine-id, MachineGuid), so the id
  itself never leaves the machine. Every tty7 on one computer gives the
  same one; older apps ignore the field, older desktops omit it.
- The app keeps the fingerprint and the pairing time with each machine.
  Pairing a computer it already holds under another key offers to replace
  the older entry ("Keep both" for more than one tty7 on it), and machines
  that share a name show when each was paired.
2026-10-07 21:01:47 +08:00
l0ng-ai 591368d80b fix(pane): don't hand the AppImage runtime's ARGV0 to the pane (#1100) (#1111)
The AppImage runtime exports `ARGV0` (the path the image was launched by)
into tty7's environment, and every pane inherited it. zsh treats an
exported `ARGV0` as the argv[0] of each external command it runs, so in a
pane every program was told it was `tty7-….AppImage`. Volta's shims choose
the tool from argv[0], so `node`, `npm` and every global package failed
with `Could not find executable "tty7-….AppImage"`.

Turning shell integration off would not have helped. `env -u
TTY7_SHELL_INTEGRATION node` only worked because `env` execs node with a
correct argv[0], and `env -u` with any variable name works just as well.

Panes now start without `ARGV0`. The removal happens before the configured
`env` block is applied, so an explicit setting there still wins.

Reproduced on Linux (Debian arm64, zsh, Volta 2.0.2) with tty7-server
started under the environment an AppImage launch leaves behind: the shims
fail with the line removed and work with it in place.
2026-10-07 21:01:32 +08:00
l0ng-ai 739a7769dc Revert "feat(mobile): drop the keyboard key beside the message box (#1096)" (#1121)
Brings back the round keyboard key beside the message box. Only the
mobile half of 41a247a1 is reverted; the pid-column removal from #1095
that rode along in the same squash stays.
2026-10-07 20:54:58 +08:00
l0ng-ai cd64038843 fix(panel): drop the pid column from Info → Processes (#1095)
The pid sat in a column of its own on every row, beside CPU% and memory.
It reads as a port number at a glance and is not what the list is read
for; the row's name, CPU% and memory already say which process is which.
2026-10-04 22:00:05 +08:00
l0ng-ai dc441be048 feat(mobile): a long press on the pane opens Select text (#1098)
Held still for half a second, a finger opens the plain-text page the
Select text menu item opens, with the run of non-blanks under it already
selected and brought into view, as a long press selects text anywhere on
the phone. A swipe, a pinch or a lift before then is what it was.
2026-10-04 20:42:53 +08:00
l0ng-ai 41a247a14f feat(mobile): drop the keyboard key beside the message box (#1096)
* fix(panel): drop the pid column from Info → Processes

The pid sat in a column of its own on every row, beside CPU% and memory.
It reads as a port number at a glance and is not what the list is read
for; the row's name, CPU% and memory already say which process is which.

* feat(mobile): drop the keyboard key beside the message box

A tap on the pane already brings up the keyboard for typing straight into
the terminal, so the round key that did the same only crowded the compose
row.
2026-10-04 20:42:47 +08:00
0x38eandCopilot App fe80363e2a fix(titlebar): centre the traffic lights on the chrome row (#1085)
`y` in `traffic_light_position()` is the gap between the window's top edge
and the top of the button frame, and AppKit draws those buttons 14 points
tall, so the lights' centre lands seven points below it. The tiles beside them
are centred in the title bar's content box instead — `TITLE_BAR_HEIGHT` less
the hairline `TitleBar` draws along its bottom edge — which is 23.5 in a 48pt
bar. The constant was still 17, the value the 40-point bar needed (13)
rewritten by hand when the bar grew to 48, without that border in it, so the
lights' centre came out at 24: half a point low, one whole device pixel on a
Retina panel.

It comes from the bar's own height now — `(48 - 1 - 14) / 2 = 16.5` today —
and a test holds the lights to the tile line, so the next change of bar height
has somewhere to move with it.

Measured on macOS 26.3 with the style mask gpui uses
(`fullSizeContentView` and a transparent title bar): the standard buttons are
14×14 points with 9 points of padding, in a 32pt title bar.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-04 17:15:12 +08:00
l0ng-ai 0e22201ee6 chore(mobile): 0.1.5 (#1094) mobile-v0.1.5 2026-10-04 16:31:05 +08:00
l0ng-ai c84f761783 fix(mobile): read frames cancel-safely so a slow link can't desync a stream (#1093)
read_frame awaits the header and the payload separately and keeps nothing
between them, so dropping it mid-frame discards the bytes it has read and
leaves the stream inside a payload. The next read takes terminal output for
a length: "Frame of 2086478377 bytes exceeds the limit" is `)"]|` off a
pane running jq, seen on a phone over 4G.

Three callers drop it routinely:
- the phone app batches output with timeout(16ms, PaneReader::next), which
  fires whenever a large frame is still arriving on a slow link;
- the gateway's control and pane loops race it in select! against outgoing
  events and output, so a paste while a pane prints can lose input bytes.

Add FrameReader to tty7-mobile-proto: it keeps a partial frame in a Decoder
and only awaits a single read(), which takes nothing when dropped. Use it in
PaneReader, ControlReceiver and both gateway loops. read_frame stays for the
one-shot reads whose stream is abandoned on a timeout, and says so.
2026-10-04 16:17:40 +08:00
Adam HitchcockandClaude Opus 5.5 6b73242b1c test: history and commit-detail tests ignore the developer's shell history and git config (#1089)
Nine tests failed locally but passed in CI. The `ui::scm::detail` tests
ran `git tag` under a global `tag.gpgsign true`, which then wants a
message; their git helper now ignores global and system config. The
`terminal::history` tests read the real `HISTFILE`, whose newer entries
pushed the tests' own past `MAX_ENTRIES`; shell history isn't loaded
under `cfg(test)`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:12:46 +08:00
Adam HitchcockandClaude Opus 5.5 4ed2685d1e test(daemon): the reap-guard test spawns /bin/sleep; singleton tests dup with CLOEXEC (#1090)
* test(spawn): the reap-guard test spawns /bin/sleep

With GNU coreutils first on PATH, `sleep` resolves to `gsleep`, whose
executable name the guard then (correctly) refuses, so
`reap_guard_rejects_a_live_process_of_another_executable` failed on
such machines. It now spawns `/bin/sleep`.

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

* test(singleton): dup the seat descriptor with CLOEXEC

Two singleton tests `dup` the seat's descriptor to simulate a lingering
reference. A plain `dup` isn't close-on-exec, so a child another test
spawns in that window (the reap test's `sleep 30`) inherits it and holds
the flock for its whole life, failing the clear under the full parallel
run. `F_DUPFD_CLOEXEC` keeps the reference in-process.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:12:31 +08:00
Adam HitchcockandClaude Opus 5.5 b65a3e4efc test(git): pin status.showUntrackedFiles in the tests' scratch repositories (#1091)
Five tty7-core tests and two tty7-server conformance checks failed on a
machine with a global `status.showUntrackedFiles=no`: git hid the files
they had just written. PINS (and so pin_repo_config) now carries
`status.showUntrackedFiles=normal`, the worktree tests' temp_repo pins
its repository, and the host conformance suite's git_repo sets the key
itself, since other crates run it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:12:16 +08:00
l0ng-ai e60bf9f12c fix(mobile): resolve relays behind DNS that sets an EDNS Z flag (#1092)
simple-dns 0.12.0 reads the EDNS OPT record's TTL at the wrong offsets: it
takes the extended RCODE from the low byte, where RFC 6891 puts the DO/Z
flags. A resolver that sets a Z bit -- a Clash-style fake-IP DNS on the
router does -- turns every NOERROR answer into RCODE 16, so iroh resolves
nothing. The gateway never reaches a relay, pairing codes carry none, and
a phone off the LAN cannot connect ("It didn't answer").

Vendor simple-dns 0.12.0 with the offsets fixed and patch it in for both
the desktop and the mobile workspace. tests/edns_z_flag.rs drives iroh's
resolver against a nameserver that sets the flag, from the root package so
CI's default `cargo test` runs it and fails if the patch stops applying.
2026-10-04 16:10:36 +08:00
l0ng-ai 54978409c2 chore(mobile): 0.1.4 (#1088) mobile-v0.1.4 2026-10-04 08:27:41 +08:00
l0ng-ai c5fe474125 fix(mobile): typing into a pane shows its cursor and what is being composed (#1084)
Tapping the terminal to type into it showed no cursor: xterm reports its
cursor as shown and in place, but leaves it undrawn on the phone. And an
input method's composing text — pinyin before a candidate is picked,
dictation before it is done — lived only in the hidden field typing goes
through, so nothing showed until it was committed, often only on Return.

While the pane has the keys, the app draws the cursor itself, a blinking
block at the pane's cursor cell, with the composing text underlined in
front of it. A program that hides the terminal's cursor to draw its own
(an agent's input box) gets only the composing text, where it lands.
2026-10-04 08:27:08 +08:00
d3f7dadd5d feat(agents): Muse Code and jcode support, native hooks for Empryo, Muse Code and jcode (#1071)
* feat(agents): recognise Muse Code and jcode

Muse Code's `muse` launcher execs a versioned `muse-bin-<version>`
binary, so detection also maps that name to Muse. Muse resumes with
`muse resume <id>` (root options on either side of the subcommand);
jcode with `jcode --resume <id>`.

* feat(agents): install native Empryo and jcode hooks

* feat(agents): add jcode lifecycle hooks

* fix(agents): preserve jcode hooks and correct Empryo removal contract

* feat(agents): install Muse native plugin hooks

* docs(agents): list Empryo, Muse Code and jcode as hook-backed

* fix(i18n): translate Empryo, Muse Code and jcode search keywords

* fix(agents): read jcode hook payload from env, route its reports like Codex, bound muse CLI calls

jcode runs hooks with stdin closed and puts session_id/cwd in
JCODE_HOOK_PAYLOAD, so reports carried no session and resume never
learned an id. Its shared server also runs every session's hooks with
the first client's TTY7_PANE, so reports go through the same
session/directory routing as Codex. muse plugins calls now time out
after 30s and surface stderr. Adds ru strings for the new keys and
reattaches Kimi's doc comment to KIMI_HOOK_EVENTS.

* docs: count Muse Code and jcode in the README agent totals

---------

Co-authored-by: kalpakprod <307643502+kalpakprod@users.noreply.github.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-10-03 20:47:25 +08:00