mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
b5c20932cb55ce13afca2f6eebf1c3f88efb8fe2
31
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b5c20932cb |
fix(diff-overlay): keep the row key pointing at the snapshot that landed
`sync_diff_rows` wrote `rows_key` only when it rebuilt the rows. A probe that found nothing new lands a fresh `Arc` over an equal snapshot, so `describes` answered yes the long way — by walking every line of the patch — and then left the key holding the *old* `Arc`. Every frame after that paid the same walk: once per wheel event, over the ~1 MB of `String` at the 20k-line budget, which is the cost the key exists to avoid. Retarget it on a match, so the pointer comparison settles the next frame. The preview was doubly exposed: `install_diff_snapshot` clears it on every landing, so the re-read produces a fresh `Arc<FileDiff>` too. Its comparison also ran eagerly, ahead of the scalars that could already have answered no; move it into the `&&` chain. `stack_corners` lost its last non-test caller when the file cards went, and `cargo build` had started reporting it. Drop it and the test that held it up. |
||
|
|
ecf3ff3577 |
perf(diff-overlay): draw the patch as a virtualised row list
The overlay built its whole patch as a nested element tree on every frame: a card per file, a header per hunk, six elements per line. gpui notifies the view on each scroll wheel event, so a few hundred lines of diff rebuilt tens of thousands of elements tens of times a second, and the window stalled. Flatten the tree into one row per line in a new `diff_list` module and draw it with `gpui::list`, which builds only the rows on screen. The rows are rebuilt only when what they are built from changes, so scrolling no longer re-splits hunks or re-clones every line, and a change to one file splices just the rows it touched rather than resetting the list and losing the scroll position. A card cannot survive that flattening — its rows are separate items now — so the frame it drew is gone, and with it the grey header bars and hunk bands that made the overlay the one view in the app still speaking gpui-component's default container language. The rows take the source control panel's own measurements instead: 26px, 10px inset, 5px radius, colour only under the pointer. The title bar's view switch loses its border for the same reason. |
||
|
|
958d8b7442 |
feat(window): dock the code panel and the diff overlay beside the terminal (#625) (#685)
* feat(window): dock the code panel and the diff overlay beside the terminal (#625) Opening a file covered the workspace. The terminal underneath kept running and was neither visible nor typeable, so reading a file while an agent talked was a toggle loop: open it, close it to read the reply, open it again. The Files tree already docks; the two surfaces you go to *from* it did not. They dock now, as a flex sibling of the terminal column rather than a narrower overlay — that distinction is the feature. `set_grid_size` is driven by the terminal element's laid-out bounds, so a column takes width away from the grid and the PTY reflows into what is left; a card painted over half the workspace would have left the grid full width with half of it hidden. `overlay_top` stops ordering a pair and starts choosing between them: a column has one child, and two `flex_1` siblings would split it and fight. Fill mode keeps the old vector, the old opaque paint and the old platform hoist untouched, so nothing about today's overlay changes for anyone who picks it. - Half the terminal column by default; drag the divider, double-click it to cycle a third / half / two thirds, or use the palette commands. Two thirds deliberately runs past the half-window cap the side panels obey — only the terminal's floor binds it. - `DOCUMENT_MIN_W` joins the width budget: both side panels reserve it the way they already reserve each other, and the column is derived from the *live* sidebar and panel widths rather than their floors, so a panel someone dragged wider is width the terminal keeps. - A window too narrow to seat both fills for that frame. The fallback is derived at render time and never stored, so widening re-docks on the next frame with nothing to undo. - Fill or dock is per tab, on the header's context menu. Reading a long file over the whole window in one tab while an agent keeps half of another is the normal case, and one global switch made each of those flip the other. A tab that has not been told reads `document_layout` from the config, which is what a fresh tab starts as — and which the menu therefore does not write, since every untold tab is reading it. - Everywhere but macOS the title bar spans the workspace, which left a bar's height of nothing above the column. The header is drawn into it, and behaves like the title bar it now sits in. With the detail panel closed the column reaches the window's right edge, so the header stops short of the trailing chrome through a width the tab strip's own reservation shares. - The docked headers drop the traffic-light inset they never had to clear, and the diff header's branch name becomes the thing that yields so the view toggle and the close tile survive a column's width. New in `config.json`: `document_ratio`, and `document_layout` for what a fresh tab starts as. Four new actions, bindable and unbound by default. * fix(window): hold the docked column to widths the strip and the file agree on Three defects in the document column, each with a guard test that fails without its fix. The tab strip did not know a column had taken width off it. On macOS the strip lives inside the terminal column and sizes itself to the window less the detail panel, so a docked document left it 340 points wider than the column it sits in and the chips ran on under the column — the same overrun the panel's own reservation was added for. Everywhere else the strip spans the workspace and the column's hoisted header is drawn over its trailing end with no fill of its own, so a chip left under it showed through the file name and stayed clickable through it. The column's width now comes off `strip_w` on macOS and off `corner_w` elsewhere, which is where the panel's already goes. The divider wrote widths the file would not keep. `Config::sanitize` holds `document_ratio` to 0.2..=0.8; the drag clamped in pixels only, so a column pushed against either edge of a wide window was saved outside that band and reopened somewhere else — on a 2560-point body, 232 points from where it was dropped. The band is a pair of shared constants now and the drag clamps to it, the way the font size and its stepper were made to agree in #550. The palette named the config's layout rather than the tab's. Fill is per tab, so a tab told to fill was still offered "Document: Fill Window" — a row that named the state it was already in and did the opposite. It reads the active tab through `ChromeState` now. Also: `document_layout`'s doc comment still described the global switch an earlier draft had, three lines after the field became a per-tab default. |
||
|
|
1424da0891 |
fix: nine UX fixes across the diff overlay, layout, settings and pane spawn (#623)
Found by driving a dev instance and measuring what an idle window costs. Two of them were frame loops that never stopped. A `Head` diff overlay calls itself stale when the cached git status disagrees with the snapshot on screen, and every landed probe wakes that check by touching the cache — but the read published its counts and left the branch behind, so a branch switched outside tty7 made the disagreement permanent: two `git` processes a lap, forever, with `refreshing…` pinned to the header and an idle window at 7% of a core. And the home page asked for a frame sixty times a second to change one glyph's opacity twice, which made a window with nothing open in it eight times more expensive than one running a shell. Both now settle: the diff read publishes the branch it found, and the home cursor flips a bool on a timer the way the terminal's own does. The rest: - The tab sidebar and the right panel each capped themselves at half the window and knew nothing about the other, so together they could take all of it — 260 points of terminal on a 720-point window. Both now cap at whichever binds harder: half the window, or what is left after the terminal's floor and the other panel's floor. The same cap bounds the drag, so a panel dragged to its limit stays where it was dropped. - Only a HEAD diff may correct the sidebar's counts. Those numbers mean `git diff --numstat HEAD`; an unstaged or staged patch answers a smaller question, so opening an untracked file from the Source Control panel took the staged lines off the total on the click. - An untracked row in the diff overlay had no click target, and once focused could not be left — the breadcrumb looks the path up in `files`, where an untracked file has no entry. Both ends fixed. - A new pane keeps the name its directory was reached by. `cwd()` alone loses it: the shell falls back to `getcwd()`, so `/tmp/x` became `/private/tmp/x` in every tab opened from the first. `PWD` carries it, and POSIX has the shell discard a `PWD` that names the wrong directory, so this can correct the name and cannot invent one. - The settings search now sees into the Keybindings page, which is generated from the binding table rather than the static index — so searching for a feature finds its shortcut, and the page filters to the matches. Closes #444. - The settings reading column is centred rather than pinned to the nav: on a window as wide as the display it was made for, 640 points of settings sat beside 1600 points of nothing. - `New Workspace…` takes the ellipsis its three sibling actions already carry — it opens a form asking for a name and a host. Every fix has a test. The re-probe loop is pinned end-to-end with `render_probe::draws() == 0` against a real repository, confirmed to fail on the old behaviour before it was kept. |
||
|
|
2dc6a88af6 |
merge: main into the Source Control branch
Conflicts were the two streams touching the same seams, resolved by taking the newer decision on each side: - main's interface font scale (rems tokens) wins in right_panel.rs; the SCM panel keeps its local px steps until it moves onto that scale, and the now-unused PANEL_TEXT constants are gone. - main's l10n_keys! macro (idents only) means the key list carries no doc comments any more; our SCM keys fold into it, and PanelUntracked stays deleted — its only caller was the panel this branch replaced. - main's Command::localized palette style carries our Git group; ORDER keeps main's visibility and our width. - main's ansi_seed/clear_ink refactor in presets.rs carries the lane colours: lanes() now clears through the same helper semantics uses. - file_tree keeps both: main's drag-and-drop targets and this branch's git decorations per row. - diff_overlay keeps both: main's sidebar-count write-back on snapshot install and this branch's epoch read and untracked preview. - main's window.prompt SSH-close confirmation supersedes the bespoke modal our branch still carried; main's tile-glyph revert stands. - main's two new guards are satisfied: the fourteen SCM actions carry authored names on the Keybindings page (their palette wording, plus a new CmdGitToggleGraph), ja translates ScmDetached, and CmdGroupGit joins the kept-in-English list — Git is a name. 2571 tests, 0 failures. |
||
|
|
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. |
||
|
|
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. |
||
|
|
62b922f2c2 |
Merge origin/main into integration/polish
main dropped the client-side command-mark store (#404) while this branch had just started reading it: the close confirmation names the command it is about to end, and the mark was the only place that text existed on the client. Keep both. The OSC 133 tokenizer main left in place already sees every mark, so the command line now rides alongside `zle_reading` and `shell_vi_mode` as one shared string — set on `C`, cleared on `B` and on a `C` that carries no line — instead of a store with a list, a lock and a cap. `busy()` reads that. The rest: - settings.rs takes main's opaque overlay surface and background layers, keeping this branch's no-match note and scrolled body. The inner `.bg()` goes, per main's reason: the root already paints it, and a second fill hides the theme image. - i18n keeps this branch's `every_key_is_translated_in_every_locale`, which walks `L10nKey::ALL` in all three locales, over main's hand-listed zh coverage test it replaced. It immediately caught three of main's new backdrop keys reading English in ja — Mica, Mica Alt and Acrylic, which is what Japanese Windows calls them, so they join the allowlist with that reason. - app.rs keeps both sides' tests and drops both sides' now-dead imports: `window_background` (main deleted the function) and `humanize_action` (this branch's keybinding note uses `keymap::action_entry` instead). Verified: `sleep 300` then ⌘W asks about "sleep 300"; ⌘W after it ends closes without asking. |
||
|
|
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. |
||
|
|
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> |
||
|
|
de2e622be5 |
fix(diff): a read that failed must not overwrite the sidebar's counts
The overlay says "could not read the diff" in words, and then published the failed read's zero totals over the +N -N the row already had right. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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`. |
||
|
|
544badfa33 |
fix(diff): give the diff overlay the scrollbar every other scroll area has
A working tree can run to thousands of lines here, and the overlay was the one long document in the app with nothing to say how far it went or where in it you were. Nine scroll areas already wrap in the shared helper; this one had not even been given a ScrollHandle. |
||
|
|
342b65ae5e |
fix(git): stop the sidebar count disagreeing with the diff it opens
The +N −N on a tab row is refreshed on an edge — the directory changing, a command finishing, an agent turn ending, the window coming back — and never polled. That is the right shape for it, but it means a working tree edited from anywhere else keeps the old number until one of those happens. Click the number and the overlay reads the tree there and then: the row said +27 −8 while the overlay it had just opened said +3 −1. A diff read is a fresher answer to the same question, so it now hands its totals back to the status cache. The Changes panel feeds the same path, so watching a diff shrink there keeps the row honest too. Only the numbers move — the branch, the root and the probe's own schedule are untouched, and a diff for a repo nothing has probed is dropped rather than inventing a row with no branch on it. The totals are exact even when the diff bodies were truncated by the repo-wide budget, which `repo_wide_budget_keeps_totals_exact` holds. |
||
|
|
8a342f2ca9 |
feat(ui): GUI localization for en and zh-Hans (#303)
* feat(ui): add GUI localization for en and zh-Hans
* feat(ui): localize search placeholders and relative time
* feat(ui): localize palette, switcher, and sftp strings
* feat(ui): localize home shortcut labels
* feat(ui): localize tray, ssh prompt, and editor strings
* feat(ui): add plural/select i18n helpers and localize sftp/settings labels
* feat(ui): localize settings search, forwards panel, and file tree
* feat(ui): localize code editor and right panel
* feat(ui): localize stop/delete workspace confirmations with plural support
* feat(ui): localize diff overlay with plural-aware summary
* feat(ui): localize pending pane, worktree prompt, and home time strings
* feat(ui): localize app menus, tray, tab strip/sidebar, and remote status strings
* feat(ui): localize switcher, file_tree, machine_mirror fallback strings
* feat(ui): localize ssh prompts, theme presets, host error wrapper, and finish remote strings
* feat(ui): localize command palette strings
* feat(ui): localize app.rs notifications, prompts, placeholders, and parse errors
* feat(ui): localize remaining theme, switcher, settings, and sftp strings
* style: cargo fmt
* feat(ui): add language selector to settings
* fix(ui): refresh locales across windows
* refactor(ui): make GUI language selection explicit
* fix(ui): localize Explorer settings after merge
* fix(ui): keep persisted theme names out of the GUI locale
A theme's name is data, not chrome: it is written into the theme YAML and
matched back with `trim_end_matches(" (custom)")`. Translating it meant a
Chinese GUI forked "Nord" into "Nord(自定义)", the next fork stacked a second
suffix on it, and the name stayed Chinese after switching back to English. The
derived-name fallback had the same problem. Both are English again.
Also in this pass:
- Give each test thread its own locale override. The locale is process-wide and
tests run in parallel, so the two tests that switched to zh-CN could flip the
language out from under another thread's English assertions.
- Rebuild the menu bar when gui_language changes in config.json, the way the
in-app picker already does — otherwise the menus kept the old language.
- Document the values the setting actually accepts. The docs still described
`auto` and `zh-Hans`, which sanitize() resets to `en`.
- Put the English words back into the Chinese search keywords for the language
setting; the other 58 keyword sets keep them.
- Drop the unused is_zh_hans helper.
---------
Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
|
||
|
|
8c1946d763 |
chore: strip every comment from the Rust sources (#268)
Removed all Rust comments -- line, block, and doc -- from the 139 tracked .rs files with `uncomment` 3.5.1. It parses each file with tree-sitter instead of matching text, so comment-like content inside string literals is left alone: the JavaScript plugin source embedded in agent_hooks.rs raw strings keeps its own `//` lines. Left alone: Cargo.toml comments and the shell scripts under scripts/. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
f6fa34a016 |
feat(ui): make the sidebar diff preview optional and bound its cost on large working trees (#247)
* feat(diff): make the sidebar diff preview optional and bound its cost Clicking a sidebar row's `+N −N` opens the working-tree diff overlay. On a big tree that could stall the window, and not everyone wants an in-app diff viewer in the first place. Two halves, matching the report. The setting: `sidebar_diff_preview` (Settings → Window & Tabs, on by default, persisted in `config.json`). Off, the branch and the counts stay exactly where they are and read exactly the same; they lose only the pointer cursor and the `toggle_diff_overlay` handler, so the press falls through to ordinary tab activation. Both come off one value — `diff_click_cwd` — so they cannot get out of step. The performance work. All five of the reporter's hypotheses held up against v26.7.6, and each fix is measured on a 300-file / 90 000-line / 4.5 MB diff (release, macOS arm64): 1. The full diff was buffered before parsing — `git_status::git` uses `Command::output()`. Now streamed line by line through the new `git_status::git_lines` into an incremental `DiffParser`: peak transient buffer 4 552 060 bytes → 50 bytes, at ~1.7× the parse CPU (3.97 ms → 6.76 ms) on the background thread, where it never touches a frame. 2. The snapshot was deep-cloned per holder inside `this.update`, i.e. on the UI thread. Now shared behind `Arc`: 2.41 ms → 11 ns per holder. 3. The element tree is not virtualized — confirmed, not cured. Rendering is not being redesigned here; instead the element count is bounded (see 4) and `MAX_RENDERED_FILES` caps the cards built at all, with a "… and N more" line for the tail. 4. Auto-collapse was per file, and counted only +/− while the rendered body also has context lines. Added `AUTO_COLLAPSE_TOTAL_LINES` over *retained* lines: sixty forty-line files, none individually large, went from 2400 side-by-side rows to zero, under a summary saying the diff is too large to render efficiently and pointing at expanding individual files or `git diff`. 5. The Changes panel probed independently and kept its own snapshot. Both now go through `spawn_shared_diff_probe`, which dedupes by cwd and installs one `Arc` into every watcher; opening the overlay while the panel already shows that repo now paints from the panel's snapshot instead of re-probing. Plus a repo-wide retention budget (`MAX_TOTAL_LINES`, `MAX_FILES_WITH_HUNKS`): 90 000 lines / 6.2 MiB of line text → 20 000 / 1.2 MiB. The `+N −N` totals deliberately escape every cap — they are compared against `--numstat` to detect staleness, so a capped total would disagree forever and re-probe in a loop. Small diffs are untouched: a forty-file, twelve-lines-each tree is not oversized and still opens expanded, asserted directly. Not verified: anything requiring the GUI. No frame timings, no visual check of the oversized banner or the settings row, and `AUTO_COLLAPSE_TOTAL_LINES` is a judgement call anchored on row count rather than a measured frame budget. Refs #239. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(diff): bound the untracked list and stop a moving default flipping toggles Five findings from the review of the previous commit, all confirmed against the source before acting. The untracked list escaped every bound the previous commit added. `git ls-files --others` reports the whole tree of anything not yet ignored, so a fresh clone before `node_modules` / `target` / `.venv` reach `.gitignore` answers with tens of thousands of paths — read through the buffering helper, retained without a cap, ignored by `oversized()`, and rendered one non-virtualized row each. That reaches the overlay without going through the diff at all, which is why the diff budget never saw it. It is now streamed through `git_lines`, capped at `MAX_UNTRACKED`, counted toward the oversized threshold, and rendered at most `MAX_RENDERED_FILES` rows with a "… and N more" tail. The reported count stays the true total via `untracked_total` — same split the diff side already makes between what is retained and what is counted, because a count that shrank with the budget would read as files having disappeared. The overlay's expand state was an inversion set — "files flipped away from their default" — which was correct while the default was per-file and stable. The repo-wide `collapse_all` moves the default for every file at once, so any refresh crossing the oversized threshold inverted every explicit choice simultaneously: the two files the user had opened snapped shut and the rest sprang open. It now stores absolute intent (`HashMap<String, bool>`), answered before the default is even computed, so nothing about the snapshot can reach it. Covered by a test that asserts an explicitly opened and an explicitly closed file both survive a transition in either direction while an untouched file follows the default. The Changes panel dropped freshly landed snapshots. `install_diff_snapshot` only wrote `right_panel.diff` when the panel was the one waiting, so a probe the overlay started was discarded even when the panel sat on that exact repo — the overlay rendered the new snapshot while the panel kept the old one, same window, same repo. The wait (`diff_pending`) and the data (`diff_cwd`) are now claimed separately: with probes deduped per cwd there is at most one in flight, so there is no out-of-order overwrite to guard against. The oversized banner reported `retained_lines()` as "diff lines", which after the budget fires is what was kept, not what changed — it read "20000 diff lines" directly under a header showing the exact +90000/-0. It now states loaded-of-total and names which cap ate the difference, and lists each axis that tripped the threshold so a big untracked list never reads as a claim that the diff is big. And the Changes panel deep-copied every untracked path String on every frame, on the UI thread, for two `len()`/`is_empty()` reads — the same cost class the `Arc` switch removed from the probe path. 995 tests pass, fmt clean, no new clippy warnings. The app was not built, launched, or driven; visual acceptance is the owner's. Refs #239. * no-mistakes(review): cap Changes panel rows and fix oversized banner truncation notice * no-mistakes(review): drive banner per-file truncation off parser flags * no-mistakes(document): correct changelog cost count and overlay-trigger docs * fix(diff): reconcile the diff-overlay work with the host-aware git refactor The rebase onto main lands this change on top of the remote-workspace work (#235, #242), which moved the git helpers into `tty7-core` and made every read take the pane's `Host`. Adapting rather than papering over: - `sidebar_diff_preview` moves to the core `Config`, where the struct now lives. - The diff and untracked reads go through `git_status::git(host, cwd, ..)`. - The shared probe, its in-flight set, and `install_diff_snapshot` key on (`HostId`, `PathBuf`) — the same path on two machines is two work trees — as does the Changes panel's `diff_pending`. - `diff_click_cwd` became generic over what identifies a repo, so the setting gate did not need to learn about hosts. - Main's newer card rounding reads `truncated`, which is an `Option` here now. The streaming diff read is deliberately absent at this commit: `Host::git` is buffered, so it is restored on top of a streaming host API in the next one. Refs #239. * feat(host): stream git reads whose size scales with the work tree `Host::git` returns a fully-buffered `Output`, and the remote implementation round-trips one over the wire. That is the right shape for the reads tty7 does constantly — `rev-parse`, `symbolic-ref`, `--numstat` — all of which answer in bytes. It is the wrong shape for `git diff HEAD`, whose output scales with the work tree rather than with anything the UI can show: this repository's own `git log -p -n 400` is 8.3 MB, and the diff overlay keeps a small fraction of it. So `Host` grows a second entry point rather than changing the first. `git_lines` delivers the same invocation a line at a time, and its **default implementation buffers** — every host gets it for free, nothing that works today changes shape, and it stays inside the "every git read funnels through the host" invariant instead of becoming a way around it. Overriding it is an optimisation, never a behaviour change: a test asserts the streamed and buffered reads yield identical lines. The local host reads straight off the pipe. The remote host adds `ControlRequest::GitStream`, answered with `ControlEvent::GitChunk` pushes and a terminating `GitEnd` carrying the exit status — the shape `WatchOpen` already proved out. Two protocol details worth the reader's attention: The **client** picks the stream id, which is why the reply carries none. Ids only need to be unique within a connection and a connection has one client, so choosing it client-side lets the receiver be registered *before* the request goes out. A server-assigned id arrives in the reply, leaving a window where a chunk that overtook it reaches a client with no entry for that id and is dropped under the unknown-id rule — silently losing the front of the diff. `WatchOpen` needs a whole deferred-start mechanism to close that window; this does not. The feature is **advertised and checked**, not assumed. A server predating `GitStream` cannot decode the variant, and an undecodable frame ends the connection — so sending it blind would not degrade, it would disconnect. Servers advertise `git-stream`; a client that does not see it uses the buffered `Git`, which is the path every remote pane used before this existed. Covered by a test against a peer advertising only `control` and `host-rpc`. Chunks carry newline-terminated line data batched to ~64 KiB, not verbatim slices of stdout: the server reads through `git_lines` itself, so line content survives exactly while `\r\n` and a missing final terminator are normalised away. The only consumer is line-oriented. Framing per batch rather than per line is what keeps a 90 000-line diff from becoming 90 000 frames. Re-measured on the rebased code, against real git output, release build (the previous figure was taken before the host refactor and no longer holds): buffered: 8 269 409 bytes resident, read 817 ms + parse 12 ms streamed: peak transient chunk 64 KiB, read+parse 557 ms Lower peak memory *and* faster end to end — parsing now overlaps with git producing output instead of waiting for all of it. The earlier synthetic measurement showed streaming costing ~1.7x CPU; that was an artifact of reading a warm page-cached file, where there was nothing to overlap with. The app was not built, launched, or driven; visual acceptance is the owner's. Refs #239. * fix(host): make unsubscribing a watch take effect at the drop, not after it CI's Windows job failed `watch_drop_unsubscribes`: an event for a file created *after* the subscription was dropped still reached a consumer holding a clone of the receiver. Tearing the watcher down is not instantaneous. The OS backend runs its own thread, and on Windows a `ReadDirectoryChangesW` completion can fire during teardown, reach the event closure while `raw_tx` is still alive, and be forwarded by a coalescer that has not yet noticed the disconnect. So "dropped" meant "stops delivering shortly", which is not what the subscription promises — and for a remote host it is the difference between releasing a server-side watch and leaking one. The handle now closes the delivery channel in its own `Drop`, before any of that unwinds. Batches already queued stay readable — `close` stops sends, not receives — which is the one thing a consumer racing its own drop may legitimately still see, and exactly what the conformance test allows for. Not this branch's bug: the change here is to git reads, not watches. But main is flaky in the same family — it failed the sibling `watch_coalesces_within_window` eleven hours ago and was hardened for that one — so this fixes the cause rather than loosening the test. Refs #239. * refactor(host): make the streaming git read part of the protocol Remote workspaces have never shipped a release, so there is no deployed server to negotiate with. The `git-stream` feature flag, the `has_feature` check and the buffered fallback behind it were all guarding against a peer that cannot exist — dead code that would have to be maintained, and read by the next person as evidence that older servers are out there. `ControlRequest::GitStream` is simply part of the control protocol now. Buffered `Host::git` stays exactly as it was, for the many reads that answer in bytes and have no reason to stream. The remote test that proved the fallback becomes one that proves the stream: the peer serves `GitStream`, splits a line across two chunks, and the client reassembles it — the case the reassembly exists for. Refs #239. * no-mistakes(review): fix remote git-stream deadlock, chunk encoding and stray docs * no-mistakes(review): stop git-stream batch growing after a send failure * no-mistakes(document): note git-stream protocol delta in remote-workspace contract doc * fix(host): bound a git stream's wait, its lines, and its concurrency Three ways the streaming git read could still hold or hang more than it should, all found reviewing #239's implementation. A stream is answered by pushes, so neither of the failure paths the rest of the client relies on covers it: the request deadline was satisfied by the immediate `Unit` reply, and keepalive watches the link, which stays up while a server-side git wedges on a network filesystem. The reader parked forever, on one of a small pool of blocking threads, and the diff probe it belonged to never released its per-repo claim — so that repository's overlay and Changes panel were stuck on "Loading…" for the life of the process. `git_lines` now waits `GIT_STREAM_IDLE_TIMEOUT` between chunks. Between, not across: a slow-but-alive read must be allowed to take as long as it takes, which is why a total deadline would be the wrong instrument. Draining moved to `drain_git_stream` so all three exits are reachable from a test without waiting out two minutes. "Incremental" bounded the number of allocations but not the size of any one of them: a line is only complete at its newline, so a work tree with a minified bundle rebuilt the whole-output peak inside `LineSplitter`, on both ends of a remote link and in the server's outgoing batch. Lines are now capped at `MAX_LINE`, and what is cut says so in the line itself rather than silently shortening a rendered diff. That also bounds the server batch, which makes `GIT_STREAM_CHUNK_MAX` a frame backstop rather than the only thing standing between a bundle and a 32 MiB payload. Finally, `GitStream` is the one request that spawns a thread outside the bounded worker pool, so nothing counted them. `MAX_CONCURRENT_GIT_STREAMS` per connection now does, with the slot returned by a guard so a refusal, a failed spawn and a panicking read all give it back — a leaked slot would be a permanent refusal, not a transient one. * fix(diff): stop the overlay re-walking the tree per frame, and re-probe a folded-in refresh Two things the shared-probe work left on the render path. The overlay asks six whole-snapshot questions while building its element tree — oversized, totals, retained lines, budget fired, per-file cap fired, untracked count — and each accessor walked `files` on its own, `oversized` walking the hunks too. `files` is deliberately uncapped (only hunks are), so that was six walks over a list whose length is the size of the working tree, on the UI thread, on exactly the tree this module exists to keep responsive. `DiffSnapshot::stats` answers all six in one pass and the per-question accessors are gone, so nothing can drift from it. Computed rather than stored, because the snapshot is built by hand with `..Default::default()` throughout the tests and a cached count would read as zero for every one of them. Deduping probes per repository is what makes one `git diff` answer every watcher, but a probe describes the tree as it was when it *started*. A refresh triggered after that — a command finished, an agent turn ended — folded into the running probe and was answered with a snapshot already known to be stale, with nothing left to trigger another look: the overlay's own re-check is gated on `loading`, which the landing clears, and the `GitStatusCache` change that would have re-armed it has been spent. A folded-in request is now remembered and re-issued when that probe lands. It converges rather than loops, because a quiet tree never sets the flag. * fix(diff): bound the stream queue, and stop two thresholds answering the wrong question Review follow-ups on the sidebar-diff branch. Five findings, four of them about a bound that was claimed but not held. The remote streaming read bounded both ends and not the middle. The reader thread serves the whole connection, so it cannot wait on a slow consumer — parking it there stalls every other reply and the keepalive with it — and an unbounded queue was the price. That reassembles the whole diff in a channel, which is the peak the buffered read was replaced to avoid, one container further along. The queue is now bounded instead of back-pressured: each chunk is charged to the stream's arrears, the drainer credits them back, and a stream 32 MiB behind is cut loose with an error rather than served. Real back-pressure would need credit-based flow control in the dialect; this is not that, and says so. `oversized` counted untracked paths on its file axis, and collapsing every file body removes no untracked rows — that section has no bodies to fold. A tree with an un-ignored node_modules and three edited files hid the three cheap things, kept the expensive one, and told the reader their working tree was too large to render. The untracked list is bounded where it is built: MAX_UNTRACKED on retention, MAX_RENDERED_FILES on rows. AUTO_COLLAPSE_TOTAL_LINES counted the context lines git prints around every hunk — four to six retained per line actually changed — against a threshold set as if it were reading `+N -N`. It fired on trees whose header said 400. 8000, compared against the 20000 the parser stops retaining at, since collapsing everything is the heavier of the two interventions and should not arrive first by much. A probe that could not run produced an empty file list, which renders as "Working tree clean" — a claim about the repository, made because a read timed out. Newly reachable, too: a stream can be refused or go silent where a buffered read could only arrive or error. DiffSnapshot::read_failed keeps the two apart. Also: StreamStop's doc comment had been glued onto StreamSlot, leaving the enum undocumented and the guard described twice; the overlay header asked totals() beside stats() rather than through it; and the watch-teardown fix riding along on this branch was in neither the PR body nor the CHANGELOG. Tests: the queue budget both ways (a stream that outruns it is cut loose, a larger one that is drained is not), the untracked axis, an ordinary context-heavy afternoon sized to fail against the old threshold and pass against the new, and empty-because-broken against empty-because-clean. Each was checked to fail against the behaviour it replaces. 1603 pass, 0 fail; fmt clean; no new clippy warnings in the touched files. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9c00648875 |
fix(ui): keep the window-drag arm alive across a repaint, and make every header draggable (#252)
* fix(ui): make every header draggable, and keep the arm alive across a repaint (#221)
Two changes to the same code, which is why they land together.
Five rows that stand in for the title bar — the tab rail's top zone, the
settings page's top strip, the detail panel's top zone, and the code and
diff overlays' headers — armed their drag with an `Rc<Cell<bool>>`
allocated inside the render function. A redraw between the press and the
first drag event handed the next frame's listeners a fresh, zeroed cell
while the press had written to the old one, so the whole hold was dead
until you released and tried again.
The press itself schedules that redraw: these rows carry `on_double_click`,
and gpui calls `window.refresh()` on mouse-down for any element with a
click listener. So a drag only survived if the first move beat the next
vsync — 16ms at 60Hz, 8ms on ProMotion. A mouse press physically nudges the
pointer and often won that race; a trackpad press is a finger pushing down
without translating, and almost never did. That is the trackpad-vs-mouse
split the issue reports. The terminal's cursor blink (a 530ms `cx.notify()`
loop) disarms it on its own even with no press at all.
`window_move_gesture` now holds the flag in `window.use_keyed_state`, which
survives frames — where gpui-component's own `TitleBar` has always kept it,
and why the ordinary caption strip was never affected. Keyed rather than
`use_state` because one builder serves several call sites and `use_state`'s
`CodeLocation` id would collide when two of these rows are on screen at
once (the rail's top zone plus an overlay header is a real combination).
A longer-lived flag has to be cleared explicitly, so releasing outside the
row disarms too; with a per-frame cell the frame boundary did that for free.
Nothing else about these rows changes — same hit boxes, same geometry, same
`WindowControlArea::Drag`, same double-click.
Grabbing the window by a header is a property of the whole app, not a
per-surface feature, so a user never has to learn which rows are draggable.
Written down beside `window_move_gesture`, along with the two things it
takes beyond arming the gesture: non-controls inside a header take no hit
box (the rule #202 set for the "duo" mark, so the drag falls through them),
and a header whose contents *do* take hit boxes by design needs a floor on
its flexible spacer.
- `panel_title` — the detail panel's section header, shared by Info,
Outline, Changes, Files and the remote Files browser — is draggable now.
Its one un-`occlude()`d control (SFTP's refresh tile) gains the wrapper
every control on a drag row needs, or Windows' HTCAPTION eats its clicks.
- The horizontal tab strip keeps a bare 80px slice of caption. Its spacer
was a `flex_1` with no minimum, so it collapsed to exactly 0px once the
chips saturated the row (~7-8 tabs on a 1440px window), leaving only three
6px gaps and a hairline above and below the chips to grab — the "the
region that works seems very small" half of the report. The chip row's
fixed-chrome reserve is corrected to match: a stale flat 100px, sized when
the corner held a 30px "+" and a 30px "⋯", becomes the ~137px the corner
actually occupies plus the handle. Chips reach their minimum width and
truncate a tab or two sooner, and the window is always grabbable.
- The rail's top-zone spacer gains the same floor.
`ui::app::window_drag_tests` drives the real `title_bar_drag` row through
gpui's test platform, where `start_window_move` is `unimplemented!()` and a
panic is therefore a reliable "the window would have moved" detector. It
pins the invariant (press → repaint → move still drags), that a press alone
does not, that a release disarms, and that two rows on screen keep separate
arms. A control test keeps the old per-frame-cell pattern alongside and
asserts it still loses the drag to the identical event sequence — without
it, the invariant test could pass for the wrong reason.
* no-mistakes(review): occlude resize handles; correct chip-reserve arithmetic
* no-mistakes(document): reorder changelog sections; record non-draggable header exclusions
* no-mistakes(document): make panel grab-handle docs version-neutral and platform-accurate
* no-mistakes(document): make workspace_head panel-width doc version-neutral
* docs(changelog): re-file Unreleased entries after the rebase onto main
The rebase onto
|
||
|
|
1f09a62568 |
fix(ui): give corner-filling children their own radius (#236) (#244)
* fix(ui): stop filled children squaring off rounded corners The cursor-shape toggles (Block / Bar / Underline) reported in #236 look rough because the selected segment's fill covers the whole corner of the track it caps, and its outer edge is a hard, unantialiased vertical cut. The track's own border arc is drawn correctly and antialiased — it just floats *inside* that square, so the corner reads as a stair-step. The controls were relying on `overflow_hidden` to shape their end segments' fills to the track's rounding. It cannot do that. `gpui::ContentMask` is a bare axis-aligned `Bounds`; `Style::overflow_mask` builds it from the element's bounds shrunk by the border widths and drops `corner_radii` entirely, and every shader applies it as a hard `clip_distances < 0` discard. So the mask only ever cuts a square, and it never antialiases the cut. A container's own corners come from somewhere else — the quad shader's SDF, `saturate(0.5 - distance)` — which is why a plain rounded card renders smooth while anything with a filled child in its corner does not. That divergence is the whole bug, and the reporter's screenshot shows both halves of it: the corner with the selected fill is square, the corner without one is a clean arc. The fill has to carry the radius itself, so it goes down the SDF path too. It sits one border-width inside the track, so the concentric radius is `outer - border`. `ui::rounding` states that rule once, with the constants and the corner-assignment helpers, and unit-tests the invariants (inset is strictly tighter than the outer radius, clamps at zero, only the end segments cap the track). Applied to every place a child paints a fill into a rounded corner: * the segmented controls (the reported one, plus the others `segmented` serves), * the −/value/+ steppers' hover fills — those glyph boxes also had to be pinned to the track's content height, because a padded auto-height box measures 31px against a 22px content box and its rounded corner would land 4½px outside the visible strip, * the theme picker's flush-mounted previews, * the diff overlay's card headers and the row that closes a card. Not reproducible locally: this is a rendering-geometry defect, not a platform one, but it is most visible at a device pixel ratio of 1, where the clip's hard edge is a whole physical pixel. Verified by reading the gpui mask/shader source and the reporter's screenshot pixel by pixel, and by the geometry tests; the on-screen result is left for visual acceptance. Refs #236 * no-mistakes(review): round diff card header when body is empty * no-mistakes(document): point Unreleased changelog link at v26.7.6 * no-mistakes(document): untrack AGENTS.md per gitignore dev-tool convention --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
208454e202 |
feat(remote): remote workspaces — a window that is one machine
Split the framework-free half of tty7 into `tty7-core` and add a headless
`tty7-server` built on it, so a workspace's filesystem, git and session state
can live on another machine while the GUI stays where it is.
- `crates/tty7-core`: wire protocol, session daemon, PTY, native SSH engine and
the domain model, with no gpui dependency. Module paths are unchanged.
- `crates/tty7-server`: the same daemon with no GUI attached, linked fully
static against musl and pushed onto the remote box. One dependency, on
purpose — a second one the GUI also needs belongs in core.
- `Host` trait + `HostId`/`HostRegistry`: every fs/git/watch call a workspace
makes goes through the machine it belongs to. `LocalHost` answers on this
box, `RemoteHost` over a routed control connection.
- `ui::host_ops`: the GUI's single door to a `Host`. Host calls block, so all
of them run on the background executor with the result landed on the UI
thread; de-duplication, staleness and error reporting live here rather than
at each call site. Enforced by a CI grep.
- Connect flow: home page → pick a configured SSH host → the machine's own
workspace list → a window bound to one workspace on it. Workspace switcher
groups by machine, this computer included.
- CI: static musl builds of `tty7-server` for x86_64/aarch64 via
cargo-zigbuild, a host-boundary grep, and version stamping factored out of
the nightly workflow. Both new jobs are non-required so branch protection
does not wedge open PRs.
Design and the interface contract it was built to are in
`docs/2026-07-27-remote-workspace-{design,impl-contract}.md`.
|
||
|
|
a67cf2b2ad |
feat(chrome): make the title bar's line whole off macOS
macOS fills the window's leading corner with the traffic lights and `TITLE_BAR_LEAD` reserves them 80px. Everywhere else that corner held nothing: the caption row's only contents are the rail's "+" and collapse at the rail's right edge and the corner chrome at the window's, so the left third of the row read as unfinished rather than restrained — while Windows treats the top-left as the app's identity slot. Three parts, all of them about that row: - `window_mark()` draws the "duo" mark (the app icon's own art) at the head of the rail on `CONTENT_INSET`, the line the search box and every row label below it start on, and follows the rail's controls into the title strip when the sidebar collapses. It is drawn, never clicked: no hover capsule, and deliberately no `occlude()`, so the drag region underneath still takes the press and the strip stays grabbable. - The rail's stand-in row now reserves the same hairline the real `TitleBar` draws inside its own height. Without it the bar centred content on 19.5 and the rail on 20, and the mark hopped half a pixel as collapsing the rail handed it from one to the other. - With the detail panel open off macOS the bar is hoisted above `[terminal | panel]` so the window controls can reach the corner, which left the code and diff overlays — anchored to the terminal column — starting 40px down, with headers drawn to *be* the title bar landing a row low. They now hang on the row that owns the bar, inset by the panel's width. Covering the caption row that way needs the headers to carry its gestures, which neither ever did with the panel open or closed: `title_bar_drag()` gives both (and the rail's row, which grew the same wiring by hand) drag-to-move and double-click-to-zoom, and their controls are `occlude()`d so HTCAPTION stops eating the clicks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD |
||
|
|
e41afaf857 |
feat(windows): one window per workspace
tty7 had exactly one window, so `main` opened it inline and every app-wide duty — tray, menus, the quit hook — lived in `Tty7App`'s constructor. This splits those apart: a *workspace* is the persistent identity (tabs, splits, cwds, name) and a *window* is a transient view onto exactly one of them. - `ui::windows` — the app-level window registry and the single place that opens a window. Exactly one window per workspace is enforced there: the daemon gives each pane a single subscriber, so a second window on the same panes would silently steal the first's output. `open` focuses the existing window instead. New windows cascade so one never lands on top of another. - `WorkspaceStore` owns session.json, so windows never race each other as writers. Closing a window *detaches* — panes keep running in the daemon and the entry stays for the picker; `StopWorkspace` kills the sessions and keeps the layout; `DeleteWorkspace` also forgets it. - Window menu lists every workspace with a monogram badge and a liveness dot, ⌘1–9 for the first nine. Same list in the palette; closed ones also appear in a home-page picker with a coarse relative age. - Sidebar collapse and right-panel visibility move onto `Tty7App`, so toggling one window's chrome leaves the others alone; the config value becomes what a new window starts with. Panel *width* stays shared — a width is a preference, not a view state. - Tray, menus, and the quit hook now walk the registry rather than belonging to a single window. Protocol goes to v2: `RemoteKind::Wsl` is a new enum variant, which is not the additive change it looks like — the enums carry no `#[serde(other)]`, so a v1 peer fails the whole decode and drops the pane's connection. The handshake now catches that skew and offers a restart. |
||
|
|
fd1062f564 |
fix(right-panel,editor): restore the git dependency and address review findings
The branch had `gpui-component` pointed at a sibling checkout by absolute path, which is why every CI job failed at manifest load. Point it back at the fork's `tty7` branch (now carrying the custom-button label-color fix the chrome tiles depend on) with the `tree-sitter-languages` feature, and re-lock. Review fixes on top: - **Changes tab churned.** `right_panel_invalidate` dropped the cached diff on every `GitStatusCache` notification — including unrelated repos' — so the list blanked to "Loading…" and spawned a fresh `git diff` several times a second while a pane produced output. Replaced by `right_panel_refresh_changes`, which compares branch and totals first and re-probes in place, mirroring the diff overlay. - **Changes tab could wedge on "Loading…".** A probe dropped because the cwd changed mid-flight left `diff_cwd` set and `diff` empty, and the render path only spawns when the cwd *changes* — so nothing re-probed. Spawn when nothing is cached and nothing is in flight. - **Find references blocked the UI thread.** `cx.spawn_in` runs on the main thread; the up-to-200 `read_to_string`s for the row previews now run on the background executor, as the comment already claimed. - **LSP frames could be lost or reordered at startup.** `send` checked `ready` outside the `queued` lock, so a frame could park behind a handshake that had just finished and never go out. `ready` now flips under that lock in `mark_ready_and_flush`. - `MarkScanner`'s ESC-in-payload branch bypassed the payload cap, so a stream of bare ESCs inside an unterminated OSC grew the buffer without bound. - The file tree's search frontier used `Vec::remove(0)`; a wide tree made that quadratic. `VecDeque`. - `procs()` documented a pane check it didn't make; it takes the pane id and makes it. - Four doc comments had been orphaned onto newly inserted functions (`pty`, `smooth_scroll`, `foreground_agent`, `file_expanded`). |
||
|
|
403cfd47a1 |
feat(right-panel): docked detail panel with Info, Changes and Files tabs
Add a right-hand detail column showing what the active pane is, not what it prints: session facts plus its process tree and listening ports (daemon-side procinfo, pull-based via QueryProcs), the working-tree diff, and the file tree. Tab row lives in the title bar, body in right_panel. Also record OSC 133 command marks client-side so the panel's Outline can list a pane's commands and scroll back to one, keyed on row text since absolute scrollback indices drift once history fills. |
||
|
|
2dd817d8cd |
fix(theme): review follow-ups — diff overlay background, hot-reload window effects, docs
- Diff overlay paints the gradient/opacity-aware window background instead of the stale representative solid. - Config hot-reload now re-runs apply_theme with the window (blur flip, traffic-light re-pin) and re-syncs the Appearance opacity slider, so hand-edits to config.json / theme files take effect fully. - Document the theme background/window settings in features docs (EN + zh-CN). |
||
|
|
4cdd413f5d |
feat(diff-overlay): per-tab lifecycle and side-by-side view
The overlay was a single app-global slot rendered over whatever tab was active: switching tabs left a stale diff occluding the new tab's body, with focus on the terminal underneath so Esc couldn't even close it. Move the overlay state onto Tab so it follows product intuition: switching away hides it, switching back restores it (re-probing if the status cache disagrees with the shown snapshot), and closing the tab drops it. focus_active now hands focus to the returning tab's overlay so Esc keeps working; probe landings update every tab showing the same cwd. Also replace the unified diff body with a GitHub-PR-style side-by-side view: removed lines fill the left column, added lines the right, aligned positionally within each change block; context lines land on both sides and leftovers pair with a muted empty cell. |
||
|
|
f26045fbb5 | style(diff-overlay): rustfmt the diff overlay module | ||
|
|
68cc4680d4 |
feat(git): click a sidebar git line to open a working-tree diff overlay
The sidebar row's third line (branch + counts) is now a click target: it opens a read-only, GitHub-style unified diff of that pane's repo as an overlay covering the terminal body. The sidebar stays visible, so other tabs' git lines remain one click away — peek at another session's changes without switching tabs. - terminal/git_diff: parse `git diff HEAD` into files/hunks/lines (renames, binary, quoted paths, per-file line cap), plus the untracked list `diff` can't see; probed off-thread, read-only via the shared GIT_OPTIONAL_LOCKS=0 helper. - ui/diff_overlay: per-file cards with collapsible hunk bodies (big files collapse by default), line-number gutters, green/red tints, Esc / close / re-click toggles; auto-refreshes off the existing GitStatusCache triggers when shown counts go stale. |