Commit Graph
395 Commits
Author SHA1 Message Date
l0ng-ai 225d1f9885 Merge main, and finish two loose ends
Conflict resolution: main's updater work (#330) already localized every
update string this branch was adding, with its own key names, so the
duplicate `SettingsUpdate*` keys and the inline `phase_text` match are
dropped in favour of main's `localized_update_phase`. The About page
keeps neither the Explorer section nor the CLI toggle, which is the point
of this branch.

Two fixes on top:

- The Explorer flag handling moves below `logfile::install`. A
  GUI-subsystem process has no console and Inno does not surface a [Run]
  entry's exit code, so the log was the only place a failed registration
  could have been reported -- and it ran before the log existed.
- Document the feature, including the two flags. Windows also ships a
  portable zip, and with the Settings buttons gone that install had no
  way left to add or remove the verbs.

Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab
2026-08-05 17:37:06 +08:00
Hongwei Qinandl0ng-ai 41117e3828 fix(ui): show remote server errors under their switcher group (#354)
* fix(install): prefer bundled server over release download for SSH remotes

SSH remote installs used , which only checks
 and ignores the server binary already shipped next
to the Windows executable. WSL already uses
to find that bundled binary.

Add  to auto-discover the bundled server and
fall back to the GitHub release download only when no matching local asset
exists. Switch  and  to use it.

This lets the Windows installer/zip (which already stages server binaries
under <exe>/server/) satisfy SSH remote installs without hitting the network.
The explicit  path keeps its strict no-fallback behavior, and WSL
remains bundled-only.

Refs: future issue/PR for bundling server binaries into Windows releases.

* style(install): fix rustfmt formatting in bundled-server tests

* fix(ui): show remote server errors under their switcher group

Remote server restart/replace failures were reported through a global
modal dialog, which mixed errors from different machines together and
blocked the UI.

- Add  to keep per-host error messages.
- Rename  to ; when a
   is available, store the error under that host's key and
  expand its switcher group. Only fall back to a modal when no target is
  known.
- Read  when building switcher groups and surface the
  message in the existing per-group error block.
- Add a Dismiss button to the group error block and clear stored errors
  when the user retries or replaces the server.

Refs: #<issue-number>

* fix(ui): keep remote errors visible when the switcher is closed

The grouped error block is only on screen while the switcher is open, so
routing every failure into it silently swallowed the ones raised from the
window menu's restart-server command and from a mismatch hit mid-connect.
Fall back to the modal whenever there is no switcher to put the error in.

Also scope the Dismiss button to its own host: it retired whatever connect
flow happened to be in `self.connect`, including one still connecting to a
different machine. And clear a stored error when a fresh connect to that
host starts, so a later successful connect does not leave the group showing
a stale failure.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-05 12:46:02 +08:00
Hongwei Qinandl0ng-ai 257d13e607 fix(ui): replace remote server binary from the mismatch dialog (#352)
* fix(install): prefer bundled server over release download for SSH remotes

SSH remote installs used , which only checks
 and ignores the server binary already shipped next
to the Windows executable. WSL already uses
to find that bundled binary.

Add  to auto-discover the bundled server and
fall back to the GitHub release download only when no matching local asset
exists. Switch  and  to use it.

This lets the Windows installer/zip (which already stages server binaries
under <exe>/server/) satisfy SSH remote installs without hitting the network.
The explicit  path keeps its strict no-fallback behavior, and WSL
remains bundled-only.

Refs: future issue/PR for bundling server binaries into Windows releases.

* style(install): fix rustfmt formatting in bundled-server tests

* fix(ui): replace remote server binary from the mismatch dialog

The version/protocol mismatch dialog previously offered a 'Restart Server'
button that only restarted the existing daemon without replacing the
incompatible binary. This left users stuck on the same mismatch after the
restart.

- Change restart_mismatched_remote_server to call replace_remote_server
  (replace binary + restart daemon) instead of restart_remote_server.
- Add a dedicated L10nKey::RemoteMismatchReplaceServer ('Update Server' /
  '更新服务器端') and use it for the dialog's action button and detail text.
- Update the mismatch title/detail copy so it describes replacing the server
  binary rather than restarting it.

Refs: #351

---------

Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-05 12:39:49 +08:00
ARNOandl0ng-ai 8a7b2f3bea style(terminal): increase powerline half-circle segments for smoother curves (#341)
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-05 11:15:55 +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
l0ng-ai ce7004f2e1 refactor(settings): rescope the About page
About had grown three sections that change system state and that nobody
looks for under "About": a PATH install, a registry write, and a daemon
restart. Two of them move out.

The `tty7` CLI goes to Agents. That page already describes tty7 <-> agent
integration in one direction (hooks reporting session status); the CLI is
the other direction, and its own description leads with "so scripts and
coding agents can drive tty7". The Loading and Unavailable arms there no
longer return early, since the CLI toggle is about this GUI's own host
rather than whichever machine the hook rows describe.

The Windows Explorer context menu goes to the installer, which is where
VS Code and Git for Windows put theirs: writing shell verbs is an
install-time decision, not a runtime preference. A task checkbox drives
new `--register-explorer-menu` / `--unregister-explorer-menu` flags, so
the key layout stays in core::explorer_context_menu instead of being
copied into the .iss. `status()` existed only to paint the settings UI
and goes with it. The uninstaller unregisters unconditionally: an install
that registered once and was later upgraded without the box ticked still
holds keys that would otherwise point at a deleted exe.

Server restart stays — it is about the app itself.

Also fixes localization the About section had skipped: eight hardcoded
English strings in the update block now have keys, and the orphaned
SettingsCheckUpdatesDesc key (which still claimed "tty7 never updates
itself", contradicted by the macOS in-app updater) is reused for a
one-line description in place of a 60-word account of the updater's
internals.

Finally, terminology in the Chinese UI. hook, agent, worktree, diff and
fork are read and spoken in English by Chinese developers, so translating
them lost more than it gained. Scrollback was worse than a style
question: 回滚 means rollback, the opposite direction. 窗格 for pane is
kept — that one is standard.
2026-08-05 10:03:41 +08:00
4ec6b1df45 fix(terminal): upload pasted images to the remote host in SSH panes (#338)
* fix(terminal): upload pasted images to the remote host in SSH panes (#337)

* fix(terminal): stage remote clipboard images in a private dir, off the UI thread

Review follow-up to the SSH image-paste upload.

`/tmp/tty7-clipboard-<user>` is a predictable name in a world-writable
directory: any local account on the remote host could pre-create it, and
the `Mkdir` result was discarded, so tty7 would have uploaded into a
directory someone else owned — readable by them, and swappable for
another image before the pane's agent opened the path. Screenshots are
exactly the payload that must not land there.

Images now stage in `$HOME/.cache/tty7/clipboard`, resolved from the
session's own `realpath .`, and the directory is verified before anything
is uploaded into it: a symlink is refused outright (`stat` would judge it
by its target), a `chmod 0700` the daemon watched succeed is the
ownership proof — POSIX only lets the owner change a mode — and a
following `stat` must report exactly `0700`. Any doubt is a hard failure
that falls back to pasting the local path rather than uploading. The
uploaded file is chmod'd `0600` once the transfer lands.

Only a verified directory is cached, so a preparation that failed is
retried on the next paste instead of latching a "ready" flag over a
directory that was never created.

All of it moves off the UI thread. Preparing the directory, starting the
transfer and polling it are blocking daemon+SSH round trips — the
workspace route gives up after 30s, the standalone-SSH route sets no read
timeout at all — and a keystroke handler must not make them. The pane
pastes from a background task instead, which is also what lets the upload
be watched to a terminal state: the SFTP panel's history only polls while
that panel is open and drops finished jobs after 30s, so a failed upload
used to leave a dangling remote path in the line and say nothing. Now it
notifies, once, naming the host and the reason.

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:16 +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
cwatanab 4c9597c7b8 fix(terminal): draw half-circle powerline caps with segmented curves (#335) 2026-08-04 20:14:44 +08:00
47aa6532f5 fix(terminal): eliminate seams between Powerline separators (#336)
* fix(terminal): eliminate seams between Powerline separators

* fix(terminal): skip the separator cover quad when the glyph is dim

The cover quad and the anti-aliased path overlap on the closing edge's
device pixel. With an opaque foreground that is a no-op, but a DIM cell
carries fg.a = 0.66, so the two compositing passes push that one column
to 1 - 0.34^2 = 0.884 alpha and tint the neighboring cell's background.
Emit the quad only for opaque separators.

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 20:14:38 +08:00
a1d89a3752 fix(i18n): correct Chinese terminology and wording (#334)
* fix(i18n): correct Chinese terminology and wording

The worst one collided two different concepts: a git worktree was
translated as 工作区, the same word tty7 uses for a workspace. "Remove
Worktree" therefore read as "delete this workspace" in a destructive
confirmation, and New Worktree Tab had three different names across the
menu bar, the palette, and its own dialog. A working tree is already 工作树
everywhere else, so worktree joins it and workspace keeps 工作区 to itself.

Also:

- Forget password means "clear the stored password", not "I forgot my
  password" — 忘记密码 reads as password recovery.
- The SSH auth mode Agent (ssh-agent) was 代理, the same word as the proxy
  fields right next to it.
- "The server holding your shells" became 保存 (stores), which is not what
  the server does with them.
- Focus follows mouse had subject and object swapped.
- "over a week ago" lost its "ago".
- The shell help said to clear "Program" while the field above it is
  labelled 程序; the sidebar-grouping help said "Scratch" while the header
  itself reads 草稿.
- "Off closes straight away" was ambiguous about what closes.
- Mark Tab as Unread lost the tab in the palette.
- An SSH profile is a saved host, not a file on disk: 配置文件 → 主机配置.
- Punctuation: 帐户 → 账户, a halfwidth comma in Ln/Col, and em dashes
  inside a sentence are now —— throughout instead of a spaced —.

* fix(i18n): close remaining zh terminology gaps

Review follow-up to the terminology pass on this branch.

- Finish the profile -> 主机配置 rename. The About blurb still said 配置文件,
  and the Hosts search keywords still only matched the old term, so searching
  settings for the words the UI now shows found nothing. Settings search is a
  plain substring match over the whole keyword blob, so 主机配置 joins
  配置文件 there and both still reach the section.
- Quote UI labels with the “” the file already uses for “显示更多选项”,
  rather than the 「」 that had been introduced in two strings.
- 一周多以前 -> 一周多前, matching its four siblings: 刚刚, 分钟前, 小时前,
  天前.
- 标记标签页为未读 -> 将标签页标记为未读.
- The remove-worktree dialog said 未提交的更改 while the changes panel, the
  diff overlay and the palette all call git changes 变更. Its confirm button
  (放弃更改并删除) is rendered from the same prompt, so the two moved together
  and the dialog stays internally consistent.

The New Worktree Tab labels are left alone: 新X in the menu bar and 新建X in
the palette is the split the file already makes for New Workspace.

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 19:59:05 +08:00
5d14603722 fix(windows): advertise terminal background to TUI apps (#332)
* fix(windows): advertise terminal background to TUI apps

* refactor(windows): keep the background hint out of config.json

The daemon needs to know whether the window is light or dark when it
spawns a Windows pane, because ConPTY drops the child's OSC 11 query
before tty7's emulator can answer it. It was reading that from
`Config::theme` — a field nothing had written since it went dead — which
meant the GUI had to rewrite the user's `config.json` every time the
effective preset changed sides.

Move the hint to `appearance.json`, beside `machine.json` in the data
dir, and leave `Config::theme` exactly as it was. It is derived state:
written by the process that paints the window, read by the process that
has to describe it, and of no interest to the user. A file of its own
rather than a field on `Machine`, because the machine tree is owned by
the daemon and flushed on a timer, so a second writer would clobber the
workspaces and panes it had not seen. Absent, unreadable, and unparsable
all read as light — what the default preset is — so a daemon that starts
before the GUI has ever applied a theme describes the default window
instead of guessing.

Also silence the `unused variable` warning the hint parameter raised on
every non-Windows build, where the `COLORFGBG` block it feeds is
compiled out.

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 19:40:20 +08:00
vkingwandl0ng-ai 0f3d176e98 fix(input): let ctrl-e accept ghost suggestions (#329)
Accept a visible inline history suggestion while preserving end-of-line behavior when no suggestion is shown.

Refs l0ng-ai/tty7#315

Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-04 19:21:32 +08:00
Hongwei Qin 761e8e75c1 fix(terminal): prevent Ctrl-U after agent interrupt (#312)
* chore: reserve issue 305 draft

* fix(terminal): ignore alt-screen-only typeahead

* fix(terminal): discard typeahead at alt-screen boundaries (#305)

* fix(terminal): drop alt-screen boundary input (#305)

* fix(terminal): discard agent typeahead on interrupt (#305)
2026-08-04 18:57:40 +08:00
8a342f2ca9 feat(ui): GUI localization for en and zh-Hans (#303)
* feat(ui): add GUI localization for en and zh-Hans

* feat(ui): localize search placeholders and relative time

* feat(ui): localize palette, switcher, and sftp strings

* feat(ui): localize home shortcut labels

* feat(ui): localize tray, ssh prompt, and editor strings

* feat(ui): add plural/select i18n helpers and localize sftp/settings labels

* feat(ui): localize settings search, forwards panel, and file tree

* feat(ui): localize code editor and right panel

* feat(ui): localize stop/delete workspace confirmations with plural support

* feat(ui): localize diff overlay with plural-aware summary

* feat(ui): localize pending pane, worktree prompt, and home time strings

* feat(ui): localize app menus, tray, tab strip/sidebar, and remote status strings

* feat(ui): localize switcher, file_tree, machine_mirror fallback strings

* feat(ui): localize ssh prompts, theme presets, host error wrapper, and finish remote strings

* feat(ui): localize command palette strings

* feat(ui): localize app.rs notifications, prompts, placeholders, and parse errors

* feat(ui): localize remaining theme, switcher, settings, and sftp strings

* style: cargo fmt

* feat(ui): add language selector to settings

* fix(ui): refresh locales across windows

* refactor(ui): make GUI language selection explicit

* fix(ui): localize Explorer settings after merge

* fix(ui): keep persisted theme names out of the GUI locale

A theme's name is data, not chrome: it is written into the theme YAML and
matched back with `trim_end_matches(" (custom)")`. Translating it meant a
Chinese GUI forked "Nord" into "Nord(自定义)", the next fork stacked a second
suffix on it, and the name stayed Chinese after switching back to English. The
derived-name fallback had the same problem. Both are English again.

Also in this pass:

- Give each test thread its own locale override. The locale is process-wide and
  tests run in parallel, so the two tests that switched to zh-CN could flip the
  language out from under another thread's English assertions.
- Rebuild the menu bar when gui_language changes in config.json, the way the
  in-app picker already does — otherwise the menus kept the old language.
- Document the values the setting actually accepts. The docs still described
  `auto` and `zh-Hans`, which sanitize() resets to `en`.
- Put the English words back into the Chinese search keywords for the language
  setting; the other 58 keyword sets keep them.
- Drop the unused is_zh_hans helper.

---------

Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-03 23:48:29 +08:00
b7e08c7e11 feat(windows): add optional windows explorer context menus (#310)
* add CLI support for opening directories in new tabs

f

* feat(windows): add optional windows explorer context menus

f

* fix(gui): restore missing windows and reject lossy paths

* fix(windows): harden explorer menu registration and native path handling

* fix(cli): preserve native GUI paths on Windows

---------

Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-03 15:23:59 +08:00
ARNOandthomas c2ee9483a9 feat(shell): add detected and custom shells to the new terminal menu (#311)
* feat(windows): add Nushell support

* feat(shell): add custom shell to the terminal menu

* feat(shell): add cross-platform Nushell support

* fix(shell): preserve custom arguments across shell inventories

* fix(shell): preserve configured command identity

---------

Co-authored-by: thomas <thomas@gmail.com>
2026-08-03 15:11:55 +08:00
l0ng-ai a8e9230eed Merge branch 'main' into feat/macos-in-app-updater 2026-08-03 14:46:21 +08:00
60a7edf434 fix(settings): make hover highlights respond immediately (#313)
* fix(settings): make hover highlights respond immediately

f

* fix(settings): keep hover row identity stable

---------

Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-03 14:46:15 +08:00
Adeline Carterandl0ng-ai e8b419b543 fix(input): forward macOS editing shortcuts to foreground TUIs (#304)
* fix(input): forward Cmd+Backspace to foreground TUIs

* fix(input): complete foreground TUI shortcut forwarding

---------

Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-03 14:29:55 +08:00
thomas f86db1563e fix(updater): keep rollback backup in staging 2026-08-03 13:45:31 +08:00
ARNO 9b62ef3f16 feat: add CLI support for opening directories in new tabs (#308)
* add CLI support for opening directories in new tabs

f

* fix(gui): restore missing windows and reject lossy paths
2026-08-03 13:33:15 +08:00
ayamir a6754b28bc feat(update): install verified macOS releases in app 2026-08-02 22:25:10 +08:00
l0ng-ai da6df709cd Remove orchestration skill setting 2026-08-02 13:12:09 +08:00
l0ng-aiandl0ng-ai 0a3accd10f fix(completion): escape inserted candidates, close spent menus, keep PATH local (#276)
Three defects in the inline completion menu, all of which produced something
wrong rather than merely unhelpful.

A candidate was inserted into the command line verbatim. A directory named
`My Documents` completed to `cd My Documents/`, which the shell resplits into
two arguments and the command breaks. `shell_escape_path` already existed for
drag-and-drop paths; completion never reached for it. `escape_candidate` wraps
it and keeps a leading `~/` unescaped, since that prefix is the user's own text
and escaping it would stop the home expansion it was typed for.

The same escape decides whether a common-prefix step is safe to write. The
prefix shared by `My Documents` and `My Music` is `My ` — writing it raw both
breaks the line and puts a space inside the open word, which closes the menu on
the next keystroke and leaves the user worse off than before the Tab. A prefix
that needs escaping now steps through the candidates instead.

A menu fed only by generators stayed armed forever when nothing matched.
`git ckout<Tab>` matches no subcommand, but git's alias generator is in flight,
so the session opens empty and waits — and the callback that would have closed
it returned early on an empty result, so the menu never learned the generator
was done. An armed empty menu swallows every later Tab instead of handing the
line to the shell. Sessions now count their generators, and the last one to
answer closes a menu that still has nothing in it.

Command completion scanned this machine's PATH in a remote pane. The remote
isolation added in 08ca3a3 covered paths and generators but deliberately left
command completion running, which was right for the builtins half and wrong for
the PATH half: `system_prof<Tab>` over SSH to Linux offered macOS's
`system_profiler`. Worse, it failed inconsistently — with no local match the
position falls through to the remote's own compsys and answers correctly, so
the bug only appeared when this machine happened to have a match. Builtins are
true on any POSIX shell and still go out; the PATH scan is now local-only.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:13:19 +08:00
l0ng-aiandl0ng-ai f3b029530a fix(core): read the login shell from passwd, not the stale $SHELL (#278)
$SHELL is a snapshot the session inherits at login, so chsh never moves
it -- a GUI launch keeps reporting the shell that was current when the
user logged in, and goes on doing so until they log out. The window's
shell menu marked the wrong entry "default" for that whole stretch.

Read the passwd entry instead, via getpwuid_r -- the reentrant form,
since getpwuid returns a pointer into a shared static another thread's
lookup can overwrite. $SHELL stays as the fallback for the rare case
where the lookup fails. The three callers that each reached for the
variable on their own -- the default-name lookup, the PATH enrichment
that runs the login shell at startup, and the shell-integration kind
probe -- now share the one function.

Same commit fixes who wins a name in the menu. Candidates were login
shell, then /etc/shells, then $PATH, and dedupe keeps the first -- so
on a machine with a Homebrew bash, /etc/shells listing /bin/bash first
handed the entry to macOS's 3.2 from 2007, old enough that
bash-completion 2.x will not load against it. Probe $PATH before
/etc/shells and widen the probe list to the POSIX shells, so the menu's
"bash" is the binary typing bash would reach; /etc/shells still catches
anything installed off $PATH.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:12:29 +08:00
l0ng-aiandl0ng-ai 48c13684a8 fix(input): restore editor after interrupting tab handoff (#290)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:12:14 +08:00
l0ng-aiandl0ng-ai 75dbbf8daf fix(terminal): render ANSI text decorations (#289)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:11:59 +08:00
l0ng-aiandl0ng-ai 1ba7930999 fix(terminal): render ANSI dim text (#288)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:11:39 +08:00
l0ng-aiandl0ng-ai fe3bc17f8c fix(ui): soften overlay scrollbars (#293)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:11:01 +08:00
l0ng-ai c396e85e0c docs(skill): launch workers interactively and wait on --changed
The taught loop started its worker with `claude -p`, but headless print
mode never stops to ask, so the `waiting` state steps 3-4 are built on
could not arrive. Step 2 now launches interactively.

Every wait after a send passes `--changed`, with the reason spelled out:
without it the loop re-reads the state it just walked in on. The exit-1
"worker died" branch is documented too, and both guards are asserted in
the skill-content test.

Also gives the install round-trip test a Drop guard, so a panic cannot
leave `CLAUDE_CONFIG_DIR` set for whatever runs next in the process.
2026-08-02 09:49:59 +08:00
l0ng-ai be9a2d2d65 fix(ui): take the orchestration-skill switch off the render path
`installed()` was called from inside `render_settings_agents`, so the
Agents page did a `read_to_string` every frame it was on screen. The hook
rows it was modelled on cache into `SettingsState` precisely to avoid
that; the skill's presence now does the same, read when the page opens
and after a change.

The switch also swallowed its outcome into `log::warn!`. The one error a
user actually hits — uninstall refusing a `tty7-orchestration` file tty7
did not write — left the switch springing back with no explanation. The
result now lands in `orchestration_skill_note` beside the switch, the way
`agent_hooks_note` does, and the install/uninstall itself runs off the UI
thread.
2026-08-02 09:49:58 +08:00
yetoneandl0ng-ai e6bdf44f3c feat(cli): session CLI for scripting and agent orchestration (#248)
* feat(cli): `tty7 wait` + the agent-coordination note

The two pieces of the original session-CLI PR that main's own CLI
doesn't cover, rebuilt as a minimal delta against it.

`tty7 wait %N --until waiting,done --timeout 600` blocks until a pane's
agent reaches a requested state — the orchestration primitive that lets
one agent sleep until its peer blocks on a permission prompt or
finishes a turn, instead of screen-scraping. A poll of `AgentStates`
rather than an `events` subscription on purpose: a one-shot stateless
question composes into scripts, survives a server restart mid-wait, and
needs no cursor management. Agentless-but-live panes read as idle via
the machine tree; a dead or vanished pane reads as exit, which ends
every wait (matched only when asked for). Timeout exits 124, the
`timeout(1)` convention.

The coordination note is discovery for the whole CLI: a marked,
idempotent block describing the verbs, installed into
~/.claude/CLAUDE.md (always; CLAUDE_CONFIG_DIR honored) and
~/.codex/AGENTS.md (only when ~/.codex exists). A one-time "Let your
agents coordinate?" prompt fires the first time a pane detects a coding
agent; a Settings → Agents switch drives the same install/remove, with
state read from the files themselves. Uninstall strips exactly the
marked block; an unterminated block is left alone rather than truncated
at a guess.

* feat(agents): replace the global note with an orchestration skill

Per review: global instructions tax every session's context and hand
every agent — workers included — the ambient authority to orchestrate
its neighbours. The common shape is primary → workers: one agent owns
decomposition, dispatch, waiting and aggregation; workers just do
bounded tasks.

A Claude Code skill fits that exactly. `core::orchestration_skill`
installs ~/.claude/skills/tty7-orchestration/SKILL.md — only its
one-line description rides in context until the user or the primary
agent explicitly invokes it, and workers never see it. The body can
therefore afford the full delegation loop (tab new → send → wait →
answer-or-capture → pane close) instead of a token-starved cheat
sheet.

The file is wholly tty7-owned: install is a plain overwrite (also the
version-refresh path), and uninstall keys on an ownership marker so a
user's hand-written skill under the same name is refused, not deleted.
Gone with the global note: the first-agent-detected prompt, its config
flag, and the CLAUDE.md/AGENTS.md writers — the Settings → Agents
switch now drives the skill install instead.

---------

Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-02 09:37:09 +08:00
ayamir ee386d3de3 fix(graphics): bound Kitty frame memory
Replace the unbounded decode channel with a bounded, latest-frame inbox so full-window browser frames cannot queue faster than they decode. Keep deletes ordered with in-flight work and discard superseded frames before decoding them.

Only retire images that reached the sprite atlas, and evict remaining atlas entries when a pane closes. This keeps hidden terminal-browser tabs and repeated pane lifecycles from retaining one decoded frame per repaint.
2026-08-01 22:14:37 +08:00
l0ng-ai 596f436c31 fix(ui): keep workspace titles derived from workspace context 2026-08-01 08:35:02 +08:00
thomasandClaude Fable 5 b8dc38fb98 fix(input): anchor the IME candidate window at the fake caret, not the parked cursor (#275)
Cursor-hiding TUIs (Kimi CLI, Ink apps) draw their caret as a reverse-video
cell and leave the real cursor wherever the frame's last write ended — for
Kimi that is the input box's right border, and the IME candidate list was
stranded there. When the cursor is hidden and its row holds exactly one
caret-sized inverse run, snap the IME anchor (and the marked-text preview)
to that run; rendering is untouched.

The gpui side (bumped here) now also answers IMR_QUERYCHARPOSITION — the
query the Windows 11 Microsoft Pinyin IME uses instead of CANDIDATEFORM —
and re-anchors the candidate window on every WM_IME_COMPOSITION.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 23:48:01 +08:00
l0ng-ai 9fd84bf555 fix(ui): don't let a bare shell name outrank the cwd/repo title
pane_title_of() picked any non-empty PaneRecord.title, but an idle
terminal's foreground process is just the shell itself (zsh, bash, ...).
That made almost every idle-shell workspace show up as "zsh" in the
sidebar instead of the far more useful cwd/repo-derived name, which
defeats the "cwd as final fallback" intent of this change.

Skip bare shell process names when picking a pane title, so the
process-name fallback only kicks in for genuinely distinctive
foreground processes (nvim, an agent, ...).
2026-07-31 23:00:53 +08:00
ayamir 3847e4d015 fix(ui): show process names in sidebar titles 2026-07-31 23:00:53 +08:00
l0ng-ai 6374a08621 fix(ui): restarting the server must not strand the window on the home page
Restarting the local server clears the window's tabs and then pulls the
layout back from the machine tree. The pull went out on the control link
we held, which pointed at the server we had just killed: `is_connected`
only flips once the reader sees EOF, so for a moment the dead link still
classifies as Ready. The call on it failed, `finish_hydration` logged a
warning and gave up, and the window stayed empty on the home page.

Drop the local link before resyncing so the pull waits for the new
server instead of racing a dead socket, and record a failed hydration as
a debt the next sync retries. The debt also stops the empty window from
diffing into "close every tab" and wiping the layout off the machine
once the link is back; a `Replace` retry is abandoned if the user has
filled the window in the meantime.
2026-07-31 19:19:47 +08:00
l0ng-ai 0c9f4baa3a fix(cli): make the PATH install reversible, honest, and safe to migrate
Follow-up on the review of #277. Seven fixes, no change to what the feature
is for.

An AppImage copy is now claimed with a marker file instead of being inferred
from "am I an AppImage right now". Keying off the runtime meant that a user
who moved from the AppImage to the tarball hit their own copy, read it as
somebody else's binary, and never got another install for as long as that file
sat there.

The Windows uninstaller takes {app} back out of HKCU\Environment. Nothing did
before: the entry is written by the app at runtime, so Inno never knew it
existed and every uninstall grew the user's PATH by one dead entry. Unix has no
equivalent hook and still leaves its symlink behind; that is now stated in the
module docs rather than left to be discovered.

An occupied candidate directory no longer ends the scan, and every platform now
reports whether the install actually wins the lookup. `Occupied` on
/opt/homebrew/bin used to mean giving up while ~/.local/bin sat free, and
Windows — which appends to PATH and so never collides — reported `Installed`
even when an existing tty7 earlier on PATH kept beating it. A new
`InstalledShadowed` names the winner.

`cargo run --release` no longer repoints the developer's real tty7 at a build
tree. `cfg!(debug_assertions)` only covered the debug half of that.

The Windows registry PATH is read, matched, and written as UTF-16 throughout.
It went through `to_string_lossy` before, so a value the registry holds but
Rust cannot represent as a String would have been written back with U+FFFD in
place of its characters — the exact PATH corruption the surrounding code is
careful to avoid.

Two tests mutated $HOME and $PATH while the rest of the binary's tests ran
beside them, and src/ui/home.rs mutates $HOME too. `candidate_dirs` takes home
as a parameter, `place` takes its mode, and the PATH-joining and registry-
joining rules are pure functions — so no test in this module touches the
environment any more. 5 tests become 11, and the Windows joining logic is
covered on every platform.

Also: the config flag reaches Settings → About and both features docs instead
of being config.json-only, startup reads config.json once instead of twice, and
the CLI's strip failure warns like its sibling instead of being swallowed.
2026-07-31 16:49:22 +08:00
l0ng-ai c275960ceb feat(cli): ship the CLI in every installer and put it on PATH at launch
The `tty7` CLI was built by every release run and thrown away: all four
bundle scripts copied only `tty7-app`, and the upload glob covers `dist/`,
which the CLI never reached. Nothing put it on PATH either, so the
agent-facing half of the product was unreachable from a shipped install.

Bundle it on all four platforms, and have the GUI link it up itself rather
than hiding the step behind a menu item most people never find.

The install has two halves. The environment half prepends the CLI's
directory to this process's PATH before the daemon is spawned, so every
pane inherits it — that alone makes `tty7` work where agents actually run,
writes nothing to disk, and behaves the same everywhere. The on-disk half
symlinks into a directory already on PATH (Unix) or appends to
HKCU\Environment (Windows), and is allowed to fail.

Candidate directories are a fixed list intersected with PATH, not the first
writable entry on it: pyenv/rbenv/asdf/mise shim directories sit at the
front of PATH on many machines and are writable, and anything dropped there
is deleted on the next rehash — silently, days later.

Debug builds get the environment half only. `target/debug` holds a `tty7`
too, so otherwise a `cargo run` would repoint the developer's real `tty7`
at a debug binary, and each isolated dev-verify instance would rewrite the
PATH of the machine it is meant to stay away from.
2026-07-31 16:49:22 +08:00
l0ng-ai d5fdea555d test(ui): the shell probe needs longer than five seconds on a busy runner
The Windows CI job flakes on a_local_window_lists_this_computers_shells:
the local shell probe runs off-thread and, on Windows, spawns wsl.exe to
list distros. With the whole test binary running in parallel on a slow
runner that can take longer than the five seconds pump_until allowed, so
the poll gave up on a probe that was merely late.

Raise the deadline to thirty seconds, matching the other polling tests in
this repo. A probe that does land still returns immediately, so a healthy
run costs nothing extra.
2026-07-31 16:26:25 +08:00
l0ng-ai 95ceed533d fix(cli): name a mistyped subcommand instead of offering it to the GUI
`tty7 tree` answered "launching the GUI is not wired up yet (would open tree)".
Bare `tty7 [PATH]` opens the GUI, so any word clap has no subcommand for lands
in that positional — every typo came back as an offer to open a directory named
after it.

A word with no separator, no leading ./~, and no matching file on disk is a
mistyped verb, and is now reported as one. Real paths still reach the launcher
and fail there for the honest reason.

Also: `ws`'s help said "the named session trees", missed by the session -> shell
pass, and long_about still advertised $TTY7_SOCKET, which no longer exists —
it is $TTY7_CONFIG_DIR now.
2026-07-31 15:07:03 +08:00
l0ng-ai a94dac3fca fix(ui): a GUI-spawned pane carries its workspace, so $TTY7_WS is set
spawn_once hard-coded `workspace: None` — the field was added to make the call
compile when the protocol grew it, and never wired up. Every pane the GUI opens
therefore reached its shell without $TTY7_WS, and the GUI is how panes are
normally opened: `tty7 ws tree`, `tab ls` and `run --keep` all refused inside
one, and `doctor` reported the workspace as missing.

The id was already there — `owner` carries it and is passed straight through.
It is taken separately rather than reused after the filter below it: `owner` is
also gated on FEATURE_PANE_OWNER, while the workspace field rides the c4p5 spawn
kind and needs no feature probe. Local routes only, matching `owner`, since a
remote server keeps its own machine tree and this id names a workspace in ours.

Extracted as spawn_workspace so a test pins it; passing None again would be
invisible otherwise.
2026-07-31 15:06:51 +08:00
l0ng-ai a61bd486d3 merge: main — kitty graphics fans out to observers too
Two conflicts, both where main's graphics work and this branch's observer work
touched the same lines.

daemon/protocol.rs: both sides appended frame kinds. INPUT_ACK (51) and
IMAGE/DELETE_IMAGE (60/61) do not collide; both kept.

daemon/pane.rs: main taught the reader to forward a chunk as an ordered
GraphicsFrame sequence instead of one Output, so an image lands at the cursor
cell the sender drew it at. This branch had lifted the same send into
fan_out_output, which also feeds read-only observers and holds each to its
budget. fan_out_output now takes the frame sequence: the no-graphics fast path
still sends one Output, and Image frames reach observers as well, gated on their
own length. A Delete selector rides `notify`, which is ungated but still drops
an observer that has stopped draining — matching the drain accounting in
server.rs.

An observer is a read-only mirror of the pane, so it sees images for the same
reason it sees text.
2026-07-31 13:59:36 +08:00
ayamir 83d9a1c547 feat(graphics): render kitty graphics with shared-memory transport (#272)
Adds kitty graphics protocol support: a daemon-side APC tokenizer lifts image transmissions out of the PTY stream before the replay ring, forwards them out-of-band as compact binary frames interleaved in stream order, and the client decodes off-thread with newest-wins coalescing per image id. Local panes take the file/shm fast path; remote panes keep pixels compressed in-tunnel. Cell size is now reported to children in device pixels so pixel-aware senders render at native resolution.

Closes #213.
2026-07-31 13:47:59 +08:00
l0ng-aiandl0ng-ai 40a8ba711b fix(input): stop a Tab on a detached remote pane from eating the line (#273)
* fix(input): stop a Tab on a detached remote pane from eating the line

Tab reaches the view through the SendTab/SendBackTab actions, which bypass
the read-only guard in on_key_down. On a remote workspace whose link is not
attached, that let a failed completion run handoff_line_to_shell: it cleared
the editor's line and wrote it to a writer nobody was reading, so the command
vanished, the editor stood down for the rest of the prompt cycle, and every
later keystroke was swallowed by the guard the Tab had skipped.

Guard tab_pressed, handoff_line_to_shell and submit_command with
accepts_input, so a pane that cannot reach its shell keeps the line instead
of handing it to nowhere. A late SFTP listing now also checks that the editor
still owns the line before acting on it - that round trip can take seconds.

A listing that fails still hands the line to the shell, same as locally.

* fix(input): name the reason a detached link turns a Tab away

Fold the link check into a `link_inactive_reason` so the Tab path logs why
it did nothing, and let `remote_path_results` consult the link as well as
the editor instead of leaning on `handoff_line_to_shell` to catch it.

Reword the failed-listing warning: with the drop guard in place the line
does not always reach the shell.

Dispatch the Tab in the disconnected-pane test through the real `SendTab`
action, since the bug was that the action skips `on_key_down`, and add a
test that a Tab on a detached pane never starts an SFTP listing — the one
thing only the Tab-path guard prevents.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-31 12:11:24 +08:00
thomasandClaude Fable 5 f24a69de66 merge: server gaps — pane observers, TTY7_* env, aggregate queries
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv
2026-07-31 09:45:55 +08:00
thomasandClaude Fable 5 c8db432b63 fix(ui): one word per concept — shells, server, connection
The workspace switcher's row menu now uses the same verbs as every
other entry point (Stop/Delete Workspace), running processes are
shells everywhere, SSH links are connections, and the background
process is the server in all user-facing text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv
2026-07-31 09:45:46 +08:00
thomasandClaude Fable 5 753d1dceee feat(core): pane observers, TTY7_* context env, and control aggregate queries
Server-side gaps for the tty7 CLI (docs/cli-design.md, the additive tier):

- Pane multi-subscriber: the single controlling subscriber keeps its
  preemption semantics, and a pane now also carries N read-only observers.
  A new pane-protocol frame `Observe { pane_id, size }` (kind 54) joins a
  connection as an observer: it gets the Snapshot replay, then
  Output/Exited/Size, and its Input/Resize is answered with an Error frame
  instead of reaching the shell. Observers never displace the GUI, never
  defer the dead-pane reap, and are pruned when their connection goes away.

- Context env injection: every locally spawned shell now carries
  TTY7_PANE (its pane id), TTY7_WS (the workspace named in Spawn), and
  TTY7_SOCKET (the control endpoint path), next to the existing TTY7
  marker. ClientMsg::Spawn gains an optional `workspace` field, carried
  by the SPAWN_OWNED frame with serde defaults.

- Aggregate queries: ControlRequest::{AgentStates, Routes, Status} with
  ReplyOk::{AgentStates, Routes, Status}. AgentStates snapshots each
  pane's live agent session state through a new Services::panes directory
  wired from the daemon's registry; Routes lists the SshManager's held
  connections with key/kind/liveness; Status reports pid, uptime from a
  process-start instant, pane count, both dialect versions, build, and
  the control socket path.

- Dialect bump: CONTROL_VERSION 3 -> 4, PROTOCOL_VERSION 4 -> 5,
  following the convention set by bed22d8 and 1792bb8/a4972d3 where every
  additive variant bumped the strict-equality handshake versions (and
  with them the tty7-server-c{control}p{protocol} install name).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv
2026-07-31 09:44:22 +08:00