feat(desktop): let the Git changes panel pick its comparison base branch - #5120
Conversation
1850701 to
6487c58
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
The picker holds up end to end — I traced the value from the Astryx trigger to the git command and found no correctness bug. One behavior I'd like a decision on, plus a few smaller things.
Behavior worth confirming
The resolved default is now persisted, so it stops following the repo. resolveAdoptedBaseBranch returns the backend's resolved branch when nothing is pinned (session-review-base-branch-model.ts:85-97) and the panel writes it to storage (session-review-panel.tsx:143-153). So a Session that never opens the picker pins a branch on its first read and keeps it for the life of that Session, across restarts. Before this PR every read re-resolved, so the panel tracked origin/HEAD. The blast radius is narrow — BASE_BRANCH_PRIORITY ranks refs/remotes/origin/HEAD first (git-review-main.ts:403-410) and that ref survives a rename of the default branch — but in a repo with no origin/HEAD the pin freezes on refs/heads/main and will not follow a later rename to trunk. If the goal is only "the trigger must name a real branch", the resolved snapshot.baseBranch could be displayed without being persisted (persist only an explicit pick) and the dynamic default would survive. Either way, the Summary should say the implicit default gets stored, since that is a behavior change beyond "the choice persists".
Findings
- The
invalid_base_branchrecovery path has no test. The clear-the-pin-and-retry-once branch (session-review-panel.tsx:123-135) is the headline answer to a pinned branch disappearing, butsession-review-panel-recovery.test.tsonly drives thegit_failedcase, and the model test stops atresolveAdoptedBaseBranch. A case wherereview.readreturnsinvalid_base_branchonce and then succeeds would cover it: request sequence['refs/heads/gh-pages', undefined], pin cleared in storage, no error banner. invalid_base_branchdropsbrancheseven though it is in scope.branchesis captured atgit-review-main.ts:67, but the rejection at:71-73returns without it, while the contract documents it as "available even when computing the selected branch diff fails" (packages/core/src/git-review.ts:66-67) —unborn_repositoryandgit_failedboth include it. Spreading it here too would make the contract uniform and let the panel keep the picker if the retry ever failed.copy.invalidBaseBranchlooks unreachable now. The panel always clears the pin and retries with no selection, and the backend only rejects a non-nullbaseBranch, so I could not construct a path where the banner atsession-review-panel.tsx:233renders in any of the three locales. Keeping it as a guard is fine; worth a line in the PR so nobody hunts for it.- nit — the picker offers the current branch as a comparison base. Options include it (the updated
git-review-main.test.tsfixture hasfeature/reviewas bothcurrentBranchand an option). Picking it diffs againstmerge-base(base, HEAD), i.e. HEAD, so the panel silently becomes "uncommitted changes" while the header still readsfeature → feature.resolveBaseBranchdeliberately skips the current branch (git-review-main.ts:431); marking that optiondisabledwould close the gap. - nit —
visibleFileCountis not reset when the base changes (session-review-panel.tsx:91,216), so a "show 20 more" position from the previous comparison carries into the new one and the button can appear or vanish. Pre-existing for Session switches, newly reachable through the picker. - nit — the buffer-overflow change is missing from the Summary and CHANGELOG.
runDiffAllowTruncated(git-review-main.ts:220-245) changes what a huge branch diff produces, and the PR answers "Yes — described under Summary above" to the behavior question. Related: the sibling--name-statuscall at:175is not wrapped, and a mid-file cut can leave a listed file with an empty or partial diff — acceptable degradation, but it belongs in the description. - nit — unrelated change: the PTY-output wait in
packages/runtime/src/__tests__/shell-run-manager.test.tsis unrelated to this panel; better as its own PR. - nit — copy conventions: the sibling ICU plural uses
{count, number}(packages/ui/src/astryx-copy.ts:150,commandPalette.resultCount), the newsearch.resultCountuses{count}. Also the comment abovesearch("Selector and MultiSelector render the same two search affordances") is stale —emptySearch/resultCountare Selector-only, MultiSelector has its own catalog keys. - nit —
options={[...props.baseBranchOptions]}(session-review-base-branch-picker.tsx:51) allocates per render and defeats Selector'sfilteredItemsmemo; auseMemoon the panel side would keep it stable. - nit — process: only the first commit carries
Generated-by: pi (…), whileCONTRIBUTING.mdasks for the trailer on each affected commit, kept through squash; andCONTRIBUTING.mdasks for before/after images on UI changes — the Storybook measurements are useful evidence, but nothing is attached. docs/superpowers/plans/2026-09-10-review-branch-refs.mdis the only file underdocs/superpowers/; tracked plans elsewhere live indocs/archive/ordocs/. Consider moving or dropping it.- Heads-up: merge state is
DIRTY(conflicts withmain); thetestcheck is green.
Verified, no issue found
Selector.onChangehands back the optionvaluein the non-clearable variant (@astryxdesign/core@0.5.2,Selector.d.ts), so what gets persisted is the canonical ref, not a label; the first read omitsbaseBranchentirely (reviewBaseBranchRequestValue).- Search filters on
option.label(Selector.js:260), so short branch names are searchable, and@astryx.selector.emptySearchResults/.resultCountdo exist in the Astryx catalog (locales/en.json:1270,1274), so the Chinese overrides land. - The
%(refname:short)bug is real: git reportsrefs/remotes/<remote>/HEADas a bare<remote>(refs/remotes/author/HEAD → authoron git 2.50 here), which the old.endsWith('/HEAD')filter let through. Enumerating%(refname)and stripping the namespace is the right fix. - Ordering cannot drift: one
BASE_BRANCH_PRIORITYlist feeds bothlistBaseBranches(:450) andresolveBaseBranch(:414), and the unranked/label tiebreak reads correctly. - Ambiguity is rejected rather than guessed: same-label local vs remote, tag vs branch, and tag-only names all fail closed (
resolveRequestedBaseBranch,:440), and the orphan-history and tag-named-maincases behave. - The CSS selectors are real (
themeProps('popover-surface'),themeProps('item')); the layer renders inline inside the wrapper unless its parent has an unsafe writing context, and the popover ispopover="auto"(top layer), sooverflow-y: autoon.maka-session-review-panelwill not clip it. - Contract blast radius is small:
baseBranchOptionsis only consumed by the desktop app, the IPC already validatedbaseBranch(runtime-host-workspace-ipc-main.ts:66-79), and both new test files sit undersrc/main/__tests__wheretest:dist(dist/main/**/*.test.js) picks them up.
8b51ef2 to
08c5a2c
Compare
3ef391c to
fa20b51
Compare
|
Could we have a before after comparison picture in the pr body? It would help clarify things easier. Thanks! |
Remove the composer footer's Git-branch chip and the read pipeline that fed it (#5487): the chip was that pipeline's only consumer, so nothing is left stranded. The branch is ambient session state the agent owns, not a parameter of the send, so the composer's control row is the wrong home for a read-only value there — and it was the one item on that row drawn as a hand-rolled span instead of an Astryx primitive. The workbar's Review face names the current branch once #5120 lands; this lands first so the two never show the same fact twice. Migration: none. Between this merge and #5120's there is no surface naming the branch — a deliberate gap over duplicating the readout. #5120 needs a trivial rebase on git-review-main.ts and packages/core/src/git-review.ts to drop the readGitBranch / GitBranchReadResult it inherited from #5487. Refs #2171, #5487 Generated-by: Maka
5942af5 to
abce327
Compare
|
@Astro-Han done |
dcc385e to
f6ffba1
Compare
|
@Astro-Han Thanks for the detailed review. Most items are addressed, including keeping implicit defaults dynamic. A few behavior, documentation, and process points remain open:
|
Astro-Han
left a comment
There was a problem hiding this comment.
Verified against head f6ffba1. My earlier behavior question stands resolved in shape — the picker is correct end to end: renderer strings reach merge-base/diff only after matching an enumerated for-each-ref entry (short-name shadowing of tags is now impossible — full refs fix a real latent bug), three-dot semantics via merge-base SHA pin both reads so a moving base can't skew them, invalid_base_branch → unpin → single bounded retry, picker stays alive on every failure that carries branch context, and it never blocks the panel (stale diff dims instead of skeletoning).
A simplification pass found the +1073 is mostly honest (the IPC plumbing already existed on main — this PR activates it), but ~135 lines defend a false premise:
P3 — the 'legacy display name' canonicalization chain is dead machinery (~100 lines incl. tests). git-review-main.ts:436-437 comments 'Old preferences contain display names', but no shipped version ever persisted a base-branch choice — the -v1 storage key is new in this PR and every producer sends option.value (a qualified ref). Once the label-matching arm dies, resolveAdoptedBaseBranch becomes a provable no-op (an ok snapshot implies exact option-value match, so adopted === current always). Cut: label matching in resolveRequestedBaseBranch, resolveAdoptedBaseBranch + its call site + testing.ts re-export, reviewBaseBranchRequestValue, and the migration/label tests. The invalid→unpin→retry path already covers a genuinely disappeared branch.
P3 — duplicated storage guards on a false premise. session-review-base-branch-model.ts:24-41 re-implements safeLocalStorageGet/Set; the comment claims an architecture check forbids the import, but siblings in the same feature already import browser-storage (model/workbar-layout.ts:23) — the ban is scoped to overlays. Delete lines 24-41, import the helpers.
P3 — session-switch reset effect is dead. panel.tsx:102-111 — SessionReviewPanel mounts with key={sessionId} (workbar-surface.tsx:521), so props.sessionId never changes in place; the useState initializer already reads storage.
P3 — stale picker options when the repo disappears. panel.tsx:140-146 — failures without branch context skip setBranches, leaving options of a gone repo rendered; setBranches(nextBranches ?? null).
Two non-blocking observations: session-keyed localStorage never prunes (dead sessions leave entries — trivial footprint, worth a hook if session-deletion has one); no story/e2e exercises the picker, so the new CSS block (review.css:28-68) has zero browser-tier verification — the label/value machinery is well tested but the popover layout isn't.
Decision for you, not a blocker: persistence scope is per-session — a new session on the same repo re-falls to the default. Per-repo persistence or no persistence (in-memory only, deletes the whole model file) are both defensible; the missing policy is whether a comparison base should outlive its session.
|
@Astro-Han Thanks for you patience review.All four P3 issues have been fixed in 1b46b24. Of the two non-blocking observations, Storybook coverage has been added; cleanup of preferences for deleted sessions remains deferred.
|
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed this current head independently. The 27-file diff adds a per-session Git Review base-branch picker and persistence, uses fully qualified branch refs in main-process Git commands, retains branch options on comparison failure, and degrades oversized diff output to a marked truncated result. I inspected selection/reload generation fencing, invalid-branch recovery, ref validation, and the Git IPC/result paths; no substantiated P0–P3 issue emerged. Node 24 install/build and 22 focused Git Review/picker/recovery tests pass; current-head CI test is green. This branch currently conflicts with main in apps/desktop/stories/workhub.stories.tsx and docs/astryx-surface-file-inventory.md; resolve and revalidate the combined result before merge. I did not run a real Electron interaction, large-repository performance, or cross-platform Git fault injection.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
Let each session select a comparison branch by canonical ref, with searchable localized options and persisted explicit choices. Keep implicit defaults dynamic, recover automatically when a saved branch disappears, and reset pagination when the comparison changes. Preserve branch options when diff computation fails and show truncated output when a large unified diff exceeds the process buffer. Add recovery and ref-resolution coverage, switch feedback, stories, and surface inventory documentation. Generated-by: pi (DeepSeek V4.1 Flash) Generated-by: OpenAI Codex Generated-by: MyFlicker Co-Authored-By: Claude <noreply@anthropic.com>
Generated-by: Codex
Regenerate the file-level inventory after 0f11ae7 added a Text usage to the review base branch picker without regenerating. The markdown row had drifted from the generator; the .paths list was already in sync. Keeps the astryx:surface-inventory coverage gate green. Generated-by: Maka
Move review comparison persistence behind the workbar preference port and share desktop storage guards. Keep implicit defaults unpinned, recover invalid saved refs, and require qualified branch refs. Generated-by: OpenAI Codex
The architecture ratchet forbids a rootDebt entry gaining a dependency edge into the platform zone, so keep the storage guard implementation in browser-storage.ts and let the platform adapter carry its own copy. Generated-by: OpenAI Codex
6c576f3 to
e45f1c8
Compare
|
@hqhq1025 Thanks for you review, conflicts are resolved now |
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed the current head (e45f1c8c). The PR adds a per-session Git Review base-branch picker. After rebasing onto current main, it keeps explicit qualified refs in the Workbar preference service; an unpinned session follows the repository's current default, and an unavailable saved ref is cleared before one retry (apps/desktop/src/renderer/features/workbar/tools/review/session-review-panel.tsx:87-132, apps/desktop/src/renderer/platform/desktop/review-base-branch-preferences.ts:26-49). The Main process accepts only an enumerated qualified ref for merge-base (apps/desktop/src/main/git-review-main.ts:63-83). The previous conflicts are resolved. I found no substantiated P0–P3 on this head.
On Node 24, npm ci, build:test, 22 focused Git Review/preference/panel tests, Desktop typecheck, renderer architecture check, and git diff --check passed. A merge-tree against current main is clean. The current-head CI test was still running when I reviewed it, so its result is not yet verified. I did not run packaged Electron interactions, large-repository performance, or cross-platform Git fault injection. No schema or migration changes.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
Astro-Han
left a comment
There was a problem hiding this comment.
Final multi-reviewer pass on e45f1c8c: no blocking issues (no P0–P2). Four minor (P3) notes below; none of them needs to hold the merge.
This round was reviewed independently by several automated reviewers built on different model families, each reading the full diff at this exact head without seeing the others' conclusions. They converged on the same result.
Correctness — checked and holding
- Ref validation / injection: the requested base is only accepted when it exactly equals a fully qualified ref enumerated by
for-each-ref refs/heads refs/remotes(git-review-main.ts:67-76); short names, tags andrefs/tags/*are rejected asinvalid_base_branch, and git is invoked viaexecFilewith argument arrays, so no unvalidated string reaches git or a shell. - Stale pin recovery: an
invalid_base_branchwith a pin clears the pin (state, ref, storage) and re-reads once with no selection; the retry cannot be rejected again, so it cannot loop. - Per-Session persistence and switching: the panel is keyed by
sessionIdinworkbar-surface.tsx,revisionRefis re-checked after every await, and there is no await between the revision check and the pin-clearing write, so a superseded load cannot clear a newer pin. - Storage failures: reads degrade to "unpinned", writes are best-effort, matching the port contract in
ports.ts.
Is the diff size necessary?
Yes, broadly. Of +1113/−62 across 22 files, roughly 600 lines are tests, ~400 are the feature (qualified-ref enumeration and validation, the picker, panel selection/recovery, the persistence port and Desktop adapter, core types, copy), and ~65–120 are CSS, stories, README, CHANGELOG and the generated surface inventory. No reviewer found unrelated changes, and none found a materially smaller implementation that keeps both "qualified refs, no tag/short-name ambiguity" and "only explicit choices are pinned". Two optional trims:
runDiffAllowTruncated/maxBufferStdout(~40 lines + one test) is a robustness fix that could be its own PR, though selecting a far-diverged base is exactly what makes the buffer overflow reachable, so keeping it here is defensible.platform/desktop/browser-storage.tsis byte-identical to the existingrenderer/browser-storage.ts(copied because the architecture ratchet forbids the platform zone depending on that root file). The two small try/catch helpers could be inlined intoreview-base-branch-preferences.tsto drop the extra file.
P3 notes
- Local and remote branches can render with the same label — see inline comment. (Found independently by three reviewers.)
- PR description is out of date: it says unique legacy short names migrate to canonical refs, but there is no migration code — a short name fails validation and is cleared as a stale pin (harmless in practice, since nothing persisted a short name before this PR). It also says the picker menu is capped at 180px wide, while
review.cssand the story assert 280px, and it mentions a PTY test stabilisation that is not in this diff. - Base can now be the current branch: dropping the old
candidate === currentBranchskip means a local-only repository onmainresolves its default base torefs/heads/mainand the header readsmain → main. The diff content is correct (it degrades to working-tree changes, and there is a test for it) and the description says this is intentional; flagging only because it reads oddly in the UI. - Persistence lifecycle and race coverage: the per-Session map in
review-base-branch-preferences.tsis never pruned when Sessions are deleted, and if storage quota is exhausted the write is silently dropped (choice works until restart). Separately, the switch/late-response protection above is structurally correct but no test drives "earlier read resolves after a later one"; a test with two pending reads resolved out of order would pin it.
Verification and limits
Live head re-checked before posting: e45f1c8c. The test check is green at this head, and the head merges cleanly with current main. Not run by this review: a packaged Electron build, real keyboard/long-name visual checks, or cross-platform git failure injection.
Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.
| branches.push({ label: value.slice('refs/heads/'.length), value }); | ||
| } else if (value.startsWith('refs/remotes/') && | ||
| (value === 'refs/remotes/origin/HEAD' || !value.endsWith('/HEAD'))) { | ||
| branches.push({ label: value.slice('refs/remotes/'.length), value }); |
There was a problem hiding this comment.
P3: a local branch literally named origin/release (refs/heads/origin/release) and the remote refs/remotes/origin/release both get the label origin/release here (line 449 and this line). The values stay distinct, so the comparison itself is right, but the picker renders only option.label (session-review-base-branch-picker.tsx:52-66), so the two entries — and the selected trigger — look identical. git-review-main.test.ts:160-175 already constructs this case on the main-process side. Suggest disambiguating only on collision (e.g. append "local"/"remote" or show the full ref) and adding a UI regression.
Astro-Han
left a comment
There was a problem hiding this comment.
Approved at @Astro-Han's explicit request after reviewing the findings above. The P3 notes are non-blocking follow-ups.
Watermark bfb315a. Done: apache#5573/apache#5600/apache#5601, apache#5521, apache#4875, apache#5723, apache#5738, apache#5742. Not applicable: apache#5737, apache#5593. Deferred: apache#5730. Consider: apache#5599, apache#5120, apache#5693. Diverged: apache#5740. Skipped: ACP, WorkHub, upstream renderer and packages/ui, one refactor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
The Changes panel previously compared only against the backend-resolved base branch, so stacked work could include inherited changes with no way to choose another comparison base. This PR adds a searchable base-branch picker beside the current branch (
current → base).baseBranchand follow the repository default; displaying the resolved default does not pin it. If a saved branch becomes invalid, the panel clears the saved selection and retries once with the dynamic default.{ label, value }: readable names in the UI, fully qualifiedrefs/heads/...orrefs/remotes/...values for comparisons and storage. Unique legacy names migrate to canonical refs; ambiguous names and tag-only refs are rejected. Local and remote branches with identical labels remain distinct.The backend enumerates full refs instead of relying on
%(refname:short), keepsorigin/HEADselectable, and shares the preferred-branch ordering with default resolution. The remote HEAD's resolved target takes precedence; the current branch can also be the base, with staged and unstaged changes still included.Fixes #5119
Before:

After:

Verification
Checked against PR head
abce32742c4c384acbb87842ade7746bf4f38296on 2026-09-20:npm --workspace @maka/desktop run build:main— passed.node --test --test-force-exit apps/desktop/dist/main/__tests__/git-review-main.test.js apps/desktop/dist/main/__tests__/session-review-base-branch.test.js apps/desktop/dist/main/__tests__/session-review-panel-recovery.test.js— 19/19 passed. Covers full-ref and legacy-name resolution, tag/name collisions, default ordering, per-Session persistence, corrupt storage, recovery after invalid selections or failed diffs, switch feedback, and output-buffer truncation.Earlier verification recorded in this PR (not revalidated against the current head): desktop typecheck, architecture checks, Biome and renderer build passed; desktop tests reported 2461/2462 with a
settings-ipc-helpersTavily failure. Storybook checks covered filtering, localized search feedback, long-name ellipsis, and updating/closing the picker after selection. The screenshots above are retained from that earlier verification.Review focus
{ label, value }and can include branch context in failure results. Legacy saved names are accepted only when they identify one enumerated branch.READYline and cursor position before taking the PID-observation baseline; it does not change runtime production behavior.AI use
Select exactly one:
Tool(s) and scope: pi (DeepSeek V4.1 Flash) — implementation and earlier local verification. Codex — PR description refresh and the focused build/test verification listed above.
Checklist
Does this PR entail a change in behavior?