* fix(windows): bundle Microsoft's ConPTY so panes can answer color queries
The in-box conhost swallows a pane process's OSC 11 background query: it
never reaches tty7's emulator and no reply is ever written back, so
applications that choose a light or dark UI from the terminal background
render a dark UI under a light theme.
tty7 already answers OSC 10/11/12 from the live theme, so nothing was
missing but a pseudoconsole that forwards the question. Microsoft ships one
as a redistributable, and portable-pty already prefers a sideloaded
conpty.dll over kernel32's, so this is packaging rather than code: the pair
goes beside tty7-app.exe, where the DLL search path finds it.
Measured on Windows 11 26200, same binary, only the pair added beside it:
in-box conhost: the terminal side never sees the query; the client times
out with no reply
bundled ConPTY: the terminal side sees ESC]11;?BEL and a real pane reads
back rgb:efef/f1f1/f5f5 under catppuccin_latte, which is
the preset's exact background
The two files are one supported unit, so the release verifier fails a
package that carries only one, a mismatched pair, or the MIT notice-less
DLL. They also join PORTABLE_MANAGED_ROOTS, without which the updater would
reject every portable archive that contains them; they are deliberately not
required by verify_portable_payload, since tty7 runs without them and a
packaging slip should fail the release rather than a user's update.
build.rs stages the pair beside cargo's output so a development build does
not quietly run on the in-box host, and the daemon logs which pseudoconsole
it got.
Closes#345
* fix(windows): restage the bundled ConPTY when it goes missing
Watching only the vendored sources meant a staged copy that left the target
directory stayed gone: the build script was cached, so it never ran again to
put it back, and the build silently fell back to the in-box conhost. Cargo
treats a rerun-if-changed path that does not exist as changed, so naming the
destinations makes the staging self-healing.
Found by deleting target/debug/conpty.dll and watching the next build not
bring it back.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
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.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* 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>
* 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>
* 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>
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.
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.
Builds before the tty7/tty7-app split installed the GUI as tty7.exe. Upgrading
only adds tty7-app.exe, so the old binary stays on disk — and a taskbar pin,
which Inno cannot rewrite the way it rewrites [Icons] shortcuts, still points at
it. The user keeps launching the previous version from their pinned icon,
against the same daemon endpoint as the new one.
[InstallDelete] runs after PrepareToInstall has stopped the daemon and released
the file lock, and before the new files land. A fresh install has nothing to
remove.
The package name and every display name ("tty7" in menus, tray, .desktop
Name, CFBundleName, installer AppName, shortcuts) stay as they were; only
the executable file is now tty7-app / tty7-app.exe, per docs/cli-design.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv
Split the framework-free half of tty7 into `tty7-core` and add a headless
`tty7-server` built on it, so a workspace's filesystem, git and session state
can live on another machine while the GUI stays where it is.
- `crates/tty7-core`: wire protocol, session daemon, PTY, native SSH engine and
the domain model, with no gpui dependency. Module paths are unchanged.
- `crates/tty7-server`: the same daemon with no GUI attached, linked fully
static against musl and pushed onto the remote box. One dependency, on
purpose — a second one the GUI also needs belongs in core.
- `Host` trait + `HostId`/`HostRegistry`: every fs/git/watch call a workspace
makes goes through the machine it belongs to. `LocalHost` answers on this
box, `RemoteHost` over a routed control connection.
- `ui::host_ops`: the GUI's single door to a `Host`. Host calls block, so all
of them run on the background executor with the result landed on the UI
thread; de-duplication, staleness and error reporting live here rather than
at each call site. Enforced by a CI grep.
- Connect flow: home page → pick a configured SSH host → the machine's own
workspace list → a window bound to one workspace on it. Workspace switcher
groups by machine, this computer included.
- CI: static musl builds of `tty7-server` for x86_64/aarch64 via
cargo-zigbuild, a host-boundary grep, and version stamping factored out of
the nightly workflow. Both new jobs are non-required so branch protection
does not wedge open PRs.
Design and the interface contract it was built to are in
`docs/2026-07-27-remote-workspace-{design,impl-contract}.md`.
The [Code] block comment used Pascal { } braces, but {tmp} inside it
closed the comment early, so ISCC parsed the trailing prose as code and
aborted with "'BEGIN' expected" — no Windows installer was produced.
Switch the comment to (* *) so brace-form constants stay literal.
The persistent daemon (`tty7.exe --daemon`) is a detached background process
that outlives the GUI and is the running image of tty7.exe, so Windows locks
the file. An upgrade or uninstall then can't overwrite/remove the binary and
fails ("file in use" / reboot required) — the Restart Manager doesn't reliably
catch a no-window, DETACHED_PROCESS daemon in its own process group.
- spawn: extract the "stop the running daemon" half of `restart()` into a
reusable `stop()` (Shutdown -> await exit -> pid reap fallback -> clear
endpoint); `restart()` is now `stop()` + `ensure_running()`.
- main: add a `--stop-daemon` CLI entry that runs `stop()` and returns before
any GUI init, so it never opens a window.
- installer: in PrepareToInstall, extract the *new* tty7.exe to {tmp} and run
`--stop-daemon` (the new binary understands the flag; an old installed one
would launch the GUI instead), releasing the lock before file copy. Mirror it
in [UninstallRun]. Keep CloseApplications as a backstop but RestartApplications=no
(the GUI respawns the daemon on next start).
Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bundle-windows.ps1 now compiles windows-installer.iss (ISCC is preinstalled
on windows-latest) from the same staged payload as the zip, producing
tty7-<version>-windows-x86_64-setup.exe: per-user install by default with an
all-users option, Start Menu shortcut, Apps uninstall entry, optional desktop
icon. Release workflow uploads the new artifact; READMEs and CHANGELOG updated.