mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
docs/readme-screenshot
45
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8b08f51773 |
ci: drop the two Claude review workflows (#441)
Remove `claude-code-review.yml` and `claude-review-fork.yml`. Both are advisory-only and never gated a merge, so the required checks on main stay exactly `rustfmt` and the three `build & test (<target>)` jobs from ci.yml. Nothing else references them: the `claude-review` label and the CLAUDE_CODE_OAUTH_TOKEN secret were used only by these two files. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
741c863c2c |
fix(windows): make the install directory actually replaceable before updating (#403)
* fix(windows): make the install directory actually replaceable before updating
The updater stopped the daemon and started the Inno installer the moment
the daemon's endpoint disappeared — but the endpoint going away is not
the same event as the images being released. The ConPTY hosts
(OpenConsole.exe) are the daemon's children, not the shells', so the
per-pane kill never reached them, and the daemon's exit(0) skipped every
destructor that would have closed them; they kept the installed
OpenConsole.exe open for seconds after --stop-daemon returned. Silent
Setup then hit the lock, took the suppressed dialog's default (Abort),
and the updater's recovery relaunched the old build — "updated,
restarted, still the old version". A daemon that died without cleaning
up made it permanent: its orphaned hosts survive indefinitely, which is
the DeleteFile-code-5 users hit even after "closing everything".
Reproduced both shapes in isolation before fixing: with a pane open,
--stop-daemon returned ~1s in while OpenConsole.exe stayed locked for
another ~1.4s; after taskkill on the daemon, the orphaned host held the
lock forever.
The shutdown now finishes what it starts, at every layer that can be
the last one standing:
* The daemon reaps its remaining descendants and waits for them
before exiting, while the endpoint — the signal stop() watches —
is still up.
* stop() reads the pidfile before asking, and waits for that process
to actually exit after the endpoint goes, not just stop listening.
* The recorded-daemon reap waits for the images to be released
instead of returning on the async TerminateProcess.
* stop_for_update(dir) — reached via --stop-daemon
--update-install-dir, which PrepareToInstall and the portable
updater now pass — also terminates anything still running from the
installation directory (the orphan case no pidfile can name) and
only returns once the .exe/.dll images there open for writing,
naming the holdouts in the error if they never do.
* The updater runs that clearing itself before invoking Setup, so a
directory that cannot be cleared fails with a cause in update.log
and relaunches the previous build, instead of Inno's bare
"DeleteFile failed; code 5".
The update dialog on Windows also told a macOS truth — "the background
service keeps running, so whatever is open in your panes survives".
Windows cannot replace a running daemon's image, so its install path
stops the service; the dialog now says so.
* fix(windows): tighten the install-dir clearing per review
- An image that fails to canonicalize stays in the lock check instead of
being silently skipped; only a positive match against the caller's own
running image is excluded.
- reap_recorded_daemon shares one deadline across the whole tree via a
new winproc::terminate_and_wait_all, which stop_for_update and
reap_descendants_of now use too — one implementation of "terminate,
then wait, bounded overall" instead of three.
- [UninstallRun] passes --update-install-dir "{app}" like
PrepareToInstall, so uninstalling after a daemon crash gets the same
orphaned-ConPTY-host cleanup as upgrading.
* fix(update): close three gaps the update audit found
- macOS updater: wait for the parent by watching getppid() reparent to
launchd instead of polling kill(pid, 0), which a recycled pid could
satisfy forever. The kill loop remains only for a hand-run updater.
- Windows: a new update guard (config-dir update.lock, held by the
updater from daemon stop to relaunch) makes ensure_running refuse to
spawn a daemon mid-install, so a tty7 CLI call or manual launch can no
longer relock the images the installer is replacing. Stale guards —
dead writer or past the TTL — are shed on sight.
- Windows portable: the update backup now carries an incomplete marker
from before the first file moves until the replacement lands. At
launch the app reports a backup still carrying it as an interrupted
update (the installation may mix two versions; the old files are
preserved), and silently removes marker-less backups a finished
update failed to delete past an antivirus hold.
* fix(update): verify the guard's writer by start time, and guard manual Setup runs
Review round three, both findings and all three minors:
- The guard no longer expires a live, verified holder: a pid is believed
to be the writer only if the process behind it started before the
guard was written (winproc::creation_time via GetProcessTimes), which
is what tells a genuine holder from a recycled pid. The TTL now bounds
only the unverifiable case, so an install slowed past ten minutes by
an antivirus sweep keeps its protection.
- Manual Setup runs get the guard too: the --stop-daemon
--update-install-dir helper holds it in its parent's name — the Setup
or uninstaller that keeps replacing files after the helper returns —
and it goes stale when that parent exits. ensure_running gained five
seconds of patience so the post-install "Launch tty7" click, racing
Setup's own exit, gets its daemon instead of an error.
- processes_running_from also matches images against the canonicalized
install-dir spelling (junction, subst, 8.3 given form).
- reconcile_portable_backups reports every interrupted backup, not the
first.
- The unix signal-and-wait loop now reuses wait_for_recorded_exit.
* style: rustfmt
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
|
||
|
|
bc03e59b6f |
ci: keep the fork review to forks (#398)
The fork job never checked where the head branch lived. It was written as the fork path and reads like one, but `pull_request_target` fires on every pull request, so labelling one of ours landed there too -- silently, and with the weaker review: no plugin, no whole-repo context, read-only tools. #389 got that instead of the review it should have had. Also corrects the `labeled` comment in claude-code-review.yml, which promised exactly the case that cannot work. A `pull_request` workflow is read from the PR's merge ref, and GitHub recomputes that on a push and not otherwise, so a PR whose last push predates the file has a merge ref without it and no label can summon it. Pushing fixes it, and also triggers synchronize by itself -- which is why the empty commit worked and the label looked broken. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
51ea055d2a |
ci: anchor fork review findings to lines, and fence the reads (#397)
Two changes to the same job. Findings now go inline. The action ships an inline-comment MCP tool that buffers rather than posts: the queue is sent after the session ends, by the action's own code, past a classifier. That keeps the property review.md was chosen for -- the reviewer writes the words, a step that cannot be argued with performs the act -- while putting a finding next to the line it is about. review.md stays for the summary, which belongs to the change as a whole. Reads are fenced. A comment body is posted verbatim, so any file the reviewer can read it can publish, and the token is in this process's environment; /proc is the short path between the two. Reads are already confined to the working directory in the default permission mode, but this job should not rest on a default. Deny is evaluated before allow, so the rules hold whatever --allowedTools says. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
ea0a0f078c |
ci: keep reviewing a fork PR after the label goes on (#395)
The label was a one-shot trigger, so a contributor who addressed the findings and pushed got no second look unless someone removed and reapplied it. It now reads as a subscription: `synchronize` re-reviews while the label is present, and taking it off stops that. Two clauses rather than one label test, because `labeled` carries the label that was applied and `synchronize` carries none -- testing only the list would spend a review every time an unrelated label landed on a subscribed PR. This does not gate the code in the next push, and the header comment now says so. The gate is spend and attention; the safety is the base-branch workspace root, the absent Bash, and executing nothing. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
59b26ce283 |
ci: stop the fork review prompt from reading as a checklist (#396)
The general instruction was one line -- logic errors, edge cases, regressions -- followed by a numbered list of four, three of them tty7-specific. A list anchors, and that one named none of what actually breaks a terminal emulator: panic paths, unsafe, ordering, resources on the failure path, an API contract that no longer holds for its new callers. Replacing it with a longer list would only move the boundary. So the prompt now states the aim, offers examples while saying outright they are not a checklist, and notes that the finding nobody listed is usually the one worth having. The repo rules stay, demoted to what an outside reader cannot know and explicitly skippable. claude-code-review.yml is left alone: its rules are appended to the code-review plugin, which brings its own methodology, so a general clause there would compete rather than add. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
fb4e8f0860 |
ci: post the fork review from a step, not the action (#394)
track_progress only fires on opened, synchronize, ready_for_review and reopened. The approval gate depends on `labeled`, which is none of them, so the action refused the run outright rather than falling back to the log. The review is written to review.md and posted by a final step that runs no model and reads one file. That adds Write to the allowlist, which costs nothing: there is still no Bash, so still no curl, and the runner is discarded after the comment goes out. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
d2a7089189 |
ci: give the fork review the PR's tree and a voice on the PR (#393)
Three changes, one shape. The run was failing at the app-token exchange because the action checks the triggering actor's repository access and a fork PR's author has none; `github_token` plus `allowed_non_write_users` is the documented pair for pull_request_target, and the label gate is what makes trusting that actor a decision somebody made. Findings now go on the pull request instead of the run log, which needs `pull-requests: write`. That is affordable only because the tool allowlist stays read-only: with no Bash there is no curl, so the worst a successful injection buys is a silly comment. The fork's tree comes back, one directory down. #392 removed it entirely because a checkout at the workspace root is what Claude Code reads as the project -- but the action's own security guide gives the middle path, a subdirectory, which keeps the project files ours while letting the review see whole files instead of hunks. `.claude/` and friends are dropped from that tree and a top-level CLAUDE.md is renamed rather than deleted, since a PR that edits it still deserves review. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
2ddf2bad25 |
ci: review a fork's diff, never its tree (#392)
actions/checkout refuses a fork ref under pull_request_target without allow-unsafe-pr-checkout, and the flag is not the fix. The working directory is what Claude Code reads as the project, so checking out a fork hands it that fork's CLAUDE.md as instructions and that fork's .claude/settings.json hooks as commands -- neither of which the --allowedTools list governs. Constraint 2 said nothing from the pull request is executed; a checked-out tree could not honour it. Check out the base branch instead and bring the contribution down as diff text in one file. The reviewer reads the diff against trusted sources rather than the merged tree, which is less context than the same-repo path gets, and the right trade for code we do not control. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
8652c7928a |
ci: review fork pull requests behind a label (#391)
A fork PR gets no secrets on `pull_request`, so claude-code-review.yml skips it. `pull_request_target` is the only event that reaches the diff with our token, and it puts that token in a job beside code we did not write, so the fork path is a separate file under four constraints: a label applied by someone with write access is the only trigger, nothing from the PR is executed, the tool allowlist is read-only, and the job holds no write permission to carry anything back out. The same label also re-runs the ordinary review, which a PR opened before that workflow existed otherwise has no way to start. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
305baa8746 |
ci: review pull requests with Claude Code (#390)
Runs the code-review plugin on every PR open and push, on our own Actions minutes rather than the managed Code Review service, and posts the findings back onto the PR. Advisory only: the required checks on main stay rustfmt and the three build & test jobs. The append-system-prompt carries the four rules a general-purpose reviewer cannot infer -- dialect bumps staying readable to an older peer, src/ui paths going through Host, i18n keys landing in all three locales -- and tells it not to repeat what rustfmt and clippy already decide. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
f45dd34cab |
feat(update): separate Stable and Nightly release channels (#386)
The update channel becomes a property of the installation rather than something derived from how version numbers happen to sort, so a Nightly follows Nightly instead of being walked back onto Stable by an update it never asked for. Stable reads /releases/latest, which excludes prereleases; Nightly reads /releases/tags/nightly. Neither feed can hand the other an update, so an installation only changes channel when the user changes it in Settings. The nightly release cannot state its version in its tag — `nightly` is force-moved every night, so `tag_name` is the literal string. It now publishes nightly.json beside the packages, falling back to parsing asset filenames for builds that predate the manifest. Prereleases are ordered by every numeric identifier in the stamp, and the stamp goes to the minute so two builds in one day are distinguishable; a stable release still outranks every dated build of its core version, which is how switching back to Stable graduates instead of downgrading. Switching channel invalidates what the old feed produced: the staged package, the deferred prompt, and the transfer still in flight, which would otherwise finish and stage a build from the channel the user just left. Settings keeps one action on the update row rather than three — the update dialog covers the rest, but it is a moment rather than a place, and where the package cannot be installed for the user the release page is the whole update path. Skipping a version is retired along with its state, its Settings row, and its localization keys. Also carries the staging work this was branched from: an update is fetched and verified while the prompt is up, so installing it is a restart, and declining one defers it instead of retiring it permanently. |
||
|
|
e47b49dfdd |
fix(bundle): declare macOS TCC privacy keys for child processes (#323)
* fix(bundle): declare macOS TCC privacy keys for child processes tty7 currently ships no NS*UsageDescription keys and no data-access entitlements, so macOS falls back to a repeated "access other apps' data" prompt whenever a child process (shell, coding agent, mole, etc.) touches a protected folder such as ~/Library/Containers, Mail, Messages, or Calendar. kitty and Kaku both declare these privacy intents, which converts the prompt into a single, clear one-time grant. Add the folder/volume usage descriptions and the matching personal-information and device entitlements to the macOS bundle so the app behaves like its terminal peers. * fix(bundle): rework TCC usage strings per review - Correct problem statement: describe child-process-denied-without-prompt instead of the Full Disk Access framing (no NS*UsageDescription key exists for that class). - Add the full usage-string set (camera, microphone, contacts, calendars, reminders, photos, location, motion, local network, bluetooth, speech recognition, system administration, apple events), kitty-style wording. - Use macOS spellings: NSCalendarsFullAccessUsageDescription / NSRemindersFullAccessUsageDescription / NSLocationUsageDescription. - Drop every entitlement that has no matching usage string; keep only com.apple.security.automation.apple-events. - Restore trailing newline at EOF in bundle-macos.sh. - Document the Full Disk Access manual-grant requirement in docs/features.md. * docs: rewrite macOS privacy as feature notes (en + zh-CN) * fix(bundle): drop the apple-events entitlement, tidy the privacy docs The entitlement did not do what its comment claimed. Nothing in tty7 or in gpui's mac platform layer sends an Apple event, and it would not help the case this change is about either: the hardened-runtime automation check runs against the process actually sending the event, which is the pane's child carrying its own signature. What TCC reads off tty7.app is the usage string in Info.plist, which stays. Entitlements are per-executable and never inherited, so granting this one only widened what injected code could reach under an identity that already holds disable-library-validation. Docs: spell out the four Full Disk Access paths instead of running them together as one nested path, drop motion from the user-facing list (Core Motion has no macOS implementation, though the key stays for kitty parity), and place the section identically in the English and Chinese files. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
e3dded4be3 |
fix(release): keep the Inno payload for the package verifier to read (#368)
`verify-windows-package.ps1` reads the Inno staging directory to check what
lands in {app} — the compiled setup.exe cannot be read back without
innoextract, which the runners do not carry. But `bundle-windows.ps1` deleted
that directory as its last act, so the verifier has failed on every Windows
build since the check arrived in #330: "the Inno staging directory is missing".
Nightly has been red for two nights (2026-08-05, 2026-08-06) and a stable
release would fail the same way — release.yml runs the same step.
Both workflows already expected the directory to survive: their upload steps
name it among the dist/ intermediates the asset globs deliberately skip. So
this drops the removal rather than teaching the verifier to tolerate an absent
payload, which would retire the check it was added to make.
Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
|
||
|
|
3ae340408b |
fix(windows): bundle Microsoft's ConPTY so panes can answer color queries (#360)
* 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>
|
||
|
|
3235cd091c |
ci: bump actions/download-artifact from 7 to 8 (#325)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: l0ng-ai <ysdpk123@gmail.com> |
||
|
|
2fa518a767 |
refactor(settings): rescope the About page (#350)
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
a6754b28bc | feat(update): install verified macOS releases in app | ||
|
|
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. |
||
|
|
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. |
||
|
|
3b68a42cb8 |
fix(installer): delete the pre-rename tty7.exe on upgrade
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. |
||
|
|
10b6741368 |
refactor: rename the GUI binary to tty7-app, freeing tty7 for the CLI
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
|
||
|
|
2a4b5c7f68 |
fix(release): name the server assets for whoever downloads them, not for cargo (#265)
`tty7-server-x86_64-unknown-linux-musl` was never a name anyone chose. Both
workflows staged the file as `tty7-server-${{ matrix.target }}`, so the build
triple went straight into a published filename — and the triple's *vendor*
field, for a Linux target with no particular vendor, is the literal word
`unknown`. It has been sitting on the releases page reading like a failed
lookup.
Of the triple's four fields only two say anything to whoever downloads this:
the architecture, which is what `asset_for_uname` picks by, and `musl`, which
is why one file runs on any distribution. So:
tty7-server-x86_64-unknown-linux-musl → tty7-server-linux-x86_64-musl
tty7-server-aarch64-unknown-linux-musl → tty7-server-linux-aarch64-musl
`<os>-<arch>` in that order because that is what the GUI assets in the same
release already use (`tty7-<version>-linux-x86_64.tar.gz`). One release should
be one naming scheme; it was two.
The triple stays everywhere it really is a build target — `cargo zigbuild
--target`, the `target/<triple>/release` path, the rust-cache key, ci.yml's
matrix. The workflows now carry both: `target` for the build, `asset` for the
filename, deliberately not the same string.
This name is a contract with more than the release step, and all of it moves
together:
- `install::asset::{ASSET_X86_64, ASSET_AARCH64}`, which is what the client
appends to a release URL.
- `bundle-windows.ps1`, which stages the musl binary for WSL. `wsl.rs` looks
for `<dir>/<asset name>` with nothing translating, so the *filename* is as
much a contract as the `server/` directory is — now said out loud in both
places, along with the consequence for `TTY7_BUNDLED_SERVER_DIR`: a
cross-compile has to be copied to the asset name, not left as `tty7-server`.
- The GUI's install prompt fixture, the checksum manifest fixtures, and the
`MissingBundled` assertions.
Nothing globs the old shape: `gh release upload dist/*`, `checksums.txt`'s
`find`, and the installer's `server\*` are all name-agnostic.
A new test pins both names as literals — the module header already says this
naming is "a *literal* contract with the release workflow", and asserting the
consts against themselves asserted nothing. It also fails on the substring
`unknown`, since that word only ever arrived here by way of `matrix.target`,
and checks neither name contains the other, which is what
`checksums::expected_digest` says out loud that it relies on.
Compatibility: a stable client asks its own frozen tag, which keeps whichever
name it shipped with, so every released client keeps working. The rolling
`nightly` tag is replaced each night and its prune step drops assets the run
did not upload — so an *already installed* nightly client 404s on the server
download until it updates itself. Accepted deliberately; the next release is
what has to be right.
Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
33eedad90a |
fix(ui): the routed-auth test no longer hangs, and no longer loses its prompt (#263)
* fix(ui): the routed-auth test can no longer hang the whole suite `a_routed_auth_prompt_carries_the_machine_that_raised_it` waited for its own prompt in an unbounded spin loop. `AUTH_MAILBOX` is process-global and `pump_auth_sheets` drains every entry in one pass, so any gpui test in this binary that drives a tick can take that prompt first — and unbounded, the loop then spins until GitHub's six-hour job limit. This is the hang CI has been paying for, named twice and on two platforms: 2h50m inside this test on x86_64-unknown-linux-gnu (run 30517182773), and again on windows-msvc (run 30526538997). It is *not* the cfg(windows) transport accepts bounded in #261; those were a separate latent six-hour hang. The loop now has a ten-second deadline and an assertion that says what an empty mailbox means and whether the responder thread had finished. Note what that does and does not buy: a stolen prompt becomes a fast, self-explaining failure instead of a six-hour outage, but the theft itself is still possible, and curing it means deciding what that process-global mailbox should be under test — a design call, not something to settle inside a CI fix. ci.yml keeps only a comment where a post-mortem step used to be, because the step was worthless twice over. It cannot work: GitHub kills the step's process tree when `timeout-minutes` trips, before the next step runs, so on run 30526538997 the dump printed two headers and nothing between them. And it is not needed: libtest already prints "<test> has been running for over 60 seconds", which was in every hung run all along. The obstacle was only ever that a job's log cannot be fetched while the job is in progress — which the `Test` timeout fixes by making the step fail. * fix(ui): a test waiting on the auth mailbox is no longer raced by a tick The previous commit made the flake loud instead of fatal; this stops it happening. CI proved the mechanism on the very next run: the new assertion fired on windows-msvc with "no routed prompt arrived within 10s ... Responder thread finished: false", 731 other tests passing, the whole suite done in 12.39s instead of hanging for six hours. `AUTH_MAILBOX` is process-global and `pump_auth_sheets` takes every entry in one pass. That is right for the app — one tick, one mailbox — and wrong in a test binary, where a test waiting for the prompt it just caused shares that mailbox with every gpui test that drives a tick. The tick drains a prompt it has no idea was spoken for, and the waiting test never sees it. `MAILBOX_TURN` arbitrates: a test that needs its own prompt back claims it for the exchange, and the drain yields while it is held. Both the static and the claim in `pump_auth_sheets` are `#[cfg(test)]`, so a release build is byte-for -byte what it was — there is one app, one tick, and nothing to arbitrate. The compromise is visible and deliberate: test-only synchronisation inside a production function. The alternative that needs no such thing is to stop the mailbox being process-global — dependency-injected per app — which is a larger change to a path this defect does not otherwise justify touching. No deadlock: the claim is the first thing `pump_auth_sheets` does, before it locks the mailbox, so the two locks are only ever taken in one order. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
4f37fa8eb8 |
fix(transport): bound the Windows test hang — no unbounded loopback accept, and CI timeouts (#261)
* ci: bound the Windows test hang and stop superseded runs holding slots
The Windows `Test` step intermittently hangs — roughly one run in ten, on
any branch, while the same commit passes on a re-run. `cargo test` has no
timeout of its own, so every occurrence ran to GitHub's six-hour job limit:
three times in one day a 75-second step held a runner slot for hours and
reported nothing about which test was stuck. One of them took the runner
down with it ("the hosted runner lost communication with the server"), and
while those zombies held slots an unrelated PR's macOS job queued for two
and a half hours.
Three changes, none of which fix the hang itself:
- `timeout-minutes` on the `Test` step (20) and `Build` step (30), plus a
60-minute job backstop. The honest budget is ~75s warm and ~3.5 min when
the step also compiles the test targets, so a trip means a hang.
- A Windows-only post-mortem step on failure that dumps the process table.
libtest names a test when it *finishes*, so the hung one is the name
missing from a truncated list; the surviving test binary names its crate
and test target instead.
- `concurrency` with `cancel-in-progress` for pull requests, so a
superseded run stops competing for the shared concurrent-job budget.
Pushes to main are exempt: each commit's run is the record of whether
that commit was green.
The hang's cause is still unknown and cannot be reproduced off a Windows
runner. This makes it report in 20 minutes instead of costing six hours.
* fix(transport): no Windows test may block forever on a loopback accept
The three `cfg(windows)` tests in the transport's test module held five
unbounded `listener.accept().unwrap()` calls, each paired with a client
thread that `unwrap()`s its `connect`. When such a thread panics — a
transient loopback refusal on a loaded runner is enough — nothing is left
to wake the accept, and nothing is left to feed the handshake read after
it. The test does not fail; the whole test binary stops.
That is the shape of the hang CI has been paying for: Windows-only (these
tests are `cfg(windows)`, so no developer's macOS run executes them),
intermittent, and mute — libtest names a test only once it *finishes*, so
no log ever said which one was stuck.
`accept_within` polls a non-blocking listener against a ten-second
deadline, then restores blocking mode and puts a read timeout on the
accepted socket. Winsock hands an accepted socket the listener's blocking
mode, so clearing it on the returned stream is a real step, not a no-op.
Verified on the host target, where the logic is identical std code: a real
client is still accepted and its handshake read still works, and a client
that never arrives fails in 10.0s instead of never.
Whether this is the exact hang CI hit is unproven — it cannot be
reproduced off a Windows runner, and `aws-lc-sys` will not even build for
the Windows target on a mac. It is the only Windows-only cluster of
unbounded network waits in the tree, and it matches every observed
symptom. Either way the six-hour failure mode is gone from here.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
|
||
|
|
bed22d899e |
Keep workspaces whole: remote reopen/restart recovery, and cross-workspace restore guards (#257)
* feat(remote): keep a remote workspace whole across reopens and restarts Reopening a remote workspace — or coming back to one whose `tty7-server` had been replaced — landed on a screen of `tty7 — disconnected` panes with their coding-agent conversations gone. Several independent holes added up to that; this closes them together, and picks up the surrounding work the same session produced. **Telling a restarted server from a blinked link.** `ControlHelloOk` now carries an `instance` minted once per server *process*. Nothing else in the handshake changes across a restart — `build` and both dialect numbers survive it — so a reconnect had no way to know its `pane_id`s were dead. It does now: a different instance rebuilds the window from its layout (same tabs and splits, fresh shells in the saved cwds) instead of re-attaching to a process that is gone. An absent instance means *unknown* and is never read as a restart. **An attach can now fail.** `Attach` has no synchronous reply, so the client returned `Ok` unconditionally and the daemon's `Error` frame was read much later by the reader thread, which has no arm for it — the pane then landed in the *link is down* state instead of falling back to a fresh shell. The client now reads far enough into the reply to classify it on the kind byte (the snapshot behind it can be megabytes) and hands those bytes to the reader thread, so a successful attach loses none of its replay. Local and remote attaches get different waits: the local one is on the UI thread. **The agent session survives to be resumed.** `TerminalView` raises `AgentSessionChanged` when the pane's agent reports a new native session id, so the layout on file catches up instead of waiting for the user to happen to open a tab. A pane that is still connecting now carries its agent through `PendingSpawn` — a save landing in that window used to write `agent: null` over the record — and `land_pane` sends `--resume` when the attach turned out to need a fresh shell. **Ending sessions says so on file.** "End Sessions" kills the panes and then drops their ids from the record, pushing the cleared layout to the machine that owns it (design §10: the remote's copy wins, so a local-only clear would be undone by the next open — the open this exists for). **The new-tab dropdown lists the window's machine.** `Host::shells` and a `Shells` control request (dialect v2) make the "+" menu a property of the machine the window is bound to. A remote window filled from this computer's `/etc/shells` offered `/bin/zsh` on a box whose zsh is elsewhere, and every pick failed to spawn. **An install reports its bytes.** The download and the SFTP upload each report progress, relayed to the client over the routed connection as a `RoutePrompt::InstallProgress`, and painted as a bar under the machine's row in the switcher. ~8 MB across two hops behind the word "connecting…" was indistinguishable from a hang. **The installer compares dialects, not version strings.** `tty7-server --protocol` prints what a binary speaks without starting it, so a connect adopts an already-running server it can talk to rather than prompting about a build difference and uploading 8 MB the machine did not need. **Switcher.** A machine's `⋯` menu holds "New Workspace" (it was a row under every machine, pushing the list a quarter of a card down) and a new "Disconnect", which drops the connection and leaves the windows open and read-only. The suspension lasts exactly as long as that machine has a window on it. Also drops three design/contract docs for the now-shipped remote-workspace work. * fix(session): stop one workspace's panes from being restored into another A restart put a copy of one workspace's seven tabs — cwds, layout and recorded agent sessions — in front of another workspace's own tabs, and auto-resumed every one of those agents a second time: six `claude --resume <id>` pairs running in parallel against the same conversations, one set per window. The record-level corruption that seeded it is still unattributed, but every mechanism that let it propagate, amplify, or go unnoticed is closable, and this closes them. **Panes now know their owner.** `Spawn` can carry the workspace the pane is created for; the daemon stores it immutably and reports it in `List`'s `PaneInfo.owner`. Restore refuses to re-attach a pane another workspace owns (`pane_attachable`) — before this, a saved id landing on somebody else's live pane attached silently, which is how one window could pick up another's shells. The field rides a new `SPAWN_OWNED` frame with a struct payload (the legacy spawn payloads are positional tuples an old daemon cannot grow), gated on a new `pane-owner` feature string: a client only sends it to a daemon that advertises it, so the legacy kinds stay byte-for-byte what old daemons expect. A pane with no recorded owner stays attachable by anyone — that is the pre-field behavior, not a new risk. **Saved pane ids are bound to the daemon process that issued them.** `DaemonVersion` now carries an `instance` minted once per process (the local twin of the control hello's), the GUI caches it at the `ensure_running` handshake, and each local workspace records it as `daemon_instance` beside its layout. Claiming a workspace whose ids came from a different instance blanks them first: daemon pane ids restart from 1, so after a reboot every saved id points at whatever unrelated shell holds the number now, and the aliveness check cannot tell a survivor from a squatter. A blank on either side means "cannot tell" and never trips it. Unlike the duplicate-claim case below, this path keeps the agent resume — the pane is genuinely gone with its daemon, and the fresh shell resuming the conversation is the feature. **A duplicate claim loses its agent resume along with its pane id.** `dedupe_pane_ids` kept the loser's layout *and* its `agent_session_id`, so the blanked leaves took restore's spawn-fresh path and auto-typed `claude --resume` for conversations the winning workspace's panes were still running — the doubling above. The winner keeps the panes and the resume; the loser keeps only cwds. **Cross-workspace saves are caught at the write.** Every terminal view remembers the workspace whose window created it, and `save_session` logs an error naming both ids if a window ever records a pane created for a different workspace — the tripwire for the still-unattributed seed corruption, so a recurrence is caught in the act instead of reconstructed from `session.json` archaeology days later. Wire compatibility both ways: `PaneInfo.owner`, `DaemonVersion.instance` and `Workspace.daemon_instance` are `#[serde(default)]` struct fields (old peers' JSON decodes, new fields are ignored by old readers), and `SPAWN_OWNED` is feature-gated as above. `daemon_instance` is client-owned in the design-§10 storage split — it names the local daemon, and the field-census test pins the classification. * fix(session): resume the agent when a local pane dies mid-restore `session_to_pane` decided whether to send a coding agent's `--resume` from `restore.is_none()` — i.e. from whether the pane looked alive when the restore started. But `alive_panes_on` runs one `List` at the top of the restore, while the attaches happen per leaf afterwards. A pane that exited in between failed its attach, fell back to a fresh shell inside `spawn_shell_terminal_in`, and then landed in the `restore.is_some()` arm: an empty shell with its conversation dropped. `ShellParts.restored` already answers this exactly, and the remote path already reads it in `land_pane`. Carry it onto `TerminalView` so the synchronous local path can read it too, and branch on that instead of re-deriving the answer from a set that may be stale by the time it is used. No behaviour change on the paths that were already correct: a view that was never restoring anything reports `restored: false`, which is the same answer `restore.is_none()` gave them. * fix(remote): check the server instance against the record, not just memory A remote workspace's pane ids were only guarded against server restarts by `RemoteLinks::instances`, an in-memory map. On the first connect after the client starts, every machine is a first sighting, so `server_restarted` answers false — and a `tty7-server` that was replaced while the client was closed sails straight through. Its pane ids restart from 1, so the saved ones now name unrelated shells, and the reconnect attaches to them: the exact id-reuse failure the local side already guards against. `Workspace::daemon_instance` was local-only for the stated reason that a remote server's identity is tracked live per connection. That tracking is correct but not sufficient — it cannot survive the client restart that makes the question worth asking. So the field now means the same thing on both sides: which process minted the pane ids in this record. `WorkspaceStore::serving_instance` picks the local daemon or the far machine's server depending on the workspace, and `finish_attempt` compares it per workspace before deciding to re-attach or rebuild. It stays client-owned: it records what *this* client last saw, so two clients on one remote workspace each keep their own and neither may overwrite the other's. An unreachable machine still records nothing, which is what keeps a good stamp from being erased with `None` — that would disarm the next check. Also in these three files: the §N references to the deleted design docs, cleaned up as part of the sweep in the following commit. * docs: drop the references to the deleted design documents The three documents this branch removed were cited ~280 times: `design §10`, `contract §8`, `§17` and friends in comments, five references by file path in code and manifests, five in CI workflows and one in the release skill. Every one of them now points at nothing. Rewritten rather than merely stripped, because most were not decoration: "design §10 makes the remote's `workspaces.json` the authority" becomes a statement in its own right, and the several that carried a Chinese phrase from the document as their justification say the same thing in English instead. Where the reference was purely parenthetical it is simply gone. Not touched: `PRD §7.1`, `brief §8` and the like, which name documents this branch did not remove and were already external before it, and the `RFC 4648 §10` test-vector citation, which is a real specification. The `host boundary` CI job loses `(§10.6)` from its name. It is not one of the required checks, so branch protection is unaffected. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
12d8cf549b |
fix(ci): read the AppImage version from [workspace.package]
The crate split left the root manifest leading with `version.workspace = true`, so `grep -m1 '^version'` returned that line verbatim and the sed fell through unchanged. Every AppImage since was named `tty7-version.workspace = true-linux-x86_64.AppImage`, which broke the nightly publish job at `sha256sum -c` — the space-split name resolved to three missing files. bundle-linux.sh, bundle-macos.sh and bundle-windows.ps1 were already anchored on `= "`; this was the one that got missed, and the only one without a guard to catch the miss. |
||
|
|
208454e202 |
feat(remote): remote workspaces — a window that is one machine
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`.
|
||
|
|
3c36632cbe |
fix(release): assemble the release as a draft once every platform is green
The four platform jobs each ran softprops/action-gh-release, so the first one to finish published a release carrying only its own assets. That release immediately became /releases/latest, which the in-app update check polls — users were prompted to download a version whose assets were still being built, and macOS users in particular could open the page minutes before a .dmg existed. A permanently failed platform left the gap forever. Build jobs now hand their bundles to a single draft-release job via upload-artifact. It runs only after all four succeed, and assembles a draft: drafts are invisible to /releases/latest, so nothing is advertised until the release skill has verified the six assets, written the notes, and published it by hand. This mirrors the shape nightly.yml already used. |
||
|
|
a42ed82a14 |
ci(release): build the tagged commit with --locked too
The release workflow is a plain checkout of the tag — nothing rewrites Cargo.toml there, so the lockfile guard the CI build just gained applies just as well, and a release is the build you least want silently re-resolving dependencies. Only nightly stays unlocked: it stamps Cargo.toml's version, which makes the lock's own root entry stale by design. |
||
|
|
9c90044c05 |
chore(deps): resync Cargo.lock with Cargo.toml and lock it in CI
Two lockfile-only dependabot bumps (#139, #140) raised resvg to 0.47.0 and sha2 to 0.11.0 in Cargo.lock without touching Cargo.toml, which asks for `resvg = "0.45"` and `sha2 = "0.10"`. Under cargo's 0.x rules the minor version is the major, so neither requirement accepts the locked version and the lockfile has been self-contradictory ever since: $ cargo metadata --locked error: cannot update the lock file ... because --locked was passed Nothing failed loudly — CI never passed `--locked` — so the cost landed on contributors instead: every local cargo invocation rewrote the lock, leaving a permanently dirty working tree to discard before each commit. Resyncing drops the duplicates too. gpui-component already pulls resvg 0.45.1, so the tree no longer builds two copies each of resvg, usvg, tiny-skia, tiny-skia-path, kurbo, svgtypes, roxmltree, imagesize and polycool. CI now builds and tests with `--locked` so the next such drift fails in the PR rather than in a working tree. The release and nightly workflows keep their unlocked builds on purpose: both stamp Cargo.toml's version and depend on cargo refreshing the lock's root entry. |
||
|
|
e34da7a36e |
Merge pull request #138 from l0ng-ai/dependabot/github_actions/actions/upload-artifact-7
ci: bump actions/upload-artifact from 4 to 7 |
||
|
|
b9f6cbb78a |
ci: bump actions/upload-artifact from 4 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> |
||
|
|
0577f7d481 |
ci: bump actions/download-artifact from 4 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> |
||
|
|
17097ea207 |
fix(nightly): upload only packaged artifacts, not dist/ intermediates (#116)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
8209fa1823 |
feat(nightly): unattended nightly build channel with prerelease-aware update check (#114)
* feat(nightly): unattended nightly build channel with prerelease-aware update check * fix(nightly): upload assets before pruning stale ones; pin nightly-to-nightly no-prompt in tests --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
e44855c3b6 |
feat(ssh): support Unix GSSAPI auth (#81)
* feat(ssh): support gssapi auth * fix(ssh): pin the russh patch to an exact rev + fail on a stalled gssapi context - [patch.crates-io] now pins rev 0d1d073 instead of tracking the fork's branch: russh is the credential-handling SSH protocol layer, and a moving branch would let `cargo update` silently pull unreviewed code. Documented the removal condition (upstream russh PR #737 releasing). - gssapi_step: an incomplete context with no output token used to claim GssapiStep::Complete without a MIC, which servers reject with an opaque failure; return an error naming the stall instead. - auth.rs module doc: include gssapi-with-mic in the Auto ordering. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
eb6c7ca3b3 |
fix(release): stop {tmp} from closing the Windows installer [Code] comment
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.
|
||
|
|
90515d9fc3 |
fix(windows): stop the daemon before install/uninstall so it can replace tty7.exe (#72)
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>
|
||
|
|
b5beba6d80 |
feat(release): ship a Linux AppImage alongside the tarball (#55)
* feat(release): ship a Linux AppImage alongside the tarball The Linux release was a bare, dynamically-linked binary built on ubuntu-latest, so it only reliably ran on Ubuntu — Fedora/Arch users hit missing/mismatched runtime libs. Add an AppImage that bundles the x11/wayland/xkb/fontconfig/freetype libs so it launches across distros. - bundle-appimage.sh: linuxdeploy populates an AppDir + deps, completions go beside the binary (usr/bin/completions, matching signature.rs's current_exe lookup), appimagetool packs it. Runs FUSE-less on CI. - release.yml: new "Package Linux AppImage" step after the tarball, libfuse2/file added to the Linux deps, *.AppImage added to the upload list. The AppImage step avoids `rm -rf dist` so the tarball survives. - README (en + zh): recommend the AppImage, keep the tarball as the bare fallback. Note: glibc is not bundled, so ubuntu-latest still sets the glibc floor. * fix(release): downscale AppImage icon to a resolution linuxdeploy accepts linuxdeploy rejected the 1024x1024 app-icon.png (its valid list tops out at 512). Resize to 256x256 with ImageMagick's convert (added to the Linux apt deps) before handing the icon to linuxdeploy. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
d0cf53522f |
feat(release): ship a Windows Inno Setup installer alongside the portable zip
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. |
||
|
|
22e1ab1694 |
tty7: a GPU-rendered, daemon-backed terminal in pure Rust
tty7 is split into two Rust processes: a persistent daemon that owns the shells and a GPU-rendered client that talks to it over a local socket. Because the shells live in the daemon, quitting and reopening the app leaves the session intact — detach and reattach, no tmux required. - Persistent sessions — the daemon holds the PTYs and child processes, so closing a window or swapping in a new build never takes a shell down. - Performance — an 11 MB `cat` completes in 95 ms and DOOM-fire renders at 888 fps; the daemon drains the PTY at device speed off the render path. - Shell-aware — new tabs and splits open in the current working directory; zsh, bash, fish, and PowerShell are set up automatically. - Enhanced prompt — inline completion, syntax highlighting, history, and in-terminal search, with rich flag/subcommand signatures for common tools. - Tabs, resizable splits, a command palette, click-to-open links, desktop notifications, eight themes, and CJK/IME input. Native builds for macOS, Windows, and Linux. Built on Zed's gpui and Alacritty's VT core. |