The overlay could only ever show one thing one way: `git diff HEAD`, side
by side. Both halves of that are now choices.
A segmented control in the header switches between side-by-side and
unified, stored in `Config::diff_view` so the choice survives the next
open. Unified is measured against the split cell rather than designed
next to it: the same 19px row, the same type, and the same 0.12 wash
behind an addition and a removal. It differs only where the shape forces
it — 34px per line-number gutter instead of 42, and a column of its own
for `+`/`−`, without which the context lines' code would start two
characters left of everything else.
The header now says which patch it is showing. A branch name for the
worktree and for HEAD as before; the same with a STAGED chip for the
index, which is otherwise indistinguishable; the commit glyph and a
short object id for a commit or a range. The subject and author of a
commit are not there yet — `DiffSource::Commit` carries only the rev,
and buying them costs another round trip that the commit detail view
will be making anyway.
Two pieces of coupling go with it. `PANEL_DIFF_SOURCE` no longer decides
whether the panel's snapshot may seed an overlay, or whether an overlay
has gone stale: the first is settled by the snapshot's own source, the
second by the overlay's. A commit and a range never go stale at all, and
the two sources the cached `--numstat HEAD` counts cannot describe now
compare `ScmData` epochs instead — read when the probe starts, so a
write landing under it is not mistaken for one the result reflects.
And the file cards drop their private A/M/D/R table for the shared
`status_glyph`/`status_color`, so a file wears the same letter here as
in the panel and the tree.
The tree had no git in it at all. Now every row asks the repository's
`StatusIndex` what it is: the name takes the status colour, and a letter
lands in a 14px trailing cell drawn by the same `git_badge` the panel and
the diff cards use, so a status letter has exactly one look everywhere.
Directories roll up to two states and no letter — a folder is not "M",
but a collapsed folder still has to say whether there is work under it,
which is the whole reason the decoration earns its place.
The unsaved-buffer dot keeps its own column and its own shape. It is not
a git indicator and never was; round-and-warning next to the letter cell
is what keeps the two from being read as one.
Ignored rows are left exactly as they were. Italic and dim already says
everything, and a tree full of `!` is noise.
Cost per row is one hash probe and no allocation: the `Arc` and the repo
roots are taken once outside the loop, and the key is borrowed straight
out of the path on any platform whose separator is already `/`.
One cache of what each repository looks like, one way to change it, one way
to say that is now stale. The panel, the file tree's decorations and the
`.git` watcher all build on this, and they are being written in parallel —
landing the contract first is what keeps them from each inventing their own.
Kept apart from git_status deliberately. That cache answers a cheap question
for a tab badge on every cwd change and every command boundary, for every
pane. This one runs `status --porcelain=v2 -uall`, which is seconds on a
large repository, and only while something is looking. Folding them would put
the expensive probe on the cheap trigger.
Invalidation is by epoch, not by key: working out which entries a `git add`
touched is a losing game, and a counter per repository cannot miss one.
run_git_op also caps concurrent network operations per host at two. The far
side serves every request from one worker pool and keepalive's Ping queues
behind the rest of it, so enough concurrent pushes and the link is declared
dead — the client is the only place that can hold the number down.
Renames `RightPanelTab::Changes` to `Scm` in place. `#[serde(rename =
"changes")]` works in both directions, so what lands on disk is unchanged
and a build from before this commit reads the config back without kicking
anyone off the panel they left open — a fourth variant could not do that,
and 260px has no room for a fourth tab tile anyway. The action name
`ShowRightPanelChanges` stays put because `Config::keybindings` is keyed by
it, and every existing custom binding would otherwise be orphaned.
The panel body moves to `src/ui/scm/panel.rs` byte for byte; it still
renders the flat `git diff HEAD` list. Alongside it the module gets the
pieces the rest of the feature is built from: the shared status glyph and
colour tables (one definition instead of three that drift), the path and
timestamp helpers a 260px column needs, and the panel's state types.
Also wires the whole surface: fourteen actions, their key bindings, ten
palette commands in a `Git` group of their own, and the translations.
`ScmCommit` takes `secondary-enter`, which macOS already gives
`ToggleFullscreen`; the two coexist because the commit binding is scoped
to the commit box, and `every_default_chord_is_claimed_by_exactly_one_action`
now checks uniqueness per context instead of globally, which is the actual
invariant gpui enforces.
`Config` gains `diff_view` and `scm_graph_expanded`. Both default to what
happens today.
Two new icons. `git-sync.svg` is deliberately not `refresh.svg`: the panel
header already carries a refresh tile, and the same glyph meaning two
different things one row apart reads as a bug.
Adds `every_action_has_a_binding_arm`, which walks every action in
`default_bindings` rather than only the ones shipping a default keystroke.
The gap it closes is an action listed in Settings with no `make_binding`
arm behind it: the user assigns a key and the key silently does nothing.
`probe` could only ever run `git diff HEAD`. A `DiffRequest` now names the
source (worktree, staged, HEAD, one commit, a range), the pathspecs, the
context width and the budget, and `DiffSource::args` is the single place any
diff argv is built — which is also the single place to test it.
Three things were verified against git 2.50.1 rather than assumed:
- `-c core.quotePath=false` is a real bug fix, not tidiness. With quoting on,
`diff --git "a/\344\270\255\346\226\207\345\220\215.txt" …`; off, `diff --git
a/中文名.txt b/中文名.txt`. `parse_quoted_pair` does not decode octal, so
every non-ASCII path in the overlay was simply wrong. `ls-files` quotes the
same way, so the untracked listing gets the flag too.
- `Commit` runs `log -p -1 --format= --first-parent`, not `diff-tree`.
`diff-tree` does not honour `--first-parent` as a narrowing of a merge: over
a merge of two branches that each added a file, `diff-tree -p -m
--first-parent` emits both files (one patch per parent, concatenated) and
dropping `-m` emits nothing at all. `log -p -1 --first-parent` gives the one
first-parent patch for a merge, an ordinary commit and the initial commit
alike, so there is no special case and no `--root`.
- A type change is not one patch. git emits a deletion and a creation for the
same path, back to back, with nothing in either header saying why; the pair
is folded back into a single `TypeChanged` entry. `old mode`/`new mode` only
appears for permission changes, which stay `Modified`.
`FileStatus` also gains `Copied` and `Unmerged`. A conflicted path arrives as a
combined diff (`diff --cc`, `@@@`, one marker column per parent), so the body
parser reads its marker width from the hunk header instead of assuming one.
The budget is a parameter now (`DiffBudget::PANEL`, `::SINGLE_FILE`) rather
than three module constants read inside the parser. Defaults are unchanged.
On the UI side, the overlay's reuse test was `(cwd, host)` — clicking a staged
file while a worktree overlay was open took the "just move the focus" branch
and went on showing the unstaged patch under the staged file's name. The source
is part of an overlay's identity now, in that filter, in the panel-seed
shortcut and in the in-flight de-duplication key. `maybe_refresh_diff_overlay`
only compares HEAD snapshots against the cached `--numstat HEAD` counts, which
are the only counts they are comparable to.
`split_hunk` and tab expansion move to `ui::diff_rows` alongside a new
`unified_rows`, so the two renderings of a hunk are built and tested in one
place, without a window. The overlay's file list finally hangs on tty7's own
scrollbar instead of a bare `overflow_y_scroll`.
Adds the three pieces the graph section needs from the data layer: the
lane assigner, the `log --pretty` parser it is fed from, and the paged
loader that puts the two together.
`LaneAlloc` is append-only. It keeps only what has to survive a page
boundary — which oid each lane is holding a place for, and the reverse
index — so a later page extends the graph instead of re-flowing the rows
already on screen. That is only possible because `GraphRow` is
row-local: a row says nothing about the rows below it, which is also why
a long-lived branch is drawn from its first row rather than staying
invisible until the page holding its parent loads.
Colour is the lane number, fixed when the lane is created. A per-branch
counter wraps at the palette size and puts branch 0 and branch N in the
same colour, which in a three-column panel is very likely two adjacent
lines; keying on the lane makes neighbours distinct by construction. The
"one branch, one colour" half falls out of the first parent inheriting
its child's lane in place. Lanes are recycled but never compacted —
compacting would move a lane out from under a row already drawn.
The log is read with RS between records and US between fields rather
than `-z`, whose record separator is NUL and so collides with the field
separator; fields are taken with `splitn` so the body absorbs any US of
its own. Paging is a larger `-n`, never `--skip`, and `HeadAndUpstream`
resolves to shas first so a push between two pages cannot shift the
window out from under page one.
The Cargo.lock line is the missing half of tty7-core picking up
smallvec, which main already carries.
Removing network_env took its neighbouring `kind_of` shorthand with it. The
shared target directory hid this: a stale test binary from another worktree
reported green twice before an isolated CARGO_TARGET_DIR showed the ten real
errors.
run_op now routes fetch/pull/push through Host::git_with_deadline. The
no-prompt environment turned out not to belong here at all — LocalHost puts
it on every call, on both sides of the wire, so ops.rs's own copy of the
same four variables was a second definition waiting to drift. Removed it;
the conformance case git_terminal_prompt_is_disabled guards the behaviour
across local and remote, which the unit test on the constant could not.
Also corrects git_output_with_env's doc comment, which claimed read paths
must not inherit the no-prompt environment. They do, deliberately: a read
path never prompts, and a request arriving over the wire carries no bit
saying which kind it is.
Adds the parser and the probe behind the source control panel: one
`git status --porcelain=v2 --branch --show-stash -uall -z` per refresh,
read through `Host::git` so the NUL stream survives a remote workspace
byte for byte (`git_lines` would reassemble it into one giant line).
- `parse_porcelain_v2` is pure and infallible: headers, `1`, `2`, `u`,
`?` and `!` records, submodule sub-state, rename scores. A `2` record
spends two NUL tokens, so the parser parks the half-built entry and
takes the *next* record as the old path — a regression test asserts
the row behind a rename is not swallowed.
- `probe_status` costs three round trips: `rev-parse`, `status`, and one
`read_dir` of the git dir for the sequencer operation. The operation
precedence mirrors git's own `wt_status_print_state`; only a parked
rebase pays for a second `read_dir` to tell `am` from rebase and `-i`
from plain.
- `StatusIndex::build` folds a status into the file tree's lookup, and
drops the per-file map past `MAX_DECORATED_FILES`.
Two behaviours were measured on git 2.50.1 and shaped the code:
- `status.aheadBehind=false` does *not* suppress `# branch.ab` for
porcelain v2 (the config is documented for non-porcelain formats), and
`--no-ahead-behind` prints `+? -?` rather than dropping the line. So
`+? -?` parses as *unknown*, never as in sync, and the `rev-list
--left-right --count` fallback fires only when the line is missing —
unconditionally re-asking would cost a remote RPC on every refresh for
a number we almost always already have.
- `-c core.quotePath=false` is a no-op under `-z`, which disables
C-quoting on its own. It is kept for consistency with the other git
invocations, not because it fixes anything here.
`repo_home` becomes `pub(crate)` so the probe can reuse the one
`rev-parse` that already resolves root, git dir and common dir.
`push`, `pull` and `fetch` run for as long as the network takes. A remote
host sent them as a plain `Git` request, whose client-side deadline is the
20 seconds an interactive query gets, so anything slower came back as a
timeout while the far side was still working.
Add `Host::git_with_deadline`, a provided method that names the ceiling at
the call site. `RemoteHost` sends the same `Git` request it always did and
only widens its own patience, so the bytes on the wire are unchanged and a
server that predates this serves it as-is: no new verb, no version bump.
`LocalHost` does not override it — `Command::output()` has no timeout of its
own, so forwarding to `git` already means "wait until git is done".
Waiting longer is only useful if the thing we wait for cannot stop to ask a
question. Give every git `LocalHost` spawns an environment that forbids
prompting: nulled stdin was not enough, because without
`GIT_TERMINAL_PROMPT` git opens `/dev/tty` itself. The read path carries the
same environment rather than a separate one — `status` and `diff` have
nothing to prompt about, and the wire has no bit that says "this request
talks to a network", so the remote server has to reach the same rule from
the same args to be protected at all.
Guard the byte fidelity the SCM panel now leans on: `-z` output keeps its
NULs through `Host::git` (`git_lines` would rejoin the records with
newlines), and a `:(literal)` pathspec reaches git unrewritten.
Adds the three pieces ops.rs was missing: GitOp::commands, the pure argv
table every write goes through; run_op, which executes those batches and
turns a failure into something the UI can act on; and classify, which
reads git's own words to decide what the failure was.
Pathspecs always carry :(literal) and always sit after a --, so a file
named a[b].txt, HEAD or -f names itself. Long lists split at 200 paths.
Non-UTF-8 paths are rejected up front rather than sent lossily.
The old command spellings throughout, never git restore, so one argv
works on a CentOS 7 dev box and here alike; the only state-dependent
branch is an unborn HEAD, and HeadState is a parameter so the table
stays pure.
Two parallel strands of the source control work each need to add one line to
ui/mod.rs. Landing both declarations up front keeps them from colliding over
it.
The source control work needs four kinds of git knowledge — how to run a
process, what the working tree looks like, what a patch looks like, and what
the history looks like — and they were about to pile into one 386-line file.
core/git.rs becomes core/git/{mod,status,diff,log,ops}.rs. Every existing
import path is unchanged: mod.rs still holds the process layer and re-exports
nothing it did not already export.
src/terminal/git_diff.rs moves wholesale into core::git::diff, leaving a
pub-use shim so diff_overlay.rs and right_panel.rs compile untouched. It never
had a gpui dependency, and the headless server should parse a patch with the
same code the GUI does.
Also lands the shared pieces the panel is built on, all inert for now:
- RecordSplitter, LineSplitter's sibling for the two git formats that are not
newline delimited (porcelain v2 -z, and log with an ASCII record separator).
It hands out &[u8] because a path in a -z status need not be UTF-8.
- git_output_with_env, so network operations can be told they have no terminal
to prompt at without the read paths inheriting that.
- The status/log/ops type contracts: the XY pair, unmerged stages, HeadState,
RepoPath (which refuses to produce a pathspec it cannot represent), the
row-local graph edge model, and GitOp with its destructive() policy datum.
No behaviour changes. 867 core and 1036 app tests still pass.
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>
* 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>
* 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>
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>
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.
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>
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.
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>
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>
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>
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>
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>
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>
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>
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>
* 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>
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>
* 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>
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.
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>
* 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>
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>
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.
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.
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>
* 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>