Commit Graph
710 Commits
Author SHA1 Message Date
l0ng-ai feb027da1f feat(scm): show an untracked file's content when its row is opened
Focusing an untracked file in the diff overlay used to fall through to
the names-only "Untracked files (N)" card — git has no patch for a
file it does not know, and `--no-index` needs a null device whose
spelling is platform business. The overlay now reads the file's own
bytes (lazily, only the focused file, 4 MiB cap) and synthesizes the
card a parsed added-file patch would produce: every line an addition,
new-side numbers, true counts past the single-file budget, git's own
NUL-in-the-first-8000-bytes binary rule. A fresh snapshot clears the
preview so an edit shows up on the same cadence a tracked file's does;
a failed read says so instead of showing an empty file.

Found in manual acceptance of the panel.
2026-08-10 13:06:15 +08:00
l0ng-ai 58d7ef5838 fix(scm): close out the review's minor findings across the data and UI layers
The second pass over the branch review: every remaining finding verified
against the code, the real ones fixed.

Data layer:
- A truncated log parse is never called complete: RecordSplitter drops
  an overlong record whole and reports the count (delivered cut short,
  a commit body cut mid-way reads as the real message), parse_log
  carries a truncated flag past MAX_LOG_BYTES, and load_page only says
  "end of history" when the parse read everything git returned.
- Every scope pins symbolic revs to shas before walking, so a commit
  landing between two pages can no longer shift where page two starts
  under Head and Refs scopes; unresolvable names read as "no history"
  rather than as a load failure. --parents was doing nothing and is
  gone; edge sort is stable so a merge's Outs keep first-parent order.
- The lane model's central invariant now names the join case — a merge
  whose second parent already has a lane reserved sends its Out onto
  that lane, one line below the cut, not two — with a golden test for
  the commonest merge topology of all, which no golden covered.
- DiffSource revs get the same could-be-an-option guard log already
  had; C-quoted paths decode the full escape set (a tab decoded to a
  literal t broke the :(literal) re-probe); rename from/to lines
  override the ambiguous diff --git header; combined-diff line numbers
  follow the sides rather than the colour, so a " +" line no longer
  drifts every number below it.
- A rename's old path stays out of the per-file decoration map, where
  it outranked a file re-created at that path; ignored records decorate
  as Ignored, not Modified; checkout <branch> gains the trailing --
  that keeps a stale name from falling back to a worktree-clobbering
  path checkout; unstage before the first commit takes -f (worktree-
  safe with --cached); batches split by bytes as well as count for
  Windows' 32K command line; a deadline expiry reports Timeout, not
  "git could not be run"; error details keep both streams.
- probe_status distinguishes "not a repository" from "could not ask":
  a dropped link keeps the cached status (stale beats blank) and rests
  10s instead of erasing the panel, while a definitive not-a-repo also
  drops the cwd→root mappings so the panel stops drawing Loading for a
  repository that is gone. Probe and watch work are wrapped against
  panics that would wedge their in-flight bookkeeping forever, watch
  landings check the wipe counter, superseded probes relaunch through
  the debounce, and a refused network slot says so instead of eating
  the click.

UI:
- Reset --hard confirms with its own words (commits fall off the
  branch), not the discard dialog's; a merge commit whose prefilled
  message the user cleared is committable again; the disabled commit
  button distinguishes "nothing to commit" from "write a message".
- Selection highlight matches on the diff source too, so a file staged
  and edited again no longer lights both of its rows for one overlay.
- The graph materializes only the rows in the viewport window (5000
  flex children per frame was most of a frame), row clicks carry the
  page Arc and an index instead of a deep Commit clone per row per
  frame, filter results are cached per (page, query), and a selected
  merge ring's hole matches the selection band under it.
- A failed commit_files read says the list could not be read instead
  of "0 files changed"; the STAGED chip and the graph's relative
  times go through the i18n table; the keys-awaiting-a-caller list is
  pruned to the seven that still are; the orphaned PanelUntracked key
  is gone; the zh commit placeholder reads naturally.

2398 tests, 0 failures. Known flake: daemon::singleton's second-claim
test, untouched by this branch, fails ~1 in 3 full parallel runs and
passes alone.
2026-08-10 12:31:58 +08:00
l0ng-ai a764d92132 fix(scm): sequence compound verbs, cap graph paging, back off failed loads
Review findings on this branch, all in the seams between async operations:

- Commit-and-push, commit-and-sync, sync and discard-all dispatched both
  halves into the worker pool at once, so a push could resolve the branch
  tip before the commit (or pull) it was waiting for and quietly send the
  old one. Compound verbs now carry a ScmFollowUp that the first half's
  landing closure starts on success only; a refused commit, a failed pull
  or a cancelled confirmation drops the follow-up with it.
- Push sent `git push <remote> <branch>` with the branch taken from the
  upstream's name — a bare name means a *local* branch, so `feat`
  tracking `origin/main` pushed stale local `main`. The refspec is now
  `HEAD:<branch>`, and the branch is validated with the full branch
  check since a `:` would smuggle a second refspec in.
- `scm.committing` was armed before the amend confirmation and never
  disarmed on failure, so a cancelled prompt (or a hook rejection) plus
  any later unrelated HEAD move cleared a message that was never
  committed. It is armed at dispatch and disarmed when the commit errors.
- Discard-all fed staged-only paths to `checkout --`, where a staged
  deletion sank the whole batch as an unmatched pathspec. Only unstaged
  paths go in, one confirmation covers both halves, and the two gits no
  longer run concurrently.
- One "load more" click at 5000 commits grew `requested` past what
  `load_page` clamps to, so the freshness check never passed again and
  every frame refetched the full page. Growth stops at the cap, the
  button hides there, and a failing `git log` is remembered per key
  instead of being retried from every render.
- A repository switch now drops the previous repository's page before
  anything can draw it or grow from it — a stale row's context menu
  used to build ops for the new repo with the old repo's rev.
- A watch that failed to open was retried at frame rate, one host round
  trip per render; it now rests for WATCH_RETRY between attempts.
