Skip to content

v1.38.0

Choose a tag to compare

@VictorQuilgars VictorQuilgars released this 20 Sep 15:21
6d39d9c

Added

  • A branch, a remote branch or a tag opens its card from the side bar. The card — what the branch is, where it stands against its upstream, whether it merges cleanly, its next steps — could only be opened by clicking the chip on its tip's graph row, which meant finding that row first. Every one of those rows now leads with a card button, before the acts: reading comes before acting. It opens exactly what the chip opens, and in the order the card needs — the tip is selected first, then the card opens, because a card whose reference is not the selected commit closes itself on the next render. A tip that is not on the page yet is reached the way everything else reaches one. (#274)

  • A pull request can be checked out, opened in a worktree and read as code — a fork's included. Its row and its sheet offered View, Copy link and Open on GitHub: a request whose head was a branch of this repository could be checked out from Branches › REMOTE once you had worked out which remote branch it was, and a request from a fork could not be checked out at all. Every request's head is fetched from refs/pull/<n>/head, which GitHub publishes whatever repository the head lives in, into a local pr/<n> — never forced, so a pr/<n> you have committed on is refused with its reason rather than quietly rewritten. From there: Switch to its Branch, Open in a New Worktree, View Changes — the files and diffs from its base to its head, the base...head the request itself shows — and Compare Base and Head, the two trees as they stand. Reading the changes fetches and stops there: the objects are what a diff needs, and switching branches to read one is how a review costs a stash. The base is read from the remote, not from a local branch of that name which may be behind. The sheet carries the same three as buttons beside its link. (#290)

  • A worktree row says where it stands, and does what a folder can do. It showed a path, a branch and whether it was the main one; locked was read from git and never used. Each row now carries what it is — active for the one this window is showing, a lock with the reason git was given, a dot when it holds uncommitted work, its branch's ↑ahead ↓behind, and a mark when its folder is gone and git will drop it on the next prune. From the row: Open Terminal Here, Reveal in File Manager, Lock / Unlock with an optional reason, and Copy Working Changes to… another worktree — which stashes here and applies there, and keeps the stash whether or not the apply lands, because dropping the one copy of somebody's uncommitted work when the second half of a two-step operation fails is not a risk to take on their behalf. A branch checked out in another worktree offered Switch, which git refuses: its row now offers Open its worktree "review", and any branch can be given Create a Worktree for "x"… without typing its name. The worktree list is one shared parse now, where it had been written out twice. (#285)

  • A stash is a commit, and its row treats it as one — every stash, not just the newest. Only stash@{0} ever had a graph row, so an older stash could not be compared, selected or copied at all. Each row now offers Compare with HEAD, Compare with Working Tree and Select for Compare, and a Copy submenu with its full sha, its message and its patch — each resolved from the stash's own ref, so stash@{3} is reached exactly like stash@{0}. Preview contents is called Show Changes, which is what it does. (#287)

  • A branch is brought up to date, given an upstream of your choosing, and tidied — without switching to it. Pull was offered on the checked-out branch alone: bringing a second branch forward meant switching to it, pulling, and switching back. Pull "x" from its upstream now fast-forwards any local branch from where it is, and refuses with the reason rather than a bare failure — diverged from origin/x (1 ahead, 1 behind) — rebase or merge it, tracks no branch, already up to date, each of which calls for something different. Change Upstream… points a branch at any remote branch, listed for you: Set Upstream only ever set <default remote>/<same name>, which was the one upstream it could give. Rebase onto its Upstream and Squash Fixups sit on the branch you are standing on, because both rewrite its commits — and the autosquash rebases onto the fork point, never onto the upstream itself, so the fixup! commits fold in and the branch does not quietly move onto whatever the remote has grown since; with nothing to fold it refuses, because a rebase would rewrite every hash for nothing. The operations live in the shared git core, so the panel and the desktop give the same answers. (#280)

  • A branch row offers what its tip commit offers. Creating a branch, a tag or a worktree from that branch, resetting the current branch to it, copying its sha or its message, comparing it with the working tree or with something selected earlier — all of it existed on the graph row the branch happens to point at, and the side bar's + always branched from HEAD. The branch's own row now carries them, in the menu's own places: the copies in Copy, the comparisons in Compare, one of each rather than two. Resetting is not offered on the branch you are already on, and a comparison with a commit already selected is greyed out against itself, exactly as on the graph. (#281)

  • A remote branch can be merged, rebased onto and compared from its own row. Branches › REMOTE offered a checkout and a delete: merging origin/release into what you are on meant creating a local branch for it first. Its row now offers Merge into , Rebase onto this and Compare, and Hide Remote "origin" from Graph — which was on the Remotes row alone, a view away from the branch that made you want it. (#282)

  • Every side bar row does its commonest act on the row itself. Only a branch row showed anything under the pointer, and what it showed was the whole menu behind a kebab: the stash, tag, remote and worktree rows were reachable by right-click alone. Each row now carries two or three icons for what its own state calls for — a branch three commits behind offers Pull, one that is ahead offers Push, one the remote has never seen offers Publish, and one level with its upstream offers no sync at all, because an icon that would do nothing is worse than no icon; a branch whose upstream is gone offers none either, since there is nothing left to sync with. Beside it, Switch on any branch that is not the one you are on, and on a tag; Apply, Pop and Drop on a stash; Fetch and Open on a remote; Open on a worktree that is not the one on screen. They are real buttons at every moment, so the keyboard reaches them — tab into a row and its acts appear — and on a touch screen, which has no hover to discover them with, they are simply there. The full menu stays behind the kebab: nothing moved out of it. (#274)

  • One click on a side bar row takes the graph to it. A click on a branch did nothing, a tag answered only a double-click, and a worktree row was a no-op — the only way to a reference's commit was the graph's own / finder. A single click on a branch, a tag, a stash or a worktree now selects its commit in the graph and scrolls to it, growing the page when the commit is not loaded, the way the finder reaches a tip three pages down; a worktree row goes to its HEAD, and the one this window is open on goes to the working changes, which are the only ones this graph can show. The double-click keeps every meaning it had — switch, land on a branch, open the stash — and takes back the reveal the first press had armed. (#275)

  • The filter field narrows whatever list is on screen, and a list of paths can be a tree. Only the branches had a filter: a repository with two hundred tags was read by scrolling. The field is above every list view now — tags, stashes, remotes, worktrees — saying which one it is narrowing, and matching a stash by what it says and a worktree by its folder or its branch. Beside the section's count, a list / tree toggle wherever the names hold slashes: the branches can be flattened, the tags can be a tree, and the choice is kept per view on this machine. A filter still flattens whatever the choice was — a tree that stays folded while you type reads as an empty section — and a list with no slash in it is not offered a toggle at all. (#276)

  • A search or a comparison can be kept, named, and found again after a restart. A query built out of author:, file: and the rest was gone the moment the field was cleared, and a comparison was gone with the view. Keep — beside the search field, and on the comparison's header — names what is on screen and files it under Kept in the side bar: a click puts it back, the pencil renames it, the cross drops it. A kept comparison remembers which of its files you had ticked as reviewed, and the tick survives the restart with it. Everything is kept per repository and outside the renderer — in the settings both hosts already persist — so a VS Code tool tab, whose storage the overview webview does not share, finds the same list. (#256)

  • Pull requests, grouped by what they need from you. The view grouped by account — whose is it. A switch above the list adds the other reading, By what they need, and it is remembered: Current Branch, Needs Your Review, Changes Requested, Ready to Merge, Blocked, Waiting for Review, Draft, Other — each with its count, the rows keeping their actions, and every open request in exactly one group: the first rule that holds decides, so a request approved with red checks is Blocked, not Ready, and a draft asks nothing of anybody yet. What a list row does not carry — the review decision, the state of the checks — comes from the same kind of search the overview runs to count them, asked only while this reading is on. Pin a request to the top; snooze one until tomorrow morning, for a week, or until its next update — a snooze is lifted by its day coming or by the request moving, and both are kept on this machine, per repository. The overview's Waiting on you lines now open the view in this reading, on their group. This is the shape #91 asked for in the panel: a reading of the pull requests view, not a page. (#257)

  • The search field takes file:, after: and before:, beside author:. file:src/main narrows the graph to the commits that touched a path, a folder or a pattern — git answers it (git log -- <path>), never case sensitive, and a bare word finds any path that contains it, file or folder, so file:cache is not a file called cache. after: and before: take a day (2026-09-01, 2026-09, 2026) or a span back from now (36h, 5d, 2w, 3m, 1y); a day starts for after: and ends for before:, so the two together are that day. Operators combine with each other and with the words — author:ana file:src/main after:1m cache — every one of them has to hold; quotes group a value with spaces, since: / until: / @: / ?: are understood too, and an unknown word: is searched as text. The query stays text in the field; under it, while it has the focus, a panel shows the operators in force as chips, each removable on its own, then the operators there are with an example each — a click writes one at the end of the query. A date that cannot be read is said on its chip rather than matching nothing. The search inside diffs and the model's search are given the words, not the operators; and a contributor whose name holds a space now filters as one author (author:"Ana Maria"). (#255)

  • A branch or tag chip opens its card. A click on a chip in the graph did nothing; what a branch is was spread over the overview, the chip's menu and the side panel. The click now selects the chip's row and opens the reference's card over the details panel — and only over it: the graph beside it stays in reach. A branch: the issue and the pull request it carries, and the worktree it is out in; an Upstream card — the name, how far apart (1↓ 2↑), one word for it (Up to date, 2 to push, Diverged, Missing from the remote, Unpublished) and the buttons that fit: Publish, Pull, Push, Fetch; a Merges into main card with the one verdict that matters — No Conflicts, 2 Conflicts (the only filled chip), Merged · Safe to delete — from the same prediction the merge dialog runs, Behind main by 3 commits, and Merge then Rebase on the checked-out branch; its last commit, who and when; then Next steps, each a sentence and one button — Switch, the pull request or the door to one, Compare with the current branch, Merge, Rebase, Delete (never the default branch from there). The kebab beside the name is the chip's own menu, not a second copy. A remote branch: a way to land on it, to compare, to open it on the remote, to delete it there. A tag: the commit it points at; for an annotated one, who tagged it, when, and what they wrote; whether the remote has it — it asks the remote, so that line arrives after the rest, and could not ask is not no; then Create Branch, Switch (Detached), Push — unless it is already there — Compare, Delete. The same chip again, Escape, the close button or a selection that leaves the tip closes it; the double-click still switches, and takes back the card the first click was about to open. A ghost chip — a branch's name worn by a row further down its line — opens the branch's card on its tip. (#258)

  • Find a branch, a tag or a worktree from the graph, with /. A small field opens at the top right of the graph, and the graph answers as you type: it goes to the best match on every keystroke and lights its row, without selecting anything. Under the field is the one match it is standing on; ↓ and ↑ walk the others in the graph's own order — 2 of 7 — and wrap; Enter takes the match as the selection. It finds local and remote branches, tags, and worktrees by their folder or their branch, from the repository's own lists rather than from the rows on the page — so a tip three pages down is found too, shown dimmed, and Enter grows the page to reach it the way a search hit is reached, up to the same limit; past it, how far back the commit is gets said instead. Matching is by substring, never by letters-in-order — three letters in order match a third of a large repository — with the whole name first, then a prefix, then a prefix of the last path segment; d/foo finds debt/feature/foo. A remote branch level with its local one is one stop, not two; what you hid from the graph is not found. Nothing matching turns the text red rather than adding a line; Escape closes it and keeps what you typed for the next time. Never while typing in a field; also in the graph header's menu. (#252)

  • Jump to the merge target with t, and see where it is. The overview already said how far the current branch is from the branch it will merge into; the graph did not show where that branch is. t selects its tip from anywhere in the graph, growing the page when the tip is beyond it, like u now does for an upstream that is off the page; with no target — on the default branch, or detached — nothing happens. The rows a branch is read against wear a mark at the graph's left edge: HEAD, its upstream, and the merge target in the purple a merged request wears — a thin bar with a faint band up to the node, one bar split in two when a row is both, opening into its names under the pointer. The minimap marks the target's day on its top edge with a square, beside the two triangles. (#253)

  • ? lists the graph's keys. One sheet, in groups — moving, going to w h u t, selecting, finding — with the keys in a column of their own; also in the graph header's menu. A test reads the graph's key handler and fails when a jump key is not on the sheet.

  • u reaches the upstream on the desktop too. The key existed and did nothing there: the desktop never knew the upstream's name, only how far it was. Every local branch now carries the branch it tracks, from one more field of the same for-each-ref — no process added.

  • Resolve every conflicted file with the model, in one click. The model could already resolve a conflicted file — from inside that file's resolver, one file at a time, so a merge that conflicted in four files was four round trips. Resolve all with AI, on the conflicts panel's list, runs the same request over every file whose content conflicts, two at a time, with its progress and a Stop; the chevron beside it takes a guidance line that goes with every file. Each result is written and staged as it lands, and waits under Resolved files marked AI, with the model's explanation, a Review of what the file becomes and an Undo that puts it back in conflict as git wrote it — from the index's own record of what the resolution replaced, and never on a file changed since, nor once the operation is over. A file deleted on one side is left to you, a file the model could not resolve stays in conflict with the reason under it, and nothing is committed: the panel stays, with its Commit & Merge, until you commit or abort. (#269)

  • A branch's card shows the pull request it carried, whatever its state. The app only ever listed the open requests, so a branch whose request was merged last month said nothing of it. The card now asks GitHub for every request the branch has had, and shows the one that matters — an open one first, otherwise the latest — with its state, Open, Draft, Merged or Closed, on its pill and in its next steps; View opens it in the app's own pull request sheet, merged and closed ones included. Create a Pull Request is offered only when no request is open and the last one did not take this very commit.

Changed

  • The graph draws the rows on screen, not every row it holds. Every loaded commit was an element — its row, its node, its part of the edges — and a page is 500 commits, Load more 500 more: a few pages in, every scroll and every render paid for rows nobody could see, and a side-bar column paid the most. The graph still holds every commit it has loaded — the keys, the jumps, the selection, the search and its count work on all of them — but only the rows near the viewport, and the edges that cross it, are drawn; the scroll height is still the whole history's, the selected row is always there, and a branch being dragged keeps the row it left from. Measured on a 50,000-commit repository after five Load more (3,000 commits held): 3,000 row elements become 38 (114,007 nodes under the graph become 1,451), a frame of scrolling goes from 66.6 ms to 16.7 ms, a click on a row reaches the screen in 30 ms instead of 171, and the renderer's settled heap from 40 MB to 16. npm run measure reports those three numbers now. (#251)
  • The drawers are cards of the frame. Creating a filter, a pull request or an issue — and reading the model's answer — opens a drawer that was glued to the side panel's edge with square corners, between cards that are rounded and set apart. It is a card now: one gap to the right of the card it comes out of, level with its top and bottom, rounded like it. Under the flush layout it is the flush extension it was.
  • Discarding asks with the app's own dialog. The trash of the working changes, Discard on a file and the deletion of an untracked one asked with the system's dialog, the one place the app still did; they ask like every other destructive action now, its button in red.

Fixed

  • The issue mark is the forge's, and the card button is not a third ring. An issue was drawn as a ring around a stroke and a dot — a ! in a circle, which is info to the pixel. Side by side on a row the two could not be told apart, and one of them is a button. The issue mark is now what every forge draws an open issue as, a ring around a filled node, and the side bar's card button is a sheet with writing on it rather than a third ring.
  • The graph's period band comes with the scrolling and goes with it. Today, Yesterday, March 2026 — the band names the stretch of time the top visible row is in, which is worth knowing while the rows are moving under you. Standing still it stated the top row's own date a second time, in a chip that sat over that row's sha and read as though it belonged to that row. It fades in on a scroll and out about a second after the last one, so it answers the question only while it is being asked.
  • A branch row's icons say what they do. Publish wore a cloud — which is the REMOTE section's own mark, a place rather than an act — where publishing is a push (push --set-upstream); it wears the push arrow now, and since a branch either has an upstream or has not, the two never share a row. Switch wore arrowSwitch, a borrowed silhouette whose open paths were declared filled: it rendered as a dark blob at every size, on the rows, in the branch card's next steps and on the pull request sheet. It is replaced by a mark drawn in the set's own hand — the lane you are on, and a move off it — and the old file is gone rather than left to be picked up again. On the pull request sheet, the arrow between the head and the base is now a plain arrow, which is what it always meant.
  • A branch that is behind offers its pull on the row. The first thing the row icons promised — behind → pull — was the one that did not happen: the row was only ever handed the checked-out branch's pull, and the fast-forward for a branch you are not standing on had landed under a different name. Both are the same act to the eye, so the row draws one icon and calls whichever it was given.
  • A branch merged by a squash or a rebase on GitHub is merged on its card. Its commits are nowhere in main, so git called it unmerged — No Conflicts, Behind main by 18 commits — and once GitHub had deleted it with the merge, Missing from the remote offered to publish a finished branch again. When the branch's request was merged from the commit the branch is on, the card says Merged — By PR #248 · Safe to delete, Deleted from the remote when PR #248 was merged, and offers deleting the local branch, not publishing it. Without GitHub, nothing changes: git cannot tell.
  • A merged branch's card offers no Fetch. It had nothing left to learn from its remote — what is left is deleting it.
  • A branch card's next steps are never cut. Compare with feat/resolve-all-conflic… did not say what it compared with: in a narrow panel a step takes a second line now, a long branch name breaking where it must, and its button keeps its place.
  • A branch's card deletes either end of it, or both, and says what the remote end costs. Its Delete Branch deleted the local branch alone, and the remote one was a second trip — to the remote branch's chip, once the local one was gone. When the branch tracks a branch the remote still has, the button now opens the choice: the local branch, the remote branch only, or both with one confirmation, the local one first; the remote branch's card offers the same when a local branch tracks it. Each choice that touches the remote names its cost: the open pull request that deleting the remote branch closes, and a merge that exists only on this machine — Merged is measured against the local main, so a branch merged by a merge not yet pushed read Safe to delete while its pull request was still open; the card says Not on origin/main yet now. And the remote branch's own Delete failed every time: it asked the remote to delete a branch called origin/x.
  • The model's two actions on a commit are each offered once, and by name. The header of a commit's details carried an unlabelled AI icon whose menu had two entries: Regenerate message, and Explain this commit — which is the Explain button just below it, a second time. The icon is now a button that says what it does, Rewrite, beside Compare: one click asks the model for a message from the commit's real diff and opens the editor on it, to be reviewed — nothing is committed until you confirm; on a commit that cannot be rewritten here (a merge, a root, another branch's) the proposal is copied instead, and the tooltip says so beforehand. The editor itself gains Generate, to ask again while reviewing. Explain stays the one way to an explanation, its guidance a chevron away. In a narrow panel the two header buttons keep their icons and drop their words.
  • A commit's refs are names again, not letters. In the one-line header of a commit's details the branch and tag chips shared the line by shrinking, and four of them in a narrow panel came out as v…, ext…, ori…, m…. A chip is now drawn whole or not at all: as many as fit, in the order that matters — where HEAD is, the branches, their remotes, then the tags — and the rest behind a +N that lists them, whole, on a click. Only a chip that is alone and still too wide is cut, with its name in its tooltip; the row is measured again when the panel is resized.
  • A message git wrote no longer outlives its operation. Mid-merge, rebase, cherry-pick or revert, the commit form starts from the message git has already written — and when the operation was finished or aborted from a terminal, that text stayed behind as a draft nobody had typed. On a clean tree a draft keeps the form open, so the Next steps card never came back, and in a short panel the message field was scrolled out of sight: nothing on screen said why. git's message is now marked as git's, and goes once no operation is in progress — if it is still word for word what git wrote. Edited, it is yours, and it stays. A draft left behind before this fix is not marked, and has to be cleared by hand once.
  • A role mark opens from anywhere between the graph's edge and its node. The zone that opens HEAD, Upstream or Target stopped an avatar's width short of every node, and a merge's node is a small dot: most of the band beside it opened nothing. It reaches the node as it is drawn.
  • A commit subject with a | in it is read whole. The graph's page was read field by field on |, so fix: a | b pushed every field after it one place along — the author became half the subject, the date the author's address. The page is read on separators a message cannot hold, in one place for both products (the shared git core).
  • A stash's preview no longer says Invalid Date. Its header is a commit's, and a stash has neither an author nor a date: the header now shows only what it has. (#273)