Commit Graph
1537 Commits
Author SHA1 Message Date
l0ng-ai f85c0fee76 fix(mobile): number TestFlight builds in UTC (#1050)
A laptop in UTC+8 numbered its builds eight hours ahead of CI, so a CI upload of the same version within eight hours of a local one came out lower and would be refused.
2026-09-30 19:32:02 +08:00
l0ng-ai 0a47cada12 ci(mobile): release iOS and Android together from one tag (#1048)
A `mobile-v<x.y.z>` tag now ships both platforms at one version: the
Android APK to a draft release as before, and an iOS build to
TestFlight. iOS no longer depends on one machine signed in to Xcode.

- The version is tauri.conf.json's; a tag that disagrees with it fails
  the run, so the repo always says what shipped. Set to 0.1.1, the
  Android build already out.
- scripts/testflight.sh signs and uploads with an App Store Connect API
  key when ASC_KEY_ID and ASC_ISSUER_ID are set, with a certificate
  Apple keeps in the cloud. That needs an Admin key.
- A manual run builds both and uploads nothing.
2026-09-30 18:20:50 +08:00
l0ng-ai 0646dc8b82 ci(mobile): match the signing digest whatever apksigner calls the signer (#1047) mobile-v0.1.1 2026-09-30 17:45:00 +08:00
l0ng-ai 399ecdf777 docs(readme): lead with three pillars instead of a feature list (#1046)
Restructure both READMEs around what tty7 is for: shells that outlive the
window, agents that drive other agents, and remote machines that work the
same way because they run the same server. Performance becomes a short
supporting section with concrete numbers; the rest of the feature list
collapses into one paragraph that links to the docs.

Also:
- Correct the persistence claim: shells survive quitting the app, not a
  reboot; after a reboot the layout, the screen tail, and supported agent
  conversations come back.
- Add a past-sessions column to the agent matrix, matching the agents
  agent_history::scan reads.
- Drop the wrapper-script / agent_launch paragraph (already in the
  configuration reference) and the separate full-quality video link.
- docs/window/search-everywhere.mdx: the Sessions tab has listed every
  supported agent, on remote workspaces too, since #977; the page still
  described only Claude Code and Codex on this computer. Also document the
  Cmd/Ctrl-E row actions.
2026-09-30 17:40:14 +08:00
l0ng-ai c834e728ce ci(mobile): keep the NDK install from failing on SIGPIPE (#1045) 2026-09-30 17:25:06 +08:00
l0ng-ai 0c2033ab07 feat(mobile): run on Android, and release a signed APK (#1044)
* feat(mobile): run on Android

The app builds and runs on Android, and fits there.

- Back button and gesture close what is open over the screen, then go
  back a screen, and from the first one send the app to the background.
  Left to the WebView they closed the app, since it has no history.
- The system bars: the page asks their size of the app (`insets`, over
  JNI), since WebViews before 140 report the safe areas as 0 even edge
  to edge. The CSS reads them as `--inset-*`, which iOS still fills
  from `env()`.
- The keyboard: edge to edge, the WebView is not resized for it and its
  visual viewport stays whole, so the keyboard covered the message box.
  MainActivity hands its height to the page, which lays out as on iOS.
- The title folds into the bar from the scroll position on every screen.
  The observer it used reported a title in plain view as out of sight on
  Android, so the bar started folded.
- src-tauri/gen/android is kept in the repo for that MainActivity.

* ci(mobile): release a signed Android APK

A `mobile-v<x.y.z>` tag builds the app for arm64 at that version, signs
it with the release key and attaches it to a draft release. The mobile
app is versioned apart from the desktop's `v*` tags.

- The release is never marked latest: the desktop updater reads
  /releases/latest and would take it for a desktop release.
- The APK's certificate is checked against the release key's, since one
  signed with another key could not be installed over earlier ones.
- The NDK is pinned, and the version must be x.y.z: Tauri derives the
  versionCode from it, and Android installs over a build only when that
  is higher.
- Gradle signs a release build when keystore.properties names a key;
  CI writes it from secrets.
- The release library is stripped: 30 MB to 20, the APK 32 to 23.

* ci: tell people how to install the phone app

The nightly release notes and each mobile-v release now say how to get
the app on a phone: the TestFlight link for iPhone, and the newest
mobile-v release's APK for Android. Both read .github/mobile-install.md.
2026-09-30 17:17:06 +08:00
l0ng-ai d40233b572 feat(tabs): keep recently closed tabs in the machine tree, and a confirm-before-closing setting (#1037)
* feat(tree): keep each workspace's recently closed tabs in the machine tree

A workspace now keeps the tabs closed from it in its machine tree
(`Workspace::closed`), so reopening one no longer depends on the window
that closed it still being open.

- `TabCloseRemembered` closes a tab into that list: the tab leaves the
  workspace as a close takes it (other windows hear `TabClosed`), its panes
  are stopped with their last screens kept on disk, and their records stay
  in the pane list. Panes the client held that the tree never recorded are
  ended outright.
- `TabReopen` takes the named or newest entry off the list and answers it
  with its pane records, for the client to rebuild on fresh shells that
  open on the old screens. The records it leaves behind are claimed by the
  successors' seeds instead of being refused as duplicates.
- Entries last 24 hours and a workspace keeps the newest 20. The daemon's
  scrollback keeper expires them on its existing timer, and an entry that
  goes takes its records and stored screens with it.
- Both requests are gated on a new `closed-tabs` hello feature, offered only
  by a peer that serves a tree and panes, so an older daemon or remote
  server is never sent them.

The field is `#[serde(default)]` and skipped while empty, so older trees
load unchanged and an older build reads this one's.

Refs #1021

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

* feat(tabs): reopen closed tabs from the machine, and a setting for when closing asks

Closing a tab now closes it into its workspace's recently-closed list on the
machine that holds the workspace, instead of into a list that ended with the
window. ⌘⇧T asks that machine for the newest entry, so a closed tab comes
back after quitting and relaunching the app, from any window of the
workspace, and in remote workspaces whose server keeps the list.

- The explicit close (⌘W, the tab's close button, bulk closes) registers the
  tab with the window's next sync, which turns that tab's `TabClose` into a
  `TabCloseRemembered`. The machine stops the panes and keeps their screens;
  the window no longer kills them itself. A tab dragged to another window or
  never rebuilt still goes out as a plain close.
- If the close cannot reach the machine (no `closed-tabs` feature, a window
  that has not pulled its layout, a sync that fails or is thrown away), the
  window falls back to what it did before: the tab goes on its own list and
  its panes are killed from here.
- Reopening waits briefly for this window's queued edits to land, so an
  immediate ⌘⇧T undoes the close it means, then rebuilds the tab through the
  existing restore: each pane a fresh shell in its old cwd, opening on its
  last screen, with its shell, SSH target and agent resume facts. The tab
  keeps its id. The window's own list is used when the machine has nothing.
- The home screen offers the next tab ⌘⇧T would reopen, read from the
  machine mirror, which now tracks this window's remembered closes.

A new setting, `confirm_close` (General > Tabs, "Confirm before closing"),
decides when closing a tab or pane asks first: `never`, `when-busy` (the
default and the old behaviour) or `always`. The SSH "Warn before closing"
opt-in is honoured under every mode. Under `always`, Close Other Tabs and
Close Tabs to the Right ask once for the whole batch.

Refs #1021

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

* fix(tree): a remembered close marks its panes' records as no longer live

The records stay in the pane list for the reopen, but the panes are stopped
right after the close. Left at live: true, `tty7 wait` on a pane of a
closed tab read it as a running agentless shell and waited forever instead
of reporting it exited.

Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
2026-09-30 16:40:28 +08:00
l0ng-ai f0f2b83b6e feat(workspace): the machine's own window takes part in workspace takeover (#1042)
One workspace is driven by one window at a time, whether that window runs on
the machine itself or on a remote client. The local GUI used to connect to its
daemon without ever claiming a workspace, so a remote client and a local
window could drive the same panes at once and fight over their size.

- The local link now claims (WorkspaceAttach) every local workspace a window
  shows. Opening one on purpose (switcher, tty7 open, a new window onto it,
  switching in place) takes it over; restore, relaunch and reconnect only
  claim a workspace nobody else holds, and otherwise open taken over.
- Preempted events for this computer put the window in the same taken-over
  state remote workspaces use: panes detached (processes untouched), the
  status strip naming the holder, the input pill, Take Back, and the
  switcher's 'taken over' badge.
- A hydration of a local workspace another client holds does not attach its
  panes, so a restored window never resizes them under the holder.
- A reconnect re-claims what the window held and leaves what it was pushed
  off alone. Nothing but Take Back or an explicit open reclaims.
- The local hello carries the machine's hostname instead of the literal
  'this computer', which is what a displaced remote client displays.
2026-09-30 16:36:10 +08:00
l0ng-ai 53f661e1a1 feat(remote): let a remote workspace target a Windows host over SSH (#1041)
A remote workspace refused any machine that was not Linux or macOS. This
teaches the installer and the link to reach a Windows OpenSSH host:

- Detection: `uname -sm` is still asked first, unchanged. When it fails
  (cmd.exe / PowerShell) or answers from Git for Windows, MSYS2 or Cygwin,
  a PowerShell probe reads PROCESSOR_ARCHITECTURE. The detected platform
  must match the SFTP home's shape, so a WSL DefaultShell over Windows
  SFTP is refused instead of installing a Linux server at /C:/...
- Assets: tty7-server-windows-{x86_64,aarch64}.exe, verified against
  checksums.txt like every other server; built by new server-windows
  jobs in release.yml and nightly.yml (ARM64 leg non-blocking), and the
  CI vcruntime guard now covers tty7-server.exe.
- Install: %USERPROFILE%\AppData\Local\tty7\bin\tty7-server-cXpY.exe via
  SFTP (/C:/... spelling). No mode bits are required or set. A running
  image is renamed aside to free its name and swept on the next install.
- Commands: every Windows command is a PowerShell script sent as
  -EncodedCommand, which survives cmd.exe, PowerShell and bash as the
  DefaultShell. The daemon is launched through Win32_Process.Create so
  it outlives the SSH session's job object, with this session's
  environment handed over; restarts use a new `tty7-server --stop`.
- Link: a Windows server is always reached by session exec with a plain
  `"C:\...\tty7-server-cXpY.exe" --stdio` (no env probe, no
  stream-local forward).
- Server: `--stdio` (control and --pane) now bridges on Windows to the
  daemon's loopback TCP endpoints instead of refusing.

Tested against a fake Windows host in install/windows_tests.rs; not yet
run against a real Windows machine.
2026-09-30 16:36:04 +08:00
l0ng-ai 8efea021e3 feat(ssh): shell integration for Windows hosts whose default shell is PowerShell (#1040)
The remote shell probe only understood POSIX answers, so a native SSH pane
on a Windows OpenSSH host always fell back to a plain shell with no prompt
marks, cwd reporting or title.

The probe is now a polyglot every default shell can read: POSIX shells
answer as before; PowerShell answers with its edition and $PSHOME, which
name the exact executable; cmd.exe echoes the line back verbatim and is
recognised, then deliberately left alone. For PowerShell the bootstrap
starts a second, interactive PowerShell of the same executable with the
existing integration via -NoLogo -NoExit -EncodedCommand (comments
stripped, about 6.5K chars total, well inside the Win32 command-line
limit), and a redialled pane's start directory (/C:/..., C:/..., UNC) is
restored with Set-Location -LiteralPath inside the encoded script.
2026-09-30 16:35:59 +08:00
l0ng-ai 0701278fe9 fix(mobile): the list fades out under the floating search bar (#1039)
Rows scrolled under the floating bar showed through sharp and cut in
half in the gap beneath it. A scroll-edge fade from the bar down to the
screen's edge softens them into the canvas, as the system's own bars do.

Claude-Session: https://claude.ai/code/session_01HVosmYyVSH3BrsLYx3tcSS
2026-09-30 15:28:22 +08:00
l0ng-ai ed87bd39b6 fix(mobile): testflight.sh puts the tty7 icon back after ios init (#1038)
`tauri ios init` fills the asset catalog with Tauri's placeholder icon,
so a regenerated gen/ shipped build 0.1.0.9 with the wrong icon. Copy
src-tauri/icons/ios over it on every run.

Claude-Session: https://claude.ai/code/session_01HVosmYyVSH3BrsLYx3tcSS
2026-09-30 15:28:16 +08:00
l0ng-ai ab029f5ea1 fix(ssh): start a redialled SSH pane in the remote directory it was in (#1035)
* fix(ssh): start a redialled SSH pane in the remote directory it was in

A native SSH pane dialled again (restore after a daemon restart, Reconnect,
a split, ⌘T on an SSH tab, waking a sleeping tab) always landed in the login
directory. The client already sent the pane's remote cwd as
`SpawnNativeSsh.cwd`, but the daemon dropped it on the floor.

The daemon now threads it through `Pane::spawn_native_ssh` and
`SshManager::run_session` into the shell-integration bootstrap, whose first
line becomes `builtin cd -- '<dir>' 2>/dev/null` (fish-quoted for fish). It
is never typed at the prompt, never lands in history, and a directory that
is gone leaves the shell in the login directory without a word. Sessions
without integration take the plain shell request as before, so jump-host
menus never see it. The per-host probe cache still holds only the shell.
Only absolute paths are honoured. No wire or protocol change.

Callers now say what they mean: a saved host or quick connect passes no
start dir instead of the local cwd of whatever tab was in front; ⌘T and a
split on an SSH pane pass that pane's remote cwd; a sleeping SSH tab keeps
its remote cwd in the session layout. The daemon's replay now sends the
remote context before the cwd, so a window that reattaches to an SSH pane
does not wipe the remote directory it was just told.

Refs #1028

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

* fix(ssh): a local shell standing in for a restored SSH leaf starts locally

pane_to_session now keeps a native SSH leaf's far directory, so the
session_to_pane fallback that brings such a leaf back as a local shell
(the redial failed, or its daemon pane is gone on reattach) would hand
that remote path to a local spawn. Pass no cwd there for an SSH leaf.

Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
2026-09-30 15:02:22 +08:00
l0ng-ai 0277ca67dc Centre the title-bar search box; bring back auto-hiding title-bar buttons as an Appearance option (#1036)
* feat(settings): an Appearance option to show the title-bar buttons only under the pointer

26.9.2 painted the new-tab and sidebar tiles only while the pointer was
over the bar they sit in; 26.9.3 took that out so the buttons would stay
discoverable. Both are fair, so it comes back as a choice: Appearance >
"Show title bar buttons on hover" (`auto_hide_titlebar_buttons`), off by
default, which keeps today's always-visible bar for everyone who has not
asked otherwise. A config written before the key existed reads as off.

With it on, the rail's two tiles follow the rail, and the collapsed
rail's pair and the trailing panel toggle follow the tab strip. The
window mark stays put, the tiles keep their layout slot so a reveal
never shifts anything, and the title-bar search box is always drawn.
The trailing toggle stays painted while the detail panel is open, as
before, since the panel's own tab row beside it always is.

The reveal is the hover sheet from 26.9.2 — a transparent last child
over each region, because `group_hover` loses the region the moment the
pointer reaches an occluding tile. Resting tiles go to zero opacity
rather than `invisible()`: gpui skips a hidden element's paint pass,
which is where its click and accessibility actions are registered, so a
screen reader could find a hidden tile by label and then not press it.
Shortcuts are actions and never depended on the tiles.

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

* fix(titlebar): centre the search box on the window, not the bar after the traffic lights

On macOS `TitleBar` leaves an 80pt lead for the traffic lights before
the tab strip — whether or not the rail stands in front of them — and
the Search Everywhere box was centred on the strip. So it sat 40pt right
of the terminal column's middle: with the rail collapsed, 40pt right of
the window's (#1033). Fullscreen added the bar's own inset on top.

The band the box centres in now reaches back over that lead, so its left
edge is the terminal column's on every platform (12pt elsewhere). Its
right edge is unchanged: the strip's end on macOS, the terminal column's
end beside a docked panel or document off it.

Reaching back puts the collapsed rail's tiles inside the band, so the
box now keeps an equal clearance at both ends — two springs with a
minimum width either side of it. In a narrow column it shrinks instead
of sliding under New Tab or the panel toggle, and stays centred while
it does. The geometry is a pure `search_band`, tested for the macOS
rail-collapsed, rail-open and fullscreen cases and off macOS.

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

* fix(titlebar): clear a hover flag whose sheet is not on screen; document the key

A title-bar hover flag is written only by its sheet, and only when the
sheet sees the pointer cross its edge. Hide the rail from its own tile, or
turn the switch off mid-hover, and the sheet leaves the tree with its flag
still set: the next time it is built the tiles came back painted with
nobody pointing at them, until the pointer happened to pass through and
out again. Clear the flag of any sheet not built this frame.

Also list auto_hide_titlebar_buttons in the configuration reference.

Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
2026-09-30 14:59:53 +08:00
l0ng-ai c0c6f90284 fix(chrome): the workspace tile points like the switcher rows it opens 2026-09-30 14:14:35 +08:00
l0ng-aiandClaude 4c8c317b91 feat(mobile): files, changes, a custom key bar, and a steadier connection (#1029)
The phone can now hand a pane a photo or file, read what an agent changed,
answer its prompts with a tap, and keep its link across leaving a pane.

Gateway and protocol:
- `Open::Upload` puts a file (up to 20 MB) in the directory the desktop
  keeps pasted images in, under a name a shell takes unquoted, and answers
  with its path. Panes on this machine only for now.
- `Open::Diff` answers with the working tree's changes against HEAD and its
  untracked files, for the directory a pane is in.
- Both are one-shot streams; an older gateway drops them unanswered, which
  the client reports as "update tty7".

App:
- Attach button in the message box: the uploaded path goes into the draft.
- Changes sheet from a pane's menu: a block per file, lines coloured.
- Settings → Key bar: remove, reorder and add keys, from a catalog or
  written out (`/compact\r`, `^C`), across pages; reset to the default.
- An agent's numbered choices show as buttons while it waits.
- Pinch to zoom, tap a link to open it, find in the scrollback, a Copy
  button for text a program copies (OSC 52), and an optional Face ID lock.
- Swipe from the left edge to go back; the iOS WebView has no page stack.
- One workspace at a time on a machine, picked from a row of chips.
- Agent avatars in their own colours.
- Each screen's watch is its own: a second watch no longer ends the first,
  which showed as "Can't reach" and a reconnect after leaving a pane.
- Dialing no longer holds the session lock, so Forget works while a
  machine is still connecting; its confirmation is an in-app sheet, since
  `window.confirm` shows nothing in the iOS WebView.
- iOS 15 minimum, as App Store Connect will require from April 2027.
- `scripts/testflight.sh` archives, signs for the App Store at export (the
  team has no devices for a development profile) and uploads.

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-30 12:30:04 +08:00
l0ng-ai e1ee74a379 fix(tabs): ⌘T on an SSH tab stays on its host; Switcher says Offline in words (#1031)
* feat(switcher): say Offline in words and dim the avatar; drop the link dot for a reachable workspace

The normal case carries nothing extra. An offline machine's row reads
"Offline" under the tab count, where Open and This window go, and its
avatar is dimmed. A dot stays only for states that want attention:
connecting, reconnecting, failed, or taken over by another client.

* fix(tabs): ⌘T on an SSH tab dials the same host and files the tab under it

A new tab from an SSH pane used to open a local shell in the default
directory, which landed in Ungrouped instead of under the host the user
was on. A native SSH pane now dials again with its spec, as ⌘D already
did; a shell that ssh'd onward from a local prompt gets a local tab that
types the same ssh command at its first prompt. Either way the tab is
seeded into the host's auto group before its pane reports in.
2026-09-30 12:29:56 +08:00
l0ng-ai 8db94666f0 fix(remote): fast, stable remote workspaces with more than ten panes (#1034)
* fix(ssh): a connection at the server's session limit is full, not dead

sshd allows ten sessions per connection by default and every remote pane is
one of them. The eleventh was refused with ConnectFailed, and the connection
was marked dead for it: taken out of the cache while all ten panes on it were
still using it. Every pane after that dialled its own fresh link, paying a full
handshake and server probe, and so did every short-lived route afterwards,
because nothing held the replacement and it went away as soon as that route
closed.

The cache now keeps a pool per destination. A refusal marks that connection
saturated for a while instead of dead; the pool hands out the first live one
with room and dials another beside them only when all are full. A route or a
native SSH pane that lands on a connection that turns out to be full goes back
to the pool for another.

* perf(remote): prove a server that is already running in one round trip

Every new connection to a machine tty7 had been to before spent six
sequential round trips proving what it already had: uname, a home lookup and
a stat over SFTP, a control probe and a process scan. The SFTP session it
opened also stayed open for the life of the connection, one of the handful of
sessions the server allows it.

One `sh -c` script now answers the common case: this dialect's server is
installed, answers the bridge probe, and is the one running (or a build of the
same dialect). Anything else falls through to the full install path unchanged.

* fix(remote): don't replace a pane that could not be reached with a fresh shell

Reattaching a restored pane treated every failure other than silence as "the
pane is gone on its machine" and spawned a fresh shell in its place. That
includes the link never opening at all — the server refusing another session,
a transport error — when the pane is still running over there. The tab lost
its session, and the old shell was left orphaned on the remote daemon; one
workspace had accumulated 53 shells for 17 panes.

Only the daemon's explicit "no such pane" now means gone. For a remote pane,
any other failure leaves it pending, and a pane that stands in for a running
one retries on its own a few times (2s, 4s, 8s, 16s, 16s) before leaving it to
Try Again. Local panes keep their previous behavior.

* perf(remote): ask a machine which panes are live over its control link

The pane liveness probe opened a pane route every ten seconds, which is an SSH
session channel on the remote side — one of the few the server allows per
connection. Once panes had taken them all, each probe dialled a whole new
connection. Ask over the control link that is already up instead: the
machine tree's pane records carry the daemon's own liveness.

* fix(scm): stop two windows from trading one repository watch every frame

Who holds a repository open is a global, reconciled by every window every
frame, but it was keyed by watcher kind alone. Two windows whose file trees or
panels sat in different repositories took the hold from each other each
frame, and every hand-over dropped the watch and opened it again. On a remote
workspace that is four round trips per cycle — about 28 control requests a
second for as long as both windows were open.

Holds are now keyed by window as well, and a closed window gives back what it
held.

* perf(ssh): open channels on one connection concurrently

The russh handle sat behind a tokio mutex held across every request on it,
and each request waits a full network round trip for its reply. So every
pane on a connection opened its channel after the one before it: on a link
with a few hundred milliseconds of latency, a workspace of twenty tabs came
back one tab at a time over about ten seconds.

Every call on the handle takes `&self` and waits on a reply channel of its
own, so the lock bought nothing. Drop it.

Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
2026-09-30 12:29:48 +08:00
l0ng-ai b32cca8266 fix(ui): right-aligned text no longer draws through the hover action buttons (#1030)
Bump the gpui fork to paint an aligned line into a layer that covers its
glyphs. The layer was sized from the unaligned origin, so the Changes panel's
flush-right directory shared a draw order with the opaque action strip painted
over it, and sprites draw after quads within one order.
2026-09-30 10:07:08 +08:00
b26c26ca66 fix(palette): an open palette keeps workspace keys, and ⌘P answers from the Settings window (#1026)
* fix(palette): an open palette keeps workspace keys, and ⌘P answers from the Settings window

While the palette was open, any app binding it did not handle — ⌘1–9,
⌘D, ⌘W — fell through to the workspace behind it. The Settings window
has no palette listener, so the palette chord did nothing there. A
keystroke interceptor, which gpui runs before matching any binding,
now handles both.

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

* fix(palette): shifted editing letters are not the query field's

The modal rule let every secondary+A/C/V/X/Z chord through as the query
field's own, shift included, so on macOS Cmd+Shift+A (New Agent Tab)
still opened a tab behind the open palette. Only redo keeps its shift.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-09-30 09:38:43 +08:00
2b94fddedc fix(ui): refocus the app when focus is lost so global shortcuts answer (#1023)
* fix(ui): refocus the app when focus is lost so global shortcuts answer

With focus on nothing, or on a handle whose element is no longer drawn (a
closed file panel, a dismissed menu), gpui dispatches keys on the window root
alone, one level above Tty7App's listeners. TogglePalette, ToggleSwitcher,
OpenSettings and every other tty7-root action went dead. on_focus_lost now
hands focus back via focus_active.

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

* fix(ui): refocus settings only in the window that draws it

Settings opens in a window of its own, but focus_active handed focus to
the page's handle whenever settings was up, including in the workspace
window. There that handle is not drawn, so the refocus on focus loss
parked focus on nothing and the workspace shortcuts stayed dead while the
settings window was open. Focus the page only when this window draws it,
and fall through to the workspace's own panes otherwise.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-09-30 09:36:50 +08:00
802f9e0792 fix(agents): a Claude resume that finds no conversation starts fresh under its id (#1024)
* fix(agents): a Claude resume that finds no conversation starts fresh under its id

A tab opened and never used, or run with transcript saving off, has no
saved conversation, and `claude --resume` then stops at an error.

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

* fix(agents): only chain the fresh Claude start where the pane's shell has ||

Windows PowerShell 5.1 and nu reject a line containing `||` outright, so
the fallback took the resume down with it there. The restore line now
chains the fresh start only for shells known to have the operator, judged
by the pane's own shell: its spawn spec, else the configured or login
shell for a local pane. A workspace pane with no explicit shell stays
unknown and gets the plain resume, since its default lives on its host.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-09-30 09:35:32 +08:00
f7e4101992 feat(daemon): panes set FORCE_HYPERLINK=1 (#1025)
* feat(daemon): panes set FORCE_HYPERLINK=1

tty7 renders OSC 8 links, but supports-hyperlinks only trusts a fixed
TERM_PROGRAM list and strips them for tty7, so Claude Code's statusline
PR links came out as plain text.

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

* docs(daemon): note that FORCE_HYPERLINK also reaches redirected output

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-09-30 09:22:06 +08:00
cd5bf5508c ci: seed rust-cache from main, skip Rust jobs on docs-only PRs (#1022)
* ci: seed rust-cache from main, skip Rust jobs on docs-only PRs

Every PR branch saved its own ~2 GB rust-cache per target, readable only
by that branch, so the repo sat over its 10 GB quota with nothing from
main and every branch built cold. Save from main pushes and manual runs
only, keep the cache when a test step fails, and build with
line-tables-only debug info so the cached target dir is smaller.

Docs-only PRs skip the Rust work: a changes job diffs the merge commit
against its base, and build skips its steps rather than the job, so the
required build & test checks still report instead of pending forever.

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

* ci: count both sides of a rename in the docs-only check

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-09-30 09:21:09 +08:00
l0ng-ai 0896404634 fix(switcher): require a name for a new workspace, and give the form buttons
The New Workspace form prefilled a random codename and offered no button,
only an "Enter to create" hint. The name box now starts empty and must be
filled; the footer carries Cancel and Create, with Create disabled (and
Enter inert) while the name is blank.
2026-09-30 09:12:39 +08:00
l0ng-ai ef203821b0 feat(a11y): name the icon buttons and search toggles
Bumps gpui-component to the tty7 branch commit that gives Button and
Input a role and a name and exposes popup menu items, and names the icon
buttons whose tooltips are elements rather than text (New Tab, the
sidebar and detail-panel toggles, Switch Workspace) and the file search
toggles (Match case, Match whole word, Use regular expression).
2026-09-30 08:43:52 +08:00
l0ng-ai bb0089f43d feat(a11y): label the remaining lists and let VoiceOver open files from the tree
Editor tabs are tabs named by file (and unsaved state) with a named close
button; Info rows read as 'label, value'; keyboard shortcut rows as
'action, keys'; SSH hosts as 'name, address' with their expanded state.
File-tree rows open on the press, which assistive tech cannot send, so
they take its press action directly: a screen reader could select a file
but never open it.
2026-09-30 03:09:48 +08:00
l0ng-ai cdd3f3c2cd feat(a11y): read Settings rows as named groups with their switches' state
Each row is a group named by its title and description, so the control in
it is heard with its setting's name; switches report on or off, the reset
link is a button, and the Modified only switch has a name of its own.
2026-09-30 02:56:02 +08:00
l0ng-ai 6d07ca41b0 feat(a11y): announce the app's own dialogs as dialogs, named by their title
The SSH sheet, the worktree form and the in-app alert are dialogs to a
screen reader, titled with the question they ask.
2026-09-30 02:48:54 +08:00
l0ng-ai 4ae1c86290 feat(a11y): expose the Changes panel's groups and files to screen readers
Group headers are buttons named with their count and expanded state; files
are tree items named by path and group; directories by name with their
expanded state.
2026-09-30 02:41:01 +08:00
l0ng-ai 420aa33cac feat(a11y): expose Search Everywhere's results to screen readers
Each row is a list option named by its title and subtitle, with the
highlighted one selected.
2026-09-30 02:23:14 +08:00
l0ng-ai 8efbbc4a38 feat(a11y): expose Settings navigation and file-tree rows; title the Settings window
Settings sections are tabs with their names and selected state, file-tree
rows are tree items named by file with selection and expansion, and the
Settings window carries a title for VoiceOver and the Window menu.
2026-09-30 02:19:08 +08:00
l0ng-ai 8f044924ab feat(a11y): expose the sidebar's tabs and the detail panel's tabs to screen readers
Each is a tab with its full title and selected state, and pressable by
assistive tech. Before, VoiceOver saw the window, its buttons and the
terminal, and nothing between them.
2026-09-30 02:12:13 +08:00
l0ng-ai e96cad4858 fix(agents): type a fork or launch command at the new shell's first prompt
Sent the moment the pane was up, the line was echoed by the tty above
everything the shell printed while starting, then read after it. It now
waits for the first prompt, for at most three seconds.
2026-09-30 02:01:04 +08:00
l0ng-ai bbc13279e7 fix(settings): drop 'Paired with …' once that phone is unpaired
Unpairing the phone just paired left 'Paired with probe.' above an empty
'No phones paired yet.'
2026-09-30 01:55:38 +08:00
l0ng-ai e66fa4d881 fix(ssh): never restore an SSH tab as a local shell; show a reattached one as connected
After the server restarted, a native SSH tab's old pane id was attached
to, the attach failed, and the fallback spawned a local shell in its
place: the tab that had been another machine was now this one, still
titled and grouped like the remote. An SSH leaf is now only reattached
when the server lists its pane; otherwise the host is dialled again.

A window reattaching to a live SSH pane also never learned its phase,
since status frames only went to whoever was attached when they were
sent. The pane keeps the last one and replays it, so the tab keeps its
connected dot and warn-before-closing still applies.
2026-09-30 01:39:32 +08:00
l0ng-ai f99fff93fd fix(ssh): log in as this machine's user when a host leaves User blank
The form says a blank User is resolved at connect, and the host card shows
$USER, but the empty string went to the server as the user name. sshd
refused it as an invalid user and tty7 reported every key as rejected.
2026-09-30 01:22:02 +08:00
l0ng-ai bf60899450 fix(cli): capture the newest segment in the modes the ones before it left on
A resize while vim was open started the newest segment inside the
alternate screen. Replayed from the ground state its redraws landed on the
main screen, so capture still showed vim's ~ column after vim had exited.
2026-09-30 01:13:35 +08:00
l0ng-ai 4880171974 fix(ui): show macOS's /private/tmp paths as /tmp
git and canonicalize hand back /private/tmp/… for anything under /tmp, and
the workspace switcher and the worktree form drew that spelling, which no
shell or pane title uses. Only what is drawn changes.
2026-09-30 01:11:18 +08:00
l0ng-ai 186d715ca7 fix(cli): don't take the prompt still being drawn for a refused exec line
The daemon reports a pane at its prompt from the D precmd writes first,
before the user's own hooks have drawn the prompt. A line exec sent in
that window was followed by that prompt's A and only then read and run,
and exec reported 'came back to its prompt without running the line'
with exit 1. Back-to-back exec calls failed that way under any prompt
theme slow enough, as did exec_returns_the_commands_output_and_exit_code.
A prompt with no command started before it now has to stay that way for
a moment before the line counts as refused.
2026-09-30 01:06:26 +08:00
l0ng-ai 31f5d38f3d fix(sidebar): back a tab's hover close button for the row's full height
On a two-line row the diff count sits below the close button's band, and
the bottom of '+2' showed under the ×.
2026-09-30 00:53:59 +08:00
l0ng-ai c3fdbe5c77 fix(settings): focus a new host's first field; name tty7-defined hosts plainly
Add host opened a blank form with nothing focused, so typing went
nowhere. A host saved in tty7 read 'Defined in: In tty7'; the label is
now 'tty7 settings', as a group heading and after 'Defined in' alike.
2026-09-30 00:50:10 +08:00
l0ng-ai a35800b96d fix(config): read a shell entry with no program as the login shell
`shell` was the one structured field still read strictly, so a hand-edited
{"args": [...]} without a program failed the whole file. It was
quarantined and every other setting fell back to its default, and changes
made in Settings afterwards were never saved.
2026-09-30 00:33:22 +08:00
l0ng-ai 6f29cac8bf fix(switcher): put the workspace the query names above ones matched by tabs
The list was ordered by current-then-recent whatever the query, so typing
'qa' and Enter reopened this window's workspace (its tabs are under
/tmp/t7qa) instead of the workspace named qa.
2026-09-30 00:28:15 +08:00
l0ng-ai 7e9225e6b0 fix(search): match a command's description by the words typed
Any subsequence of a long description counted as a hit, so 'theme' listed
Git: Discard All Changes second, through the letters of 'Throws away every
uncommitted change'.
2026-09-30 00:24:24 +08:00
l0ng-ai 6ac8d3fd64 fix(switcher): select the suggested workspace name so typing replaces it
The New Workspace form opened with the generated name and the caret in
front of it, so typing a name produced 'qadusky-raven'.
2026-09-30 00:20:43 +08:00
l0ng-ai e1ab8e26fc fix(panel): name the shell a default-shell pane is actually running
The Info tab named the login shell for any pane on the default shell, but
a server started from a terminal spawns the shell it was started from.
The root of the pane's process tree is that shell.
2026-09-30 00:16:08 +08:00
l0ng-ai 1aa6e1e35b fix(daemon): set up integration for the shell a pane actually runs
Launched from a terminal, the daemon spawns the shell it was started from
rather than the login shell. The name handed to shell integration still
came from get_shell(), which is always the login shell, so tty7 started
from bash with zsh as the login shell ran bash with zsh's ZDOTDIR and no
rcfile: no prompt marks, no prompt editor, no completion.
2026-09-30 00:11:02 +08:00
l0ng-ai e0308bd54f fix(terminal): keep output without a trailing newline through a resize
zsh's PROMPT_SP pads dangling output until it wraps onto the prompt's row.
The grid took that wrap for the prompt's own first row, so the next resize
at the prompt cleared the output and widening joined it onto the prompt.
At the prompt marks, a padded wrap after dangling output is turned back
into a hard break.

Also: editor breadcrumbs, breadcrumb reveal and Problems labels match
files against the project root's real path, so a project under a symlink
(macOS /tmp) no longer shows the absolute path; the terminal font size
setting says points, which is what its stepper shows.
2026-09-29 23:34:32 +08:00
l0ng-ai 5ba588f6d2 fix(mobile): let Settings renew a pairing code, and say when one is spent (#1019)
While a code was on screen the Show code button was disabled, so the only
way to a fresh code was Cancel then Show code. Worse, any attempt at a
code closes the offer, so after a mistyped paste or a dropped connection
the page kept showing a dead code for up to ten minutes with no hint.

- The button reads "New code" while a code is up, and replaces it.
- A code that can no longer pair stays on screen faded, saying why
  (expired, tried, or replaced by a newer offer), with New code beside it.
- The validity note counts down instead of saying "10 minutes".
- Cancel, and switching phone access off, now withdraw the offer on disk;
  before, a dismissed code could still pair until it expired. Only our own
  offer is withdrawn, never one opened elsewhere since.

The gateway's pair_code now returns the offer's secret alongside the code,
and State gains pairing_is_open / has_open_pairing / close_pairing.

Claude-Session: https://claude.ai/code/session_01BDVpJ78s7RQVcj57vjTcfA
2026-09-29 22:50:59 +08:00