- Non-network writes on a remote host ran under the interactive
  20-second deadline while the server ran the job to completion, so a
  slow pre-commit hook was reported failed and then landed anyway. Every
  write now goes through git_with_deadline, 120s for local verbs.
2026-08-10 11:24:09 +08:00
l0ng-ai 5c65e2b08f test(git): pin the git config the fixtures assume, and one path spelling
Two Windows-only failures, both of them the tests asserting on the
runner's git rather than on the code.

`core.autocrlf` is `true` by default in Git for Windows, so a file
written as `one\n`, committed, and restored by `checkout --` comes back
as `one\r\n`. The `-c` list the helpers already pass would not have
fixed it: the checkout in that round trip is `run_op`, production code
running its own git with no overrides. So the pins go into
`<repo>/.git/config` right after `init`, where repository config
outranks the system config that carries the default.

The other is the same path-spelling mismatch fixed earlier in
`git_data.rs`: `git rev-parse` answers with forward slashes and no
extended-length prefix even on Windows, while `fs::canonicalize` returns
`\\?\C:\…`. Both name the same directory and the Win32 APIs take either,
so the production path is right and only the comparison needs one
spelling.

Fixed as classes rather than as instances. `core/git/mod.rs` gains a
`test_support` module holding the `-c` list, the repo-config pin and the
path normaliser, and `status.rs`, `log.rs`, `ops.rs` and `diff.rs` all
read from it — `ops.rs` had no config pins at all and `diff.rs` was
missing gpgsign. `git_data.rs` keeps its own copy because a
`#[cfg(test)]` item does not exist in the `tty7-core` the binary crate
links against; a comment says so and points at the other copy.

The normaliser has its own test over literal `\\?\C:\…`, `C:\…`, git's
`C:/…` and a unix path, and the line-ending fix was reproduced locally
by pointing `GIT_CONFIG_SYSTEM` at a config with `core.autocrlf = true`:
that panics exactly as CI did with the pins reverted, and passes with
them. One latent hazard hardened while here — `status.rs`'s scratch
directory had no pid in its name, unlike its sibling, so a leftover that
resisted removal would have been silently reused as a fixture.
2026-08-09 17:02:29 +08:00
l0ng-ai 0106430ecd merge: main into the Source Control branch
The one conflict is an import list in `diff_overlay.rs`: this branch added
`SharedString` for the unified view's row labels, main added `Background`
and `Hsla` for the window backdrop work. Both sides are still used, so the
resolution is the union.

Worth recording why this merge happened when it did. `main` moving is not
normally urgent — branch protection dropped its strict check, so a branch
behind main still merges — but a *conflicting* branch is different: GitHub
cannot compute `refs/pull/N/merge`, and every workflow that triggers on
`pull_request` silently stops running. Three pushes in a row registered no
CI at all on #424 while other PRs kept going green, which reads as a GitHub
incident and is really just an unresolved conflict.
2026-08-09 16:20:19 +08:00
l0ng-ai d0d5e149c7 fix(git): compare watch directories in one spelling of a path
`scm_watch_dirs` hands back what `git rev-parse` answered, and git writes
forward slashes and no extended-length prefix even on Windows. The test's
expected side is built from `fs::canonicalize`, which on Windows returns
`\\?\C:\…` — so the two named the same directory and compared unequal, and
the Windows job failed on a path the watcher would have been perfectly
happy with.

Both spellings reach the same directory through the Win32 file APIs, so
the watcher is right to pass git's answer straight to `Host::watch` and
nothing changes outside the test. The five assertions now go through a
`one_spelling` helper and keep their exact-equality teeth; on unix it is a
no-op, which is why this was invisible until Windows CI ran the branch for
the first time.
2026-08-09 16:08:46 +08:00
l0ng-ai 4d5ac5913c fix(scm): gate the graph's idle test on unix, like its three siblings
`test_window::harness_with_pane` is `#[cfg(unix)]` — it hands back a
`std::os::unix::net::UnixStream` — so a test module that calls it has to
be gated the same way. `panel.rs`, `detail.rs` and `file_tree.rs` all
declare theirs `#[cfg(all(test, unix))]`; this one said only
`#[cfg(test)]`, which broke the Windows test build with E0425 while
compiling fine everywhere a developer looks.

Nothing was lost by gating it: the module holds one test, and its own
doc comment already says it has to run against a real repository and a
real pane.
2026-08-09 15:46:56 +08:00
l0ng-ai e76d545655 test(git): build the log tests a repository instead of reading this one
Both tests pointed `load_page` and `local_branches` at
`CARGO_MANIFEST_DIR` — the checkout the tests happen to be running
inside — and then asserted things only a developer's clone is true of.
`actions/checkout` clones shallow, so "five commits" found one, and a
pull-request build checks out the merge ref detached, so "this checkout
is on a branch" found no branch. Both assertions were about the
environment; the code under test was answering correctly in each case.

They now build their own history, following `status.rs`'s `scratch()`:
a temporary directory removed on drop, `user.name` and `user.email`
pinned because a runner has neither, and `symbolic-ref` rather than
`init -b` so the branch name does not depend on the git version. One
test forks a side branch and merges it back over seven commits, so lane
layout is checked against a shape with more than one lane in it; the
other makes a single commit that modifies, adds, deletes and renames, so
the `--numstat` / `--name-status` join is exercised on four paths rather
than on whatever HEAD happened to touch.

