mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
5e6bc1246fb8a1bf8ca4b705ba2b2062c8730ca4
648
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5e6bc1246f |
fix(workspace): stop switching workspaces from destroying live sessions (#410)
* fix(workspace): stop switching workspaces from destroying live sessions Switching workspaces rebuilt every pane it was asked to restore, and a window that rebuilt nothing then deleted the workspace outright — tree and store both. Three separate guesses, each one authorizing an irreversible act: - `session_from_tree` erased a pane's id when the tree said `live: false`. That flag is a cached observation from another process, reloaded as false on every server start, so a quiet pane read as dead while its shell was running. The restore had nothing to attach to and spawned a fresh shell over it. - Two servers could start against one config dir. `run_with` decided another server was dead by failing to connect once, then unlinked its socket and bound its own. The loser kept `control.sock` with an empty pane registry, so `MachineGet` reported every pane dead and nothing logged an error. - `finish_hydration` marked a window informed before the rebuild and without looking at the result. `tabs_from_session` drops any tab whose panes all fail to start, which is every tab when the pane socket is unreachable — leaving a window that was empty and authoritative at once, and the next switch deleted a workspace with ten live tabs. Each is now settled by whoever holds the truth: attaching decides whether a pane is there, an advisory lock decides which process is the server, and a deletion needs the machine's own mirror to agree that the workspace is empty. * fix(state): quarantine a corrupt views.json instead of silently discarding it machine.json already sets a corrupt file aside before falling back to defaults; views.json just logged and returned None, and the next save overwrote whatever the file held. Move the quarantine helpers to config so both loaders share them. Also make the no-lock-primitive fallback in the daemon singleton report Unavailable rather than Taken, so a platform without flock still gets a server instead of one that refuses to start. * fix(restore): a failed List no longer reads as every pane being dead Review follow-ups on #410, all three the same shape the PR exists to stamp out: - alive_panes_on flattened a failed List RPC into an empty alive-set, which made pane_attachable respawn every pane in the batch over its running session. The failure now surfaces as None and the attach itself decides, the way session_from_tree already leaves it to. - pub fn run() bypassed the singleton lock entirely; it had no callers, so a future one would have silently reintroduced the split-brain. Removed. - Singleton::path() and the field behind it were unused. Removed. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
9c54ccf8e8 |
fix(terminal): complete WSL panes over the distro's \\wsl$ share (#408)
* fix(terminal): complete WSL panes over the distro's \\wsl$ share Tab in a WSL bash pane always fell through to the shell: the pane's filesystem is foreign, so the completion engine had no cwd to list and handed every Tab back to bash. Now a WSL pane's POSIX cwd (OSC 7) is translated to the distro's \\wsl$ share, which this process can read like any directory: - complete_foreign lists paths against the share but keeps everything that would consult this machine switched off: no PATH binaries in the command position, no generator scripts, and `~` is left to the shell (it names the distro's home, not this machine's). - Absolute words stay inside the share: Windows join semantics keep the UNC prefix when a rooted word lands on it, so `ls /etc<Tab>` lists the distro's /etc, not C:\etc. The automount stays on the share for the same reason. - A wsl.exe pane spawned without --distribution now resolves the default distro from the registry (Lxss\DefaultDistribution), so its remote context names a real distro instead of an empty placeholder. - A WSL workspace no longer claims the SSH remote-listing path; only a spec-carrying workspace does. Verified end to end against a live Ubuntu-24.04 through an isolated daemon: the default distro resolves, cwd frames flow on cd, and the translated share lists from the Windows side. * style: cargo fmt * fix(terminal): keep the \\wsl$ completion route on this machine's panes A WSL pane owned by a remote host reaches its distro through that host, not through a local \\wsl$ share -- a same-named local distro would list the wrong machine's files. Gate wsl_share_cwd on host locality, so a remote host's WSL pane falls back to handing Tab to the shell. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
692cb76635 |
fix(windows): never join the pane reader on the UI thread (#407)
* fix(windows): never join the pane reader on the UI thread On Windows, shutdown() does not wake a thread parked in a blocking read() on the same socket (it does on unix). detach_link, adopt_relink, and Drop all shut the writer down and then joined the reader thread, counting on that wake-up - so when the peer stayed silent (a routed pane whose SSH leg went zombie: nothing arrives, no FIN ever comes), the join blocked its caller, the UI thread. That is the whole-window 'not responding' hang right after a remote workspace reconnects. Teardown now sets a per-reader quit flag and abandons the thread instead of joining it. The reader's read always times out within 500 ms so a parked reader notices the flag promptly, and it re-checks the flag under the term lock before every grid mutation, so a retired reader can never touch the freshly reset grid of an adopted link. The new windows-only tests fail on the old code by blocking >3 s in detach_link; the reader teardown previously had no Windows coverage at all (the whole test module is cfg(unix)). * fix(windows): gate every buffered frame on the reader's quit flag Review follow-up on #407: the quit checks guarded the grid and the exited flag, but a retired reader could still hold complete frames in its local buffer and process them — and most arms (Cwd, Prompt, Agent, and especially Exited via child_exited) write shared state that adopt_relink reuses across the swap, so a buffered Exited from the abandoned link could close the freshly adopted pane. Check the flag once per frame at the top of the inner loop, so a retired reader processes nothing at all; the adopt test now also fires a late Exited down the abandoned link and asserts the pane survives. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
a570cb1496 |
fix(windows): carry launch flags into agent resume commands (#406)
On mac/linux the daemon reads the foreground process's real argv, so resume_command can replay launch flags like --dangerously-skip-permissions. The Windows path detects the agent from the OSC 133;C shell-integration command capture but threw the command line away, stamping an empty argv: every resumed agent came back bare, with none of its launch flags. Tokenize the captured command (case-preserved, quotes trimmed, PowerShell call operator dropped) and stamp it as the pane's launch argv. Detection keeps lowercasing its own copy; replay_flags' safety allowlist already rejects tokens a whitespace split would mangle, falling back to the bare resume command as before. Fork commands share the same argv source, so they are fixed by the same change. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
817447bd48 |
feat(agents): recognize Oh My Pi and install its status hooks (#405)
Issue #376 asked for `omp`. Oh My Pi is a fork of Pi (can1357/oh-my-pi, descended from badlogic/pi-mono), but the fork is where the similarity stops for our purposes: it ships one binary of its own — `omp`, the only `bin` in `@oh-my-pi/pi-coding-agent`, and it never installs a `pi` — and it keeps its config under `~/.omp`. A pane running it was therefore not detected at all, and aliasing `omp` onto `CLIAgent::Pi` would have been worse than nothing: the status bridge would land in `~/.pi`, and Resume Session would offer `pi --session <id>` to a binary that spells that flag `--resume`. So it gets its own variant, wired the whole way through: | | | |---|---| | Detection | argv stem `omp`, distinct from `pi` in both directions | | Avatar | its own mark, normalized from the project's `assets/icon.svg` | | Resume | `omp --resume <id>`, opting out on `--no-session` | | Fork | `omp --fork <id>` — a verified fork command, so the menu item appears | | Hooks | Settings → Agents, at `~/.omp/agent/extensions/tty7/index.ts` | The status bridge is the one piece the fork did not change. Oh My Pi inherited Pi's extension contract intact — same default-exported factory, same `session_start` / `agent_start` / `agent_end` / `session_shutdown`, same `ctx.sessionManager.getSessionId()` — so `pi_extension_ts` now takes the agent and substitutes two things, the package it imports the type from and the slug it calls the emitter with. Pi's generated file is byte-identical to before, so no installed bridge goes stale. `--resume`, `-r` and `--session` are three spellings of one flag in Oh My Pi; all three shed when a session command is rebuilt, while `--session-dir` is a different flag and rides along. `fork_command` now honors the same `--no-session` opt-out `resume_command` already did — Oh My Pi rejects `--fork` outright under it, and no existing agent declares an opt-out. 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>
|
||
|
|
c5aaf2b4f7 |
fix(theme): floor the tokens that bypassed the contrast machinery (#400)
* fix(theme): floor the tokens that bypassed the contrast machinery Sidebar text, the caret and hairlines were flat blends with no floor, sitting one line away from tokens that are bisected to hit a target exactly. Semantic inks were floored against the window background but painted on popovers and sidebar rows, which sit a step toward the foreground. - sidebar_fg is now floored at 4.5:1 on the sidebar fill it is painted on, not on the background. Four builtins landed it at 3.35-3.92:1. - caret is conditioned to 3:1. The default Light theme shipped an orange caret on pure white at 2.07:1. - border keeps its blend but gets a 1.5:1 floor so a divider is worth the same in every theme, instead of ranging 1.26-1.64:1. - semantic inks and fills clear their floor on background, sidebar and popover, not just the background. Themes that already cleared a floor are untouched. * fix(theme): floor the hairline on every neutral fill it divides Review catch on #400: border is handed to sidebar_border and drawn on popover chrome, but was floored against the window background alone — a narrower guarantee than the one this PR gives sidebar_fg and the semantic inks. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
fe558a1c8e |
fix(terminal): localize the right-click menu and the search bar (#401)
The terminal's own context menu was the last surface still speaking hardcoded English in a three-locale app — 14 literals, while the tab and switcher menus route every item through t(). The search bar had three more, and its Previous / Next / Close buttons were icon-only with no tooltip at all. It also spoke a fourth vocabulary for one action: "Maximize Pane" for ToggleMaximizePane, which the menu bar, the palette and Keybindings all call "Zoom Pane". "Close Pane" is likewise "Close Pane / Tab" everywhere else, and it is the accurate name — the action closes the tab when the pane is the last one. Adds AppMenuSplitLeft / AppMenuSplitUp and SearchFind / SearchMatchCase / SearchUseRegex across en, zh and ja. The fork submenu now takes its title from the locale table instead of tty7-core's fork_label(), which is a capability probe in a crate that has no locale table. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
1f91719da9 |
fix(terminal): paste images into remote panes on macOS, and into WSL by its own path (#399)
Staging a pasted screenshot to a file and handing the agent its path was built off macOS only. A local macOS agent reads the system clipboard itself when it sees SYN, and that carries the image at full fidelity, so the platform kept SYN and skipped staging entirely. That reasoning stops at the pane boundary. An agent in an SSH pane or a remote workspace reads the clipboard of the host it runs on, which never holds this machine's screenshot, so SYN was a no-op and pasting did nothing at all. Remote panes now stage and upload on every platform, decided by one predicate instead of a platform cfg over the whole feature; a local macOS pane still forwards SYN untouched. macOS screenshots reach the pasteboard as TIFF, which agent vision rejects the same way it rejects a Windows BMP, so those transcode to PNG on the way out. A WSL pane was broken for a different reason: it shares this machine's disk but not its path syntax, so it needs no upload, only a rewrite -- and there was none. The agent got C:\Users\...\paste-1.png and found nothing there. The paste now carries the automount view, /mnt/c/Users/... A path with no mapping, such as a UNC temp directory, keeps the Windows name, which at least says where the file went. Seven pre-existing tests for this feature were compiled out on macOS by the same cfg and now build and pass there. Three were added: the staging predicate, the WSL rewrite (including UNC and drive-relative refusals), and the TIFF-to-PNG transcode, whose failure mode is otherwise a silent no-op. |
||
|
|
e4fff64833 |
fix(daemon): stop offering to carry on beside a mismatched server (#389)
Both handshakes compare their version for equality and hang up on anything else — the pane protocol in `ensure_running`, the control dialect in `host::server`'s hello. So a server whose number disagrees cannot be talked round, and the "Keep Shells" button offered a state that does not work: panes still spawn while every machine-tree call is refused, which is how a window opens with no tabs and saves none of the ones you make. Taking that button was indistinguishable from the bug it sat next to. Restart or quit, then. Quitting is the half that destroys nothing — the server and every shell under it keep running — which makes it a real answer for someone who would rather go install the matching build than lose a session mid-flight. Quit goes first, the way every other destructive prompt here is ordered: NSAlert and TaskDialog give the first button Return, and this prompt arrives unasked at launch, the moment a stray Return is likeliest, so the reflex key has to be the one that destroys nothing. Restarting stays a click or an arrow away. A prompt dismissed without an answer re-arms instead of falling through: `take_mismatched_daemon` takes, so without this a closed window is a back door into the state this prompt exists to prevent. The copy is rewritten to one line of diagnosis and one line per button, in all three locales, listed in button order. It was a paragraph that had to be read twice to find out what the buttons did. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
43a22855a2 |
perf(graphics): move kitty frame pixels instead of copying them (#388)
A re-transmitting sender like terminal-browser sends a fresh full-window frame per rendered frame — ~26 MiB of RGBA at Retina resolution. On the client that buffer was copied twice for no reason on the way to the atlas: `decode_frame` allocated a new Vec for the payload tail behind the 30-byte header, and the uncompressed path of `to_rgba8` then cloned it again before the in-place BGRA swap. Thread ownership through instead: - `Image::decode_frame_owned` consumes the frame Vec the reader already owns off the socket and drains the header off the front, reusing that allocation as the pixel buffer rather than allocating and copying a fresh one. - `Image::take_rgba8` moves the pixel buffer out on the uncompressed fast path (the shm/file transport hands us pixels already in `f=32` layout), so `decode` swaps R<->B in place with no clone. The compressed inflate, the PNG guard, and the declared-dimension inflate bound are unchanged; `f=24` still repacks because RGB->RGBA changes the length. Removes two ~26 MiB per-frame touches on the client hot path. On a 3216x2160 frame the decode+normalize step drops from ~1.78 ms to ~0.89 ms — ~0.9 ms saved per frame, ~53 ms/s at 60fps. This does not touch the wire frame layout or the daemon-side transfer; it is a pure client-side allocation cut. |
||
|
|
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> |
||
|
|
d992d6d9a9 |
Smooth scrolling: spread a wheel detent over frames instead of jumping it (#382)
* feat(terminal): animate wheel scrolling instead of jumping a notch at once Sub-line scroll positions were already in place — the view keeps a fractional remainder and paints the grid shifted by it — but the position was a function of the event, not of time. A notch arrived and the whole distance was applied at once, so whether it looked smooth came down to how fine-grained the platform's deltas happened to be. A macOS trackpad reports pixels, so it did. A wheel on Windows reports whole lines (gpui multiplies the notch by the system's scroll-lines setting, three by default), the fraction came out zero every time, and the view jumped three lines per notch. The sub-line machinery was present and never engaged. A 120-step notch is one discrete pulse; no arithmetic on the delta recovers a continuous gesture from it. So make position a function of time: a notch adds to a remaining distance and each frame consumes a share of what is left, ~120ms to land, exponential, with a sub-pixel remainder snapped rather than approached since every frame of it costs a repaint. Line deltas are discrete and get animated; pixel deltas are continuous and do not — putting an animation between a trackpad and the grid would only add lag. Mouse reporting and alternate-scroll keep forwarding whole lines, which cannot be spread over frames either. The distance in flight is relative rather than an absolute target, so output arriving mid-scroll shifts the grid without dragging the animation elsewhere. Everything that moves the view on its own cancels what is in flight first. Settings -> Terminal -> Scrolling -> Smooth scrolling, on by default. Also pin the scratch config dir in the terminal-view test harness: building a view reads the config, and which test got there first decided whether that touched the real user directory. * fix(terminal): tell a trackpad from a wheel by phase, not delta type The first cut split the two on the delta variant — Lines meant a wheel, Pixels meant a trackpad. That holds on Windows and Linux. It does not hold on macOS, which sets hasPreciseScrollingDeltas for a wheel mouse as well, so a wheel reported pixels, took the trackpad path, and was never animated. Measured on this machine, one detent arrives as a single ~103px event — about five lines at a 21px line height, applied in one go. That is a worse jump than the three lines Windows produced, and it was invisible to a check based on the delta's type. Size cannot separate them either: a trackpad flick reaches ~3 lines in one event while an inched wheel moves ~0.6, so the ranges overlap and any threshold misclassifies a quarter of the trackpad stream. Phase can. Only a device that can gesture ever reports Started/Ended; a wheel is Moved forever, on every backend. So track the gesture instead — and hold it open on a 150ms idle timer rather than closing it on Ended, because lifting the fingers is not the end of the stream: the momentum tail keeps delivering Moved events larger than the gesture that spawned them, and animating those would smooth what the system already smoothed. Also skip the animation for jumps under a line. Inching a wheel one detent at a time lands there, and spreading a half-line move only adds lag to something that already reads as continuous. Tests are now shaped after the measured event streams rather than after an assumption about which variant each device sends. * chore: trigger a Claude review of this branch --------- 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> |
||
|
|
f1f632edd1 |
fix(daemon): notice a server that is only a control dialect behind (#387)
* fix(daemon): notice a server that is only a control dialect behind The pane protocol and the control dialect are versioned apart, and the launch check only compared the first. A server from before the v4 to v5 control bump answers the pane handshake with this build's own number, so it was waved through as ours while every machine-tree call was refused: the window opened with no tabs, the only trace a log line the default config does not write anywhere. Ask the control socket too, before calling the server ours, and say which version disagrees when offering the restart. A window that still opens empty now says why in the window it happened to. * fix(daemon): do not call a newer server old, and say the empty-window reason once The dialect handshake reports disagreement, not direction, but the restart prompt read every mismatch as "from an older build". A daemon left running by a newer build passes the pane-protocol check the same way an older one does, so the prompt told users their newer server was old — the same wrong guess the remote path stopped making in #384. Split the copy in two and pick by which side is ahead, as `dialect_complaint` already does for remotes. The window's own explanation had two ways to misfire. A hydration superseded by a newer one still announced an emptiness someone else was already filling, because only `owe_rehydration` checked the epoch; it now reports whether it claimed the debt and only the owner speaks. And since a failed pull is retried from every `sync_window`, a machine that never answers repeated the same notification every fifteen seconds — latched to once per window, cleared when a pull finally lands so a later outage still gets a word. --------- 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. |
||
|
|
fb4f008828 |
fix(remote): say a version-mismatched server is old, not that it is not tty7 (#384)
A handshake refused on the control dialect was shown with the protocol layer's own wording — "java answered, but not as a tty7 server: control peer (build …) speaks control v4, this build speaks v5". The far end is tty7; it is a build on the other side of a dialect bump, and the reader cannot act on the dialect numbers either way. Restate it as which side is behind, and name the action on the button: "Update Server", not "Restart Server". The failure that followed was also invisible. The switcher paints a failed `connect` in preference to `remote_host_errors`, and restarting or replacing a server cleared only the latter, so whatever went wrong during the install was covered by the complaint that started it — the button read as doing nothing at all. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
6773a8c366 |
refactor(settings): drop the close-confirmation setting and trim About copy (#383)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
b9956ad6af |
feat(switcher): two-column panel with IDEA-style Ctrl+Tab (#380)
* feat(switcher): two-column panel with IDEA-style Ctrl+Tab
The switcher listed workspaces only; reaching a tab inside one meant
opening it first. It is now two columns — workspaces on the left, the
tabs of whichever one the cursor sits on to the right — and Ctrl+Tab
raises it as a most-recently-used tab switcher that commits when the
modifier comes up.
Picking a workspace or a tab now switches this window in place. A second
window is something you ask for, with the platform modifier or "Open in
New Window", rather than what happens by default.
New workspaces get a codename ("amber-yak") instead of inheriting
whatever directory their first shell started in. The generator moves out
of worktree.rs into core::codename so both callers share it.
* fix(switcher): review fixes — platform-gate the Ctrl+Tab key test, park the new-window tab
- The key_intent test asserted only the macOS half of the Tab chord; off
macOS Ctrl *is* the secondary modifier, the raw key falls through on
purpose (the chord arrives as NextTab), and the test now says so
instead of failing there.
- open_at_tab probed a freshly opened window's tabs exactly once, but a
new window hydrates them asynchronously — route through
activate_tree_tab so the pick parks until the tab arrives.
- Escape mid-rename backs out of the rename instead of tearing down the
whole panel.
- A one-tab workspace reads '1 tab', not '1 tabs'.
Claude-Session: https://claude.ai/code/session_01V7xjxdUGWQp93KQDuFuh8d
* style: cargo fmt
Claude-Session: https://claude.ai/code/session_01V7xjxdUGWQp93KQDuFuh8d
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
|
||
|
|
05bfa8a269 |
feat(ui): add ja-JP locale and split i18n into per-language modules (#372)
The single en/zh tuple table becomes one module per language behind a `SUPPORTED_LANGUAGES` table, and Japanese joins English and Simplified Chinese. - `gui_language` accepts `ja-JP`; anything unrecognized still falls back to `en`. - The language picker and `refresh_locale_state` both read `SUPPORTED_LANGUAGES` instead of keeping their own copy of the code list. - Language names in the picker stay endonyms (English / 简体中文 / 日本語) in every locale, as English and Chinese already were. - The zh and ja key tables are exhaustive, so a new `L10nKey` fails the build until it is translated rather than silently rendering English. Co-authored-by: Chihiro WATANABE <chihiro.watanabe@live.jp> |
||
|
|
bb72be338d |
fix(windows): respect system proxy for remote server downloads (#364)
The GUI update check already uses reqwest, which reads the Windows system proxy from the registry by default. The remote server installer / bundled-server fallback uses ureq, which only reads HTTP_PROXY/HTTPS_PROXY environment variables unless the win-system-proxy feature is enabled. Enable ureqs win-system-proxy feature so that release downloads inside the daemon also honor the Windows system proxy set by tools like Clash (System Proxy mode), v2rayN, etc. This is a no-op on non-Windows platforms. Fixes the inconsistency where the update check could reach GitHub through the proxy but the actual download would time out trying to connect directly. |
||
|
|
4a8a4bcbaa |
feat(proxy): macOS system proxy, Windows SOCKS parsing, manual override (#367)
Resolve an HTTP/SOCKS proxy for tty7's own update checks and release downloads, from (in order) a new `http_proxy` config field, the platform system proxy — Windows registry / macOS SCDynamicStore — and the HTTP_PROXY/HTTPS_PROXY/ALL_PROXY environment variables. Programs running in a pane are deliberately unaffected: they inherit their proxy from their own environment, as in any other terminal. Fixes #365. |
||
|
|
a7de7db2c4 |
feat(windows,macos): clickable toasts, richer context, and i18n (#373)
Desktop notifications now carry the pane they came from: clicking one reveals that pane's window, tab and split. Windows shows a WinRT toast with an `Activated` handler, macOS uses mac-notification-sys' click response, and both route through the existing tray dispatch channel. Linux keeps the plain notify-rust path. Titles gained context — an agent name or the machine, then the workspace — and bodies name the command or agent alongside the duration, all of it translated. Notification text is sanitized on every path: it comes off the terminal, and a stray control byte used to make the Windows toast XML fail to parse and lose the notification outright. Co-authored-by: Hongwei Qin <exqinhongwei@outlook.com> |
||
|
|
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> |
||
|
|
17c0ea2935 |
fix(terminal): honour application cursor key mode (#366)
* fix(terminal): honour application cursor key mode (#361) Arrow, Home and End were always sent as their CSI form, no matter what the foreground program had asked for. Programs that turn on DECCKM via smkx -- which is every ncurses full-screen app -- expect the SS3 form, because that is what xterm-256color spells kcuu1 & co. as, and ncurses matches terminfo byte for byte. htop was the report: ncurses failed to match \E[A, handed the bytes to htop one at a time, and htop binds `[` to "lower priority", so every Up or Down bumped the selected process's nice value instead of moving the selection. The same breakage hits ncdu, mc, dialog, menuconfig, nmtui. Shells were unaffected because readline and zle bind both forms, and the mouse wheel already got this right in wheel_route(). Named keys now also carry their modifiers the way terminfo declares them (kLFT=\E[1;2D, kUP5=\E[1;5A, kDC3=\E[3;3~), instead of dropping Shift/Ctrl entirely and prefixing Alt with a bare ESC. Cmd stays out of the modifier parameter -- xterm has no encoding for it. The legacy and kitty encoders shared this table already; they now share one function, so both follow DECCKM. KittyFlags grew past its name and becomes KeyFlags. Verified in a dev instance: with DECCKM on the arrows arrive as ^[OA ^[OB ^[OD, with it off as ^[[A ^[[B ^[[D. * test(terminal): guard the SS3 cursor walk-back on line handoff --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
0190036a2e |
chore(ui): remove the Outline right panel (#375)
Removes the Outline right panel and all of its wiring: the `RightPanelTab::Outline` variant, the `ShowRightPanelOutline` action and its handler, the tab-strip button, the palette command, the keymap arm, `render_panel_outline`, `TerminalView::{command_marks, scroll_to_mark}`, the `one_line` helper, and four i18n keys.
An existing config value of "outline" falls back to the default Info tab via `de_lenient`, and a user keybinding naming `ShowRightPanelOutline` degrades to a logged warning rather than breaking the keymap.
Also drops what the removal stranded: `Marks::list` is now `cfg(test)` (its last production caller was `command_marks`), and `icons/list.svg` — the Outline button's icon, with no other user — is gone.
Closes #374
|
||
|
|
e1531cdea6 |
revert(windows): drop the taskbar status dot (#377)
The per-window taskbar overlay badge (#355, for #199) is removed, and with it the in-flight follow-up that was making its green "finished a turn" state reachable: the feature is not wanted. Nothing shipped — the badge only ever existed in Unreleased — so this is a plain removal rather than a deprecation, and its CHANGELOG entry goes with it instead of gaining a "Removed" counterpart. What goes: `ui::taskbar` and its `ITaskbarList3::SetOverlayIcon` poll, the `taskbar_status_icon` config flag and its Settings → Window & Tabs row and strings, `Tty7App::taskbar_signals`, `TerminalView::shell_busy` / `RemoteTerminal::shell_busy` (the overlay was their only caller), the `raw-window-handle` dependency and the `Win32_UI_WindowsAndMessaging` feature it needed, and the feature docs in both languages. A stale `taskbar_status_icon` left in someone's `config.json` is ignored, as any unknown key is. The tray badge and the in-window status dots are untouched; they were always the ones the taskbar was mirroring. |
||
|
|
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>
|
||
|
|
4dbbe95be9 |
refactor(settings): trim the About page down to what it is for (#358)
The About page carried a marketing paragraph and two multi-sentence explanations that walked through updater internals and per-platform support. None of it helps someone who is already running the app. Drop the feature-list paragraph, cut the update and server text to a single sentence each, move the tech credits to the bottom of the page, and render the update toggle with settings_row so it matches every other switch in Settings. The tagline and credits line now match the README. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
0c61e533b3 |
fix(terminal): put back the cursor ConPTY parks at the end of a repaint (#362)
conhost's VT renderer brackets every frame it paints with `?25l` … `?25h` so
the cursor does not flicker across the repaint, and it moves the cursor
explicitly just before the `?25h` only on the frames where it painted the
cursor. On the other frames the show commits wherever the last erase or write
left it — the tail of the status line, the head of a row — and the cursor
blinks there until conhost's next frame moves it back.
tty7 repaints when a batch of pty output lands, so it draws that cursor for a
frame. A TUI that repaints on a spinner produces one every tick, which reads as
a second cursor blinking in the wrong place. macOS never shows it: no ConPTY
sits in between, and the TUI itself always moves the cursor before showing it.
Measured on Windows 11 26200 from a raw ConPTY capture of a Codex session, a
110x30 pty, cursor-visible dwell per cell:
in-box conhost: 295 ms across 42 frames parked at the end of the status line,
each stray corrected 7-15 ms later by the following frame
with this fix: that cell never appears; those frames fold back into the
composer cell the repaint hid the cursor on
A scanner over the stream pairs the hide with its show and marks the show as
parked when the run in between moved the cursor around to paint but did not end
on a move — nothing chose the cell it is about to appear on. The repair then
restores the cell the cursor stood on when it went invisible, which is where the
correcting frame would have put it anyway. A hide and a show more than 100 ms
apart are an application keeping the cursor off for the length of some work, not
a renderer bracketing one frame, and are left alone.
This does not cover the other ConPTY cursor artifact: conhost also samples an
application's partially written frame, and then it *does* emit an explicit move,
so the stray position is genuine — just transient — and nothing in the stream
tells it apart from a real one. Only a render-side settle would catch that.
Microsoft's ConPTY redistributable avoids both, so with the bundled pair beside
the daemon this is inert; it earns its keep on hosts without it, notably a
remote Windows server.
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>
|
||
|
|
7c0598c7e0 |
fix(bell): ring the system bell on Windows (#359)
ring_system_bell() was macOS-only and returned false everywhere else, so on Windows and Linux the Audible mode fell straight through to its visual fallback: Visual, Audible, and the new Both were three names for one behavior. Windows has MessageBeep, so two of those three now differ. MB_OK plays the "Default Beep" scheme entry, which follows the user's choice in Sound Settings rather than synthesizing a fixed tone at the speaker the way Beep() does. The Win32 metadata files MessageBeep under Diagnostics::Debug despite it being a user32 export, hence the extra windows-sys feature; no new crate and no dbghelp. Linux is left on the flash fallback on purpose: libcanberra and PipeWire are runtime links away and XBell does nothing under Wayland. Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
27bb1864df |
feat(windows): taskbar status overlay per window (#355)
* feat(windows): taskbar status overlay per window (#199) Stamp a colored status dot on each window's taskbar button using the same palette as the in-window agent dots: - blue while a shell command or agent is working, - amber when an agent is waiting on the user, - green when work finishes while the window is unfocused (cleared on activation). Adds a `taskbar_status_icon` setting (default on, Windows only) and a Settings -> Window & Tabs row. The overlay is updated by a foreground poll that aggregates agent status and shell busy state across each window's panes, diffing against the current taskbar badge and only calling ITaskbarList3::SetOverlayIcon when the badge changes. Includes unit tests for overlay priority and the done-while-unfocused edge tracking. * fix(taskbar): retry a failed overlay instead of caching it as drawn Four fixes on top of the overlay: - A failed SetOverlayIcon was still recorded in `shown`, so a badge the taskbar never took was remembered as drawn and never retried. Stamp now reports success, and a failure drops the interface so the next tick re-creates it — which is also what an Explorer restart needs. - `create_failed` was a permanent latch: one CoCreateInstance failure killed the badge for the whole process, though Explorer may simply not be up yet when the first window opens. Use the tray's attempts/cooldown backoff instead, which this module otherwise copies. - The overlay's accessibility description was hard-coded English in an app that localizes everything else. Reuse the panel and tray strings. - Render the dot at 32px, not 16. SetOverlayIcon wants 16x16 at 96 dpi, so at 150%/200% scaling the shell upscaled a 16px icon; `tray::icon` already renders at 32 off macOS for the same reason. Also drops the Win32_Graphics_Gdi feature: CreateIcon, DestroyIcon and HICON all live in Win32_UI_WindowsAndMessaging, and the build and the taskbar tests pass without it. Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Co-authored-by: l0ng-ai <ysdpk123@gmail.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> |
||
|
|
fcd8f00be6 |
feat(bell): add combined visual + audible terminal bell mode (#357)
* feat(bell): add combined visual + audible terminal bell mode Currently BellMode only offers None, Visual, and Audible. Audible falls back to Visual if the system bell cannot be rung, but there is no way to intentionally get both at once. Add a `Both` variant that rings the system bell *and* flashes the pane, exposing it as a fourth option in Settings -> Terminal -> Bell. Existing `none`/`visual`/`audible` values remain backward compatible. Updates config serialization tests and i18n keys/translations/test list. * fix(bell): align the settings copy and picker with the new Both mode The bell description still enumerated three outcomes and the settings search keywords omitted both, so the new mode was invisible to search and contradicted by the row that labels it. Drop the picker's catch-all onto the default instead of Both, which is the shape the PR just fixed. Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
2c462dd027 |
fix(windows): refresh the environment for newly created panes (#349)
Windows hands every process a private copy of the environment block at `CreateProcess` time and never updates it. A tty7 daemon that has been up since before an installer edited `HKCU\Environment` therefore gave a brand-new pane its startup `PATH`, and the freshly installed command was unresolvable until tty7 restarted (#333) — while a Windows Terminal launched from Explorer found it, because Explorer rebuilds its own block when it sees `WM_SETTINGCHANGE`. Rather than chase broadcast messages, `daemon::windows_env` re-reads the two hives Windows itself composes a process environment from — the machine `Session Manager\Environment` and `HKCU\Environment` — at the moment a pane is spawned, and pins the merge onto the pane's command. The merge is a pure function over (machine, user, process, configured overrides), so every semantic that matters is unit-testable without a registry: - Names are keyed case-insensitively, so a `Path` from the process block and a `PATH` from a hive collapse into one variable instead of reaching the child as two. - `PATH` and `PSModulePath` are *combined* — machine value first, user value appended — which is what Windows does and what keeps a per-user install from shadowing the system half. Nothing is ever written back to the user hive; that would bake the machine half into it permanently. - `REG_EXPAND_SZ` values are expanded against the merged map, so a user value naming a machine value naming a process value resolves. A reference that resolves to nothing is left verbatim, as Windows leaves it, and the chain is depth-bounded so a self-referential value cannot hang the spawn path. - The machine hive's `USERNAME=SYSTEM` is dropped, as Windows drops it. - Directories the daemon's own `PATH` held that neither hive lists stay reachable, appended behind the registry entries: freshening `PATH` should only ever add resolvable commands, never take one away. - Configured `env` overrides are applied last and win outright. Non-Windows builds are untouched: only the registry reader and the spawn wiring are `cfg(windows)`, while `cfg(test)` keeps the pure merge compiling everywhere so its tests run on every platform. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
964576040f |
fix(shell): point $SHELL at the shell the pane actually runs (#348)
A pane configured to run fish still advertised the login shell, because pane_environment() injected TERM, the TTY7_* markers and TERM_PROGRAM but never touched SHELL -- so the pane inherited the GUI session's login-time snapshot of it. Everything that spawns "the user's shell" read that: tmux's default-shell started zsh inside a fish pane, and so did sudo -s, an editor's shell escape, and any coding agent picking a quoting dialect from $SHELL. The failure is silent -- fish rejects the bash line, the agent's sentinel file never appears, and the rejected text stays in the line editor to concatenate onto the next send. Inject SHELL alongside the other markers, set to the absolute path of the program the pane is about to exec. That program is read off argv rather than off the shell tty7 resolved: an argv-replacing integration injection and the parent-shell override both rewrite argv, while CommandBuilder::get_shell() keeps answering the passwd entry. Only an absolute path is ever written. A configured command may be bare (the inventory keeps it bare so PATH decides which install wins), and consumers exec $SHELL under a PATH of their own, so a bare name is resolved against the PATH the pane will inherit -- the user's env-block PATH when they set one -- and skipped when that finds nothing. A stale login shell beats a name that resolves somewhere else. An explicit SHELL in the user's env block still wins, the same precedence TERM_PROGRAM has: tty7 describes the pane, the user's config gets the last word. Windows is deliberately left out. Neither cmd nor PowerShell reads SHELL; the tools that do are the POSIX emulations (MSYS/Git Bash, Cygwin, WSL), and they want a POSIX path, not the Windows one this would have to give them. That also leaves the WSL pane alone, where the pane program is wsl.exe and the distro's own login shell is the right answer. Native SSH panes are unaffected: they never build a local command, and the remote sshd sets SHELL from the remote passwd entry. Closes #342 Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
ada89bb81a |
fix(terminal): route right-click to the app while mouse reporting is active (#347)
A TUI that turns mouse reporting on (vim with `set mouse=a`, lazygit, tmux, …) draws its own right-button menus, and tty7 was delivering one right-click to both consumers: `TerminalElement::register_mouse_handlers` forwarded the press to the application, while the `.context_menu(…)` on the terminal surface was attached unconditionally and popped tty7's own menu over the top of it. Gate the host menu on a single pure predicate, `should_show_context_menu`, and call it from both sides so one click can only ever feed one consumer: while reporting is active the unmodified right-click is the application's alone, and Shift stays the escape hatch that reaches tty7 — the same override Shift already provides for selection and for the wheel. gpui-component's `ContextMenu` element owns the right mouse-down that opens the popup: it wraps the terminal surface, so its listener fires before any handler we can attach, and the builder closure it calls gets no event to inspect. The verdict is therefore latched on our own right mouse-down (safe, because the builder runs from a `window.defer` that lands after the whole mouse dispatch has unwound) and a suppressed menu is expressed as an item-less `PopupMenu`, which that element already skips rendering. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
8a7b2f3bea |
style(terminal): increase powerline half-circle segments for smoother curves (#341)
Co-authored-by: l0ng-ai <ysdpk123@gmail.com> |
||
|
|
4cf3d4dad5 |
fix(install): prefer bundled server over release download for SSH remotes (#344)
* 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 --------- Co-authored-by: l0ng-ai <ysdpk123@gmail.com> |