Owning the history let the assertions get stricter rather than looser:
the commit count, the branch list and the per-file `(added, removed)`
pairs are now exact, the paging check is honest about how much history
exists, and the merge row is asserted to carry two parents and two
outgoing edges. The three other tests reading `CARGO_MANIFEST_DIR` were
audited and left alone — one bails when `git log` fails, one falls back
to a short sha on a detached HEAD, and the third is `#[ignore]`.
2026-08-09 15:21:48 +08:00
ARNOandl0ng-ai 61efe27f2d feat(windows): add native backdrop material presets (Mica / Acrylic /… (#412)
* feat(windows): add native backdrop material presets (Mica / Acrylic / Blur)

Adds a Background material dropdown (Auto / Blur / Mica / Mica Alt /
Acrylic / Off) that maps onto the native Windows backdrop APIs already
provided by the gpui fork — Mica and Mica Alt via
DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE), Acrylic via the new
DWMSBT_TRANSIENTWINDOW material, and Blur via the classic
ACCENT_ENABLE_ACRYLICBLURBEHIND path — with no fork changes required.
* config: introduce WindowBackdrop in tty7-core with lenient kebab-case
  deserialization, defaulting to Auto for existing configs
* theme: resolve the backdrop through a build-number fallback chain
  (Mica/Mica Alt need Windows 11 22H2, Acrylic needs 22H2 natively and
  1809 via classic acrylic, Blur needs 1809; older builds fall back to
  plain translucency) and default the background alpha to
  SYSTEM_MATERIAL_OPACITY (0.82) while a material is active
* settings: replace the blur toggle with a localized backdrop dropdown
  that only lists the presets the current Windows build actually
  supports, and keep the settings panel fully opaque so workspace
  translucency never shows through it
* theme: make the file sidebar and right detail panel follow the window
  opacity so the backdrop material shows through the whole workspace,
  keeping row-level accents opaque for readability
* i18n: add backdrop keys for en, zh-CN and ja-JP, covered by the
  translation completeness test

* feat(theme): let the sidebar and right panel follow the window opacity

* update GPUI

* fix(windows): gate the sidebar translucency to translucent windows and sync the opacity slider

fix(windows): gate the sidebar translucency compensation to active materials

* fix(windows): derive the material opacity default from the resolved appearance

* fix(theme): keep WindowBackdrop semantics consistent on non-Windows

f

* fix(theme): stop Windows-only materials from pinning the blur on other platforms

* docs(changelog): document the Windows backdrop material settings

* refactor(theme): share the default window-opacity derivation

* fix(ui): keep gradient presets behind the settings panel and scope its fallbacks

* fix(ui): keep the settings theme picker legible and the backdrop label honest

f

* fix(theme): let every backdrop variant defer to the local blur toggle on non-Windows

* fix(settings): restore the backdrop dropdown selection on locale refresh

* fix(ui): keep the opened-file editor surface opaque under window translucency

* fix(settings): rebuild backdrop options after selection

* fix(settings): ignore synced windows backdrop overrides on other platforms

* fix(settings): preserve synced windows backdrop on non-windows reset

* fix(diff): keep the full-window overlay background opaque

* fix(windows): keep Auto opaque and stop the backdrop from misreporting itself

Ten findings from a review of the backdrop-material work, all in the
Windows-only paths.

The root one: `material_active` treated `Auto` as a material whenever the
legacy blur toggle happened to be on. `Auto` is the default in every config
written before this setting existed, and plenty of them carry
`window_blur: true` from the switch that no longer renders on Windows, so an
untouched install would drop from opaque to 0.82 alpha - with its file
sidebar and right panel at 0.15 - on first launch after the update, with no
visible control to undo it. Only an explicit pick in the dropdown now buys
the translucent defaults. The switch comes back on Windows while the
backdrop is `Auto`, since that is exactly when the legacy flag still decides
something.

The rest:

- Mica and Mica Alt fell back to `Blurred` with no lower bound, asking for a
  blur that does not exist below 1809 - and build 0, which is what a failed
  `RtlGetVersion` reports. They now degrade to plain translucency like
  `Blur` and `Acrylic` already did.
- Acrylic is no longer offered below 22H2, where it resolves to the very
  same classic WCA blur as `Blur`. A test now asserts that no two offered
  presets render identically on any build.
- `reload_from_config` re-applied the theme and the opacity slider but not
  the backdrop dropdown, so an external config change switched the window's
  material while the control kept naming the old one.
- The settings, opened-file and diff overlays were made opaque so the OS
  backdrop cannot show through their text; that also hid the theme
  background image, which used to show through them. They paint their own
  copy of it now, and the fill they share moved into
  `theme::overlay_background`.
- The SFTP transfers tray painted `workspace_surface_color` inside the right
  panel, which already paints it, stacking the same translucent surface
  twice into a darker band with a hard seam.
- `apply_theme` re-issued `set_background_appearance` on every `Config`
  mutation in every window. With a DWM material that now costs a
  `SetWindowPos(SWP_FRAMECHANGED)` frame recalc, so dragging the opacity
  slider recalculated the frame once per mouse sample; it is skipped when
  the appearance is unchanged.

* fix(ui): dim the overlay background image, and stop telling Windows it is macOS

Two defects found while driving the previous commit's changes in the app.

The overlays repaint the theme background image over their own opaque fill,
so it survives them being made opaque - but nothing dimmed it. Before those
overlays were opaque the image reached the eye through their translucent
fill; painting it at full strength put the settings text straight on top of
the wallpaper and made the panel unreadable at any image opacity above about
half. They now paint the image and then the workspace's own fill over it,
which is exactly the strength the image had through these overlays before,
and which needs no new constant to say so. Shared as
`app::overlay_surface_layers`, empty when the theme has no image so a
themeless window paints no second pass of anything.

The Windows-only blur row reused `SettingsBlurDesc`, whose text ends in
"(macOS)". It gets its own key in all three locales, describing the job the
flag actually still has on Windows: feeding the `Auto` material.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-09 15:14:54 +08:00
l0ng-ai e46bcbcf56 chore(terminal): drop the client-side command-mark store (#404)
Removing the Outline panel (#374 / #375) took away the only reader of the
client-side command marks. The scanner kept running on every batch of PTY
output, and it was the one scanner that forced the batch to be split before it
reached the emulator, so it was not free.

Gone: `MarkScanner`, `Marks`, `CommandMark`, `record_mark`, `Cut::Mark` and the
tests that only covered them. With the cursor cut as the sole cut left, the
offset sort is a no-op and goes too — `ParkedCursorScanner` already reports in
ascending order — and `Cut` itself collapses into a plain `CursorCut`.

Kept: `zle_tok` and `mode_tok` read the same OSC 133 bytes and are load-bearing
for `zle_reading` / `shell_vi_mode`, including the deliberate live-vs-snapshot
split. Daemon-side OSC 133 handling is untouched.

Dropping `marks().clear()` left `clear_scrollback` with no anchored-state
invalidation at all, and it never had any for the other store that needs it:
kitty image placements are anchored to an absolute scrollback row, so purging
the history moves every anchor and the frame paints over unrelated text or
resolves past the viewport, with no redraw coming since the daemon does not
replay out-of-band image frames. Clear the image store there, as the reattach
path already does, and route the purge through
`Term::clear_screen(ClearMode::Saved)` so a selection reaching into the
discarded rows is invalidated instead of clamping onto the viewport.

Closes #378
2026-08-09 14:49:47 +08:00
l0ng-ai 96c997ad59 fix(scm): keep the row buttons on screen, and settle the panel's surfaces
Three real defects and a design pass, all found accepting the panel against
a running app.

The hover buttons on a file row erased themselves the instant the pointer
reached them. The strip called `occlude()`, and gpui's `Frame::hit_test`
stops at the first `BlockMouse` hitbox and drops every hitbox inserted
before it — which includes the row's own, because a parent prepaints before
its children. So the row stopped counting as hovered, `group_hover` stopped
applying, and `Interactivity::paint` returned early on `Visibility::Hidden`
before drawing either the backing or the buttons. The tooltip outlived them
because it is an `on_hover` listener armed on the last frame that painted,
which is why what was left on screen read as a grey box where the buttons
should have been. Replaced with `on_any_mouse_down` and
`stop_propagation` — the idiom `switcher.rs` already ships — and two gpui
tests now fail if `occlude()` comes back.

The commit-detail view drew "No files changed" above "Loading…" while its
read was still out, and the graph's new-branch field was the only `Input` in
the application without `.appearance(false)`, so it wore gpui-component's
default border.

The rest is the panel's visual language, which had drifted into tty7's
dialog vocabulary. `bg(theme.input)` occurred exactly once in the whole
application and `.primary()` only ever appears in modals and the settings
page, yet the commit box was a filled bordered field with an accent focus
ring and the commit button a filled slab — in a panel where nothing else is
outlined and separation is carried by surface and space. The commit area is
now two soft rounded fills, both from `field_fill`, at half the surface
ramp's first rung: `hover` is what a row wears for the moment a pointer is
on it, and a field that wears its fill permanently is the loudest thing on
an idle panel at that strength. Focus takes the whole rung instead of a
ring.

The split button lights as one shape rather than one end — it has no outline
around either half and a seam one pixel wide, so half a lit pill read as a
paint bug — and its halves paint nothing themselves in any state. That last
part is not only about hover: gpui-component resolves a custom variant's
*selected* paint from its `active` slot, and a dropdown holds its trigger
selected for as long as the menu is open, which parked a block on the
chevron for the whole time the menu was being read.

Smaller things in the same pass. The graph's conventional-commit prefix is
inline muted text rather than a coloured pill, which stops a second colour
column competing with the lane gutter beside it and un-ragged the left edge
of the subjects. Commit-detail refs no longer paint `theme.accent` at full
opacity under muted text — that is the system's loudest neutral fill, and it
made an ordinary `origin/main` shout over the HEAD chip it was meant to
defer to. The commit button is compact and right-aligned beside a
staged-file count, the history filter hides behind a toggle that takes its
query with it when it closes, and the detail view says how many lines a
commit moved. `TILE_SIZE_XS` and `TILE_GLYPH_XS` moved to `app.rs`, so the
SFTP and port-forward panels no longer import tile sizes from the Source
Control module.

The lane-gutter fold is gone. It bought about six characters of subject
width, but folding the lanes away leaves a list rather than a graph, so
nobody would ever press it.
2026-08-09 14:35:54 +08:00
l0ng-ai 09a653d10b fix(workspace): make the CLI and the GUI agree on what exists (#423)
Five places where a workspace, a tab or an attachment was real on one side of the socket and invisible on the other. They share a root: the GUI kept its own list of which workspaces exist (WindowViews on disk) and consulted the machine tree only for the ones already in that list, so anything created by another client was unreachable by construction.

- The switcher lists workspaces the machine holds but this client has never opened, and opening one keeps its id instead of claiming a fresh one.
- for_workspace_at hydrates whenever the machine holds tabs, so opening a workspace no longer saves an empty session over them.
- finish_hydration writes a full window back over an empty tree, which is what puts a ws rm'd workspace back under the same id.
- A deletion nothing has open is forgotten here too, instead of haunting the switcher until a restart.
- Workspace::attachment travels over the wire (minus the token that proves the hold, which stays on the connection that owns it) and is stripped in persist, so tty7 ls can name the host holding a workspace.
- tab ls / ws tree fall back through name -> agent -> cwd leaf -> process name, and tab ls grew a read-only GROUP column.
- tty7 new --open raises a window on the workspace it just made.
2026-08-09 14:18:19 +08:00
ARNO 8981cc7429 fix(wsl): start a pane when fish is the distro's default shell, with its integration (#422)
A WSL pane whose distro defaults to fish never started: `wsl.exe --` hands the
command line to that default shell, which parses the POSIX bootstrap before
`sh` can receive it. Every wsl.exe invocation that carries an argv — the pane
bootstrap, `RemoteLink::wsl`, `RemoteLink::wsl_shell`, and the installer's
`sh -s` / `tee` calls — now goes out under `--exec`, which runs the program
itself. `--` keeps its other meaning, "just start the default shell", for the
no-argv case.

Those fish panes then came up with no shell integration at all, because the
bootstrap only ever had a bash arm: no OSC 133, no OSC 7, so no prompt marks,
no exit status, no cwd, and `tty7 wait` and busy/idle status dead in the pane.
The `case "$SHELL"` now has a fish arm carrying FISH_INTEGRATION inline, the
way `remote::bootstrap_command` already does over SSH — POSIX-quoted, since
`sh` parses this script rather than the user's own shell.

The PTY tests' completion marker is anchored on its terminator: `133;D;1` is a
prefix of `133;D;127` and `133;D;130`, so they would have gone green on a
bootstrap that never ran the command they type.
2026-08-09 13:53:23 +08:00
dongyuwei 1c1b4e2c92 feat(terminal): include fish_history in command history search (#421)
fish's history joins the Ctrl+R menu and inline completion, locally and on
remote hosts.

`fish_history` looks like YAML and isn't: fish escapes only `\` and newline and
quotes nothing, so a YAML reader drops every record holding a `: ` or a leading
`[` and truncates anything with a ` #` — conventional-commit messages, `echo
a: b`, `[ -f x ]` tests. It is read with a line scanner shaped like fish's own
reader instead, which also keeps pane construction off a per-record libyaml
parse.

Each history file fetched from a remote host now carries the name it came from,
so the far end's fish records go to the fish reader rather than arriving as
literal `- cmd:` rows in the menu.

Multiline commands are skipped rather than half-recalled: `append`, the shell
handoff and the reverse-search menu are all single-line, so an entry that
cannot be run is worse than one that isn't offered.
2026-08-09 13:52:54 +08:00
l0ng-ai 078dbec98f merge: the commit graph
Two conflicts, both from the graph and the commit detail view landing in the
same week:

- scm/mod.rs: each had removed its own dead-code allow. Both are gone now;
  status_rank's stays, since the file tree is what will use it.
- graph.rs built a CommitDetailView by hand while detail.rs had grown a
  constructor that takes the commit the caller already holds. It now goes
  through open_commit_detail with the row's own commit as the seed, which is
  what the detail view's author asked for: a click costs one git command
  (the file list) instead of two.

The test that covered the old hand-built view asserted `loading`, which was
an artefact of building it directly. It now asserts what its name says — that
the seed arrives — and covers the unseeded path too.
2026-08-09 11:01:12 +08:00
l0ng-ai 25bfe67a7b fix(scm): budget the graph gutter's insets, not just its lanes
The share was measured against the lane strip alone, so a 260px panel asked
for six lanes and an 84px gutter out of a 78px budget. Five is what fits, and
what the width was chosen for.
2026-08-09 10:45:58 +08:00
l0ng-ai 5418d66903 feat(scm): draw the commit graph in the panel's history section
The history section now renders a real `CommitPage`: lanes, nodes, merge
rings, refs and ages, over rows that behave like every other row in the
panel. It replaces the three-lane figure from the G7·0 spike, whose shape it
keeps unchanged — one canvas over the whole list, `paint_quad` for everything,
lane centres snapped to device pixels before the quad is built.

What the section is for decided most of the rest. 260px leaves about 26
characters beside the gutter, and this repository's subjects run to a median
of 64, so reading a message here was never going to work: what a reader gets
is where the branches are, where they merged, which refs sit where, and how
recently anything moved. Two things buy back what can be bought:

- The conventional-commit prefix comes off into a chip. `feat(terminal): ` is
  12.7 characters on average, and the type is exactly the part that reads
  better as three coloured characters than as prose. The split is strict, so
  `Merge pull request`, `fix:it` and a bare URL all keep their whole line.
- The lane gutter folds to a single column on request, worth another six.

Lane colours are derived, not tabled. `Theme::lanes()` seeds from the palette
in the order blue, yellow, magenta, green, cyan, red — no two neighbours share
a hue family, red and green are never adjacent, and red is last because a
panel three or four lanes wide never reaches it — then walks each one to
`ACCENT_FLOOR` on the window, the sidebar and a popover. Across the nine
builtins the worst contrast is 3.00:1 (untreated, `catppuccin_latte` sits at
2.31 and `rose_pine_dawn` at 2.05) and the worst adjacent pair is ΔE 13.8,
against a JND of about 2.3. A hard-coded palette would have been the one
colour in this file that ignores the theme, and the contrast tests cannot see
a literal.

Some notes on the drawing:

- Segments are deduplicated by column before anything is painted, which is
  what makes the overflow bundle work: five lanes folded into the last column
  produce one line, not five stacked at five alphas. `project` is a pure
  projection and never feeds back into the layout, so dragging the panel
  narrower re-columns for free and no branch changes colour.
- Cross-lane turns are right angles, and at a 12px pitch they read
  unambiguously — the same call tig, lazygit and `git log --graph` make. The
  horizontal runs half a line width past both centres, which is exactly what
  closes the corners the vertical stubs leave open.
- No `paint_layer` per line. Zed's graph does that; each one is a full-drawable
  render pass. `BoundsTree` already orders overlapping primitives, and edges
  arrive sorted by `paint_rank`, so the node's own line lands last.
- Nodes are rounded quads rather than paths: the quad shader rounds with an
  exact SDF and analytic anti-aliasing, where `PathBuilder` fills every vertex's
  `st` with `(0, 1)` and gets 4x MSAA alone.
- Paging grows `requested` and re-runs the query. The layout is deterministic,
  so a longer run reproduces the same prefix row for row and nothing on screen
  moves; `--skip` is O(skip) and slides under you when a ref moves. It is a
  row, not a scroll trigger — a remote `git log` is an RPC, and
  scroll-to-load turns one flick into a burst of them.
- Filtering hides the gutter. Lanes drawn across a subset of history would
  connect commits that are not adjacent, so a search result is a flat list,
  which is what it actually is.

Seventeen tests. Four in `presets` run with the existing contrast batch and
assert the floor on all three surfaces, adjacent ΔE, determinism and the seed
order. The rest cover projection, snapped lane centres, the width clamp, the
prefix split (including a Chinese subject, which is where byte indexing goes
wrong), band deduplication, the filter and the scope label. One runs a real
repository through a real pane and asserts the settled section draws zero
frames while idle — a canvas that repaints every frame reads as correct code.
2026-08-09 10:44:16 +08:00
l0ng-ai cc0e4f1edf merge: the commit detail view 2026-08-09 10:21:39 +08:00
l0ng-ai 5497252aec feat(scm): read a whole commit in the panel's second-level view
The graph gives up text for shape: a 260px row has room for about 26
characters beside its lanes, and this repository's commit subjects run
to a median of 64. This is where the rest comes back — the full subject,
the body, every ref, the parents, and the files the commit touched.

The file list is two commands, not one. Git accepts `--numstat` and
`--name-status` together and then quietly drops the numstat half
(measured on 2.50.1), so they run separately and join on the path. It is
`log -1 --first-parent` rather than `diff-tree -m --first-parent`, which
does not narrow a merge: on the same git it emits one diff per parent
and concatenates them, so a two-parent merge came back with every path
listed twice. `log` is also how `DiffSource::Commit` walks the patch,
which is what makes this list and the overlay's cards agree file for
file.

`DiffSource::Commit` now carries an optional label, so the overlay's
header can say what the commit was about instead of eight hex digits.
The label deliberately takes no part in the source's identity:
`PartialEq`, `Hash` and the overlay's string probe key are all derived
from one `tag()` function, because the same commit opened with a subject
in hand and without one has to stay one patch, one in-flight probe and
one overlay. The key used to be built from `Debug`, which would have
split the probe cache the moment a label arrived.

Also lifts `local_branches` out of the panel's inline `for-each-ref`,
and keeps the `%(upstream)` that `parse_refs` had been asking for and
throwing away.
2026-08-09 10:20:34 +08:00
l0ng-ai 60b4ebacfd spike(scm): prove the graph's one-canvas rendering plan (G7·0)
A hard-coded three-lane figure, no data layer attached, standing in for the
history section. It exists to answer the four questions the whole rendering
plan rests on before any of it is built on top, because a "no" to any of them
invalidates the rest.

Verified against an isolated dev instance (own config dir and daemon),
screenshots read back:

1. One `canvas` absolutely positioned over the rows, inside the section's own
   `overflow_y_scroll`, paints where it should — lines and nodes land on their
   rows and stay on them after scrolling. A canvas per row was never on the
   table: every `Paths` batch costs a full-drawable render pass.
2. `window.content_mask().bounds` gives usable culling bounds. With 33 rows and
   a 195px viewport it reported `705..900` and the paint loop covered rows
   0..10 — 24 quads instead of ~70. Scrolling moved that to rows 16..26 while
   the mask stayed put, which is exactly right: the mask is the viewport, the
   canvas bounds are what moves.
3. Clicks reach the row `div` through the canvas. A click landed inside the
   lane gutter, on top of a painted line, and selected the row underneath.
   `Canvas::id` returns `None` and it implements no interactivity, so it never
   registers a hitbox — being above in z only decides paint order. That is what
   buys the rows gpui's native hover, click and scroll-into-view for free.
4. The height divider, which is `right_panel_resize` rotated onto the other
   axis, tracks the cursor 1:1 and clamps where it is told to.

Everything is drawn with `paint_quad`: straight segments as thin rects, nodes
as rounded quads for the SDF's analytic anti-aliasing, and cross-lane turns as
right-angle elbows. The elbows read cleanly at a 12px lane pitch — tig,
lazygit and `git log --graph` all draw them square — so curves stay behind a
switch rather than in the first version. Lane centres are snapped to device
pixels before the quad is built, not after, or a column of lines changes width
as it scrolls.

The next commit replaces the fake rows with `CommitPage` and keeps this shape.
2026-08-09 10:08:57 +08:00
l0ng-ai b4e7add65d chore(release): v26.8.2 v26.8.2 2026-08-09 09:59:26 +08:00
l0ng-ai d8d822092d chore(scm): reserve the graph and commit-detail mount points
Both land in panel.rs — the history section below the file list, the detail
body in place of it — and both are being written in parallel. Landing the two
call sites and their stub modules up front keeps them from colliding over the
same function.

The detail body replaces the working tree's rather than sitting beside it, so
the two can never be on screen each claiming to be the file list. The history
section sits outside the scroller: it pages, and sharing a scroll region would
mean scrolling back past hundreds of commits to reach the message box.
2026-08-09 09:43:15 +08:00
l0ng-ai 16d7c1b5a2 refactor(scm): retire the panel's old flat-diff state
The panel now reads WorkingTreeStatus, so RightPanelState's diff, diff_cwd
and diff_pending had no writer left — and with them went PANEL_DIFF_SOURCE,
the seed shortcut that borrowed the panel's snapshot, and the write-back that
kept it fed. The overlay always names its own source now.

Two things were still reading that state and quietly getting nothing:

- the source control tile's badge, which now counts entries from the same
  status the panel draws, so the number and the group headers cannot disagree
- the branch row's busy spinner, which was inferring "the push finished" from
  the repository's epoch moving. Anything else that moves the epoch — a .git
  event, a file save — would have dropped the spinner mid-push. It asks the
  slot counter run_git_op claims from instead, which is the operation itself.

scm_network_busy reads through try_global rather than default_global: it runs
from render, where taking the global mutably queues a global-observer effect
on every frame.
2026-08-09 02:10:53 +08:00
l0ng-ai 1e525a65b6 merge: the Source Control panel 2026-08-09 02:03:50 +08:00
l0ng-ai 293d761cba feat(scm): show the branch, its distance and one button to close it
A row of its own between the title and the message box. Not the title's
trailing slot: off macOS the tab tiles render after it, and a branch name
is the elastic element here — it would be the first thing squeezed.

A branch level with its upstream says nothing at all. The quiet state is
the common one, and a chip that is always there stops being read; ahead,
behind and "publish" are the three things worth interrupting for, plus
the sequencer operation the repository is parked in and the amend badge.
Interactive and plain rebase read the same, because git writes
`rebase-merge/interactive` for every rebase and the distinction is not
one the repository on disk can make.

The branch name opens the switcher, which lists the local branches from
`for-each-ref`, re-read whenever anything could have moved a ref. Naming
a new branch is an inline input rather than a dialog: `window.prompt`
only offers buttons, and there is no modal component to reach for.

The root every operation runs from is now resolved with its own
`rev-parse` rather than borrowed from the cheap per-tab cache. That cache
holds a repository's *home*, which is a different directory inside a
linked worktree, and it is only filled in for panes whose shell reports a
cwd — the panel would have sat on "Loading…" forever without one.

Two render-idle tests hold the panel to asking git once and then going
quiet, over a real repository and over a directory that is not one. The
hazard they cover is specific: `scm_refresh` reaches for its cache
through `default_global` from inside `render`, so a watcher that notified
on every global write would ask for a frame from inside a frame forever.
2026-08-09 02:02:43 +08:00
l0ng-ai 7695287f4a feat(git): connect the remaining invalidation sources and subscribers
The watcher landed with one subscriber declared and one invalidation source
wired, because the three others live in files it did not own. Wiring them:

- the file tree announces working-tree edits it sees, skipping anything under
  .git so the repository's own watch is not doubled into the same window
- the editor announces a save, which is the working-tree edit neither watch
  can see when the tree is not showing that directory
- a pane announces a command boundary, which is the only signal for a command
  that edits a file nowhere anyone is looking. It only moves the epoch: the
  probe rides the app's next render, which refresh_git_status is about to
  cause anyway by writing GitStatusCache

The tree and the editor also declare themselves as watchers, so decorations
and gutters keep a repository live on their own rather than only while the
panel happens to be the visible tab. Both target the active pane's
repository, which is what the panel picks too, so the three subscriptions
usually collapse onto one watch.
2026-08-09 01:36:20 +08:00
l0ng-ai c2f7574bfa feat(scm): commit from the panel
A multi-line message box over a primary button, both pinned above the
file list so they stay reachable however far down the changes go. The
box rests at 30px — the height of `panel_search`, so every input row in
the panel sits on one line — and grows to six rows.

`secondary-enter` commits, bound inside the `ScmCommit` key context the
box installs. On macOS that chord is `ToggleFullscreen` at the window
level; the two coexist because gpui resolves a keystroke by walking
outwards from the focused node, and a test asserts exactly that — the
chord is shared and scope is the only thing telling them apart.

The button says what pressing it would do: "Commit All" when nothing is
staged, since a plain `git commit` would commit nothing and a silent
`-a` would be a lie. Amend is a menu item rather than a checkbox row,
because 260px does not have a row to spare.

Drafts are keyed by working tree, so switching tabs or panes keeps the
message. A commit is only cleared from the box once HEAD has actually
moved: clearing it on dispatch would lose a carefully written message to
a pre-commit hook that rejects it.
2026-08-09 01:36:01 +08:00
l0ng-ai 72776d3367 fix(git): only repaint when a status probe changed what is shown
Landing a probe called refresh_windows unconditionally, so the answer that
changes nothing — a re-read confirming what is already drawn, or the "still
not a repository" reply for an ordinary directory — cost a frame anyway.

rewriting_a_file_in_a_displayed_directory_costs_no_frames caught it: that
test asserts the file tree redraws nothing when a file it is showing is
rewritten, and the probe the tree kicks off for its own root was putting a
frame behind every such write.

Comparing the whole WorkingTreeStatus is O(entries), but it runs once per
probe rather than once per frame, which is the trade this is making.
2026-08-09 01:29:26 +08:00
l0ng-ai f63c1680f9 merge: a .git watcher, debouncing, and subscription gating 2026-08-09 01:27:10 +08:00
l0ng-ai 200d27286a feat(scm): stage, unstage and discard from the rows
Hovering a row brings up its buttons, absolutely positioned over an
opaque backing so they cover the tail of the directory instead of
pushing it aside — hovering must not move a pixel of the list under the
pointer. The header of each group carries the same verbs applied to all
of it.

Which buttons appear follows the group: Changes and Untracked get
discard and stage, Staged gets unstage, and a conflict gets "open" and
"mark resolved" — the latter being `git add`, because git has no other
verb for resolving.

Anything that can lose work goes through `window.prompt` first, keyed
off `GitOp::destructive` so the data layer stays the one place that
decides what is dangerous. That is the project's only confirmation
mechanism; no modal component is introduced.

A path that is not valid UTF-8 cannot be sent to git as a pathspec, so
its own buttons are disabled with a tooltip saying why, and group
actions leave it out — `validate` rejects the whole operation over one
of them, which would otherwise punish everyone else in the group.
2026-08-09 01:27:06 +08:00
l0ng-ai 71081236d0 feat(git): watch .git, debounce it, and stop when nobody is looking
The source control data layer had a cache and a way to invalidate it, but
nothing that noticed a change on its own. This adds the three pieces that
make the panel live: a `.git` watch, one debounce in front of every source
of invalidation, and a subscription gate so a repository nobody is looking
at costs nothing at all.

The watch covers `<git_dir>`, the common dir when a linked worktree makes
them different, the three `refs/` roots and the namespaces under
`refs/heads` — `Host::watch` does not recurse, so each one has to be named.
Never the working tree: recursively watching one over SSH is a disaster, and
edits there already reach the same bus from the file tree, the editor and
the command boundary.

Everything invalidating goes through one `scm_invalidate`, so a `git add`
and the watcher event it provokes fall in one window and cost one probe
between them. There is no self-triggering to defend against: the read path
sets `GIT_OPTIONAL_LOCKS=0`, whose only effect is to stop `git status`
writing back `.git/index`, so probes provably cannot wake the watch that
schedules them.

Also fixes four things that could not survive contact with a live panel:

- A network slot was released in `run_in`'s landing closure, which does not
  run if the view died first — one lost slot per abandoned push, forever.
  The claim is now a guard that rides in the work closure instead, which
  runs either way and needs no `App` to release.
- A probe that found no repository never wrote `read_at`, so the root stayed
  stale and every frame spawned another `rev-parse`. A pane sitting in an
  ordinary directory is normal, and "there is no repository here" is an
  answer like any other; `known_status` now reports it as one.
- Nothing marked a window dirty when a probe landed, so the panel waited for
  the next unrelated repaint.
- `ScmData::clear_host` had no caller, and neither did `GitStatusCache`'s
  equivalent: a dropped SSH link left the branch and the file list the
  machine had on the way down, with nothing to say so.
2026-08-09 01:25:39 +08:00
l0ng-ai fd5209f7fa merge: a unified diff view, and per-source overlay staleness 2026-08-09 01:20:49 +08:00
l0ng-ai 45879d1c22 feat(diff): add a unified view and name the source in the header
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.
2026-08-09 01:19:50 +08:00
l0ng-ai b92d9afa03 feat(scm): group the panel by index and working tree
The Changes tab ran one `git diff HEAD`, so it could not tell a staged
change from an unstaged one and gave every row the letter `M`. The panel
now renders `WorkingTreeStatus`, which reports both halves of `XY`
separately, in the four sections git itself talks about: Merge Changes,
Staged Changes, Changes, Untracked.

A row wears the letter of the half its group is about, so a file added to
the index and then edited again reads `A` under Staged and `M` under
Changes — and clicking it opens the matching patch, `--cached` for a
staged row and the working tree for the rest.

The group chevron sits in a box exactly as wide as `git_badge`, so the
fold arrows and the status letters below them form one column.

Two things guard the render loop. The pane's directory is turned into the
repository root before anything is cached or run, because porcelain
pathspecs are relative to the root and a write from a subdirectory would
name the wrong files; and the `ScmData` watcher compares before it
notifies, since `scm_refresh` reaches for the global through
`default_global` from inside `render` and an unconditional notify would
ask for a frame from inside a frame forever.
2026-08-09 01:19:02 +08:00
l0ng-ai 5f57d32eee merge: git status decorations in the file tree 2026-08-09 01:17:59 +08:00
l0ng-ai f6e8569920 feat(scm): decorate the file tree with git status
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 `/`.
2026-08-09 01:16:19 +08:00
l0ng-aiandl0ng-ai 8b7215f0d8 fix(remote): let a WSL machine restart its server, and call it "server" in zh (#417)
* fix(remote): let a WSL machine restart its server, and call it "server" in zh

The machine menu offered "restart server" only when `target.is_ssh()`, so a
WSL distro's row had just "new workspace" and "disconnect" — and the router
refused the action for anything but SSH, even though `restart_wsl_daemon`
had been sitting in `install/wsl.rs` unused since it was written. A distro's
server is installed and launched from this computer exactly like an SSH one;
only the transport differs.

- `RemoteTarget::hosts_our_server()` replaces `is_ssh()` as the gate in the
  sidebar menu and in `restart_window_daemon`. A `--stdio` workspace still
  has nothing to restart: it is whatever program the user named.
- The router grows WSL arms for both actions — `RestartServer` relaunches
  the distro's daemon, `ReplaceServer` (new `replace_wsl_server`) publishes
  the bundled binary first. The mismatch dialog's "update server" used to be
  a dead end on WSL for the same reason.
- `AppRestartServerNotSsh` is now `AppRestartServerNoServer`: the message no
  longer talks about SSH, since the only machine it can fire for is a local
  `--stdio` one.

The Chinese table called tty7's server 服务器 / 服务器端 / 后台服务; it now
says `server`, the way it already keeps `shell`, `pane` and `agent` in
English. The macOS Services menu keeps its 服务.

* fix(i18n): keep 服务器 where it means the SSH host, and test the new gate

The sweep that renamed tty7's own server to `server` in the Chinese table
also caught two SSH settings where 服务器 was never tty7's server: the
host-key description talks about the key of each machine being connected
to, and the banner switch suppresses the login banner sshd prints. Written
as `server` both now read as if tty7 ran something there, which is exactly
the confusion the rest of the sweep removes. Put the Chinese word back in
those two.

`hosts_our_server()` is the gate the whole change hangs on and nothing
exercised it, while the test next door still asserted under the name
"only SSH machines have a server to restart" — the rule this branch
retires. Name that test after what it actually checks, the transport, and
add one for the new predicate. It matches on the variants rather than
negating a single one, so a transport added later has to say for itself
whether we can restart what is on the far end.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-09 01:04:25 +08:00
l0ng-aiandl0ng-ai a6eccc6937 fix(terminal): complete a WSL workspace's panes over its share again (#418)
`wsl_share_cwd` opens with `if !self.host_id.is_local() { return None }`,
which throws away the very case its second branch exists for. A pane in a
WSL workspace is served by the daemon inside the distro, so `set_workspace`
gives it `RemoteTarget::Wsl.host_id()` — never `HostId::LOCAL` — and the
`workspace.target` branch below is unreachable. Tab in those panes fell
back to bash, while the wsl.exe pane in a local workspace (remote context,
no workspace, host LOCAL) kept working.

The locality check belongs to that first branch alone: a `wsl.exe` pane
reaches the distro of whichever machine hosts it, so a remote host's pane
must not read a same-named share here. A `RemoteTarget::Wsl` workspace is
this computer's distro by construction — tty7 reaches those by running
wsl.exe locally, which is why the sidebar labels them "WSL · this computer".

The decision moves into a free `wsl_share_distro`, testable the way
`loopback_plan` is, with all four combinations covered.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-09 01:01:32 +08:00
l0ng-ai 75c3af1047 feat(git): add the source control data pipeline
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.
2026-08-09 00:59:51 +08:00
l0ng-ai ac5983056c style(git): collapse the ahead/behind fallback into one condition 2026-08-09 00:55:39 +08:00
l0ng-ai 38899186dd merge: the Source Control panel's scaffold and wiring 2026-08-09 00:52:03 +08:00
l0ng-ai 80e8e09548 feat(scm): turn the Changes tab into Source Control and wire the surface up
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.
2026-08-09 00:51:06 +08:00
l0ng-ai c675796ef7 merge: three diff sources, and the rows both views are laid out from 2026-08-09 00:41:20 +08:00
l0ng-ai 58e8ba16d1 feat(git): ask for three kinds of diff, and lift the row model out of the renderer
`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`.
2026-08-09 00:40:21 +08:00
l0ng-ai 9285d6d94f merge: commit log parsing and graph lane layout 2026-08-09 00:40:01 +08:00
l0ng-ai b27d0d322b feat(git): lay out the commit history in lanes
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.
2026-08-09 00:38:54 +08:00
l0ng-ai 6ab4799bc1 fix(git): restore the classify test helper dropped with network_env
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.
2026-08-09 00:37:28 +08:00
l0ng-ai b8757372b3 feat(git): give network operations the long deadline
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.
2026-08-09 00:34:59 +08:00
l0ng-ai 5171880056 merge: a long deadline for network git, with no protocol change 2026-08-09 00:31:34 +08:00
l0ng-ai 94f9f2ba3b merge: porcelain v2 working-tree status 2026-08-09 00:31:25 +08:00