Skip to content

feat(desktop,host-service): add a commit graph to the workspace sidebar - #6257

Open
acseven wants to merge 7 commits into
ParadevAI:mainfrom
acseven:feat/git-graph-sidebar
Open

acseven wants to merge 7 commits into
ParadevAI:mainfrom
acseven:feat/git-graph-sidebar

Conversation

@acseven

@acseven acseven commented Aug 7, 2026 •

Copy link
Copy Markdown

What & why

Adds a fourth Graph tab to the v2 workspace sidebar: a lane-based commit graph whose primary value is ref classification, not the lanes. The lanes are the frame; what the tab is actually for is surfacing branches and worktrees that exist in git but have no open workspace behind them.

Commit history was only reachable as a flat base..HEAD dropdown — no topology, and no view at all of the branches and worktrees that accumulate outside the currently-open workspaces. A branch whose worktree was deleted, or a worktree with no workspace row, was invisible until it caused a problem.

Read-only. No write actions (checkout, cherry-pick, revert, prune) in this PR.

Relates to #4935 and #5594 (#3849 folded in).

Host service — git.listGraph

  • New procedure alongside listCommits, which is untouched (desktop and mobile both consume its output type). getCommitFiles is the one existing procedure that changed: it now also returns the commit's own metadata (subject, body, author, date, parents), so a diff pane opened from the graph renders its header without a second spawn.

  • Tip set is seeded from git's own refs, never from workspace records. refScope picks how wide it goes, and all five values are served:

    Scope Tips Reads as
    head HEAD this workspace's own history
    open-workspaces + base ref, worktree branches, detached worktree heads branches open in other Superset workspaces
    local (default) + every local branch, the current branch's upstream everything on this machine
    remote HEAD + --remotes what the last fetch saw on the server
    all --exclude=refs/stash --all every ref, tags included

    remote and all pass one flag rather than enumerating refnames, so a tag-heavy repo can't overflow the command line. Detached worktree heads are still named explicitly — they aren't refs. Under open-workspaces, a worktree git marks prunable is excluded: its directory is gone, so it is not an open workspace.

  • Ref state is classified per local branch: open · detached-worktree · prunable (+ reason) · merged · orphan-branch. Containment comes from git branch --merged <baseRef>, not from graph topology, so a branch merged far outside the window still reports merged. prunable comes from git's own porcelain flag (gitdir file points to non-existent location) rather than a per-branch existsSync, so classification costs no synchronous fs calls on the host event loop.

  • Every git spawn happens in the worker (gitGraphLogTask), including base resolution, so both stdout draining and parsing stay off the host event loop. Decorations are built from for-each-ref and resolved through ResolvedRef — never from %D, never from a shortname prefix.

  • Overlapping traversals coalesce, so a git:changed storm during a rebase doesn't fan out into duplicate walks.

  • The pagination cursor is tagged with the scope it was issued for. --skip counts rows of one traversal, so replaying a local offset against head would window a list that never had those rows; a cursor from another scope restarts at page one instead of serving a silently wrong page. Scope is also part of the coalescing dedupe key, so two scopes never share one walk.

Renderer

  • assignLanes — pure, tested, no React or tRPC. Active-lane sweep over topo-ordered input; lane identity is pinned to the originating branch so a refetch doesn't reshuffle the graph.
  • Virtualized list, per-row inline SVG (not one tall canvas), so virtualization stays trivial. Row height is derived from the commit's own refs — no measurement pass — and the size cache is keyed by commit hash so it survives refs moving underneath an open tab.
  • Lane colours are new additive --graph-lane-1..8 tokens in both themes, equal chroma/lightness so the lanes read as one family and survive a theme switch. No component hardcodes a colour; the live values come from shared/themes/graph-lanes through applyGraphLaneColors, and the hex in globals.css is only the pre-hydration fallback set.
  • Ref badges encode state in texture as well as colour — fill, dashes, strike — so they still read at 260px and for a colourblind viewer, leaving hue free for lane identity.
  • Row tint in the lane's colour makes lane membership readable without tracing lines, and survives the lane compression narrow widths force.
  • Commit clicks open the existing diff pane kind; the graph owns its own persisted selection, separate from the Changes tab's filter, so the two surfaces no longer fight over one piece of state. Shift-click ranges, cmd/ctrl-click forces a new pane, double-click pins — matching the file tree's convention exactly.
  • Read-only row context menu (copy hash / subject / open in new tab).
  • The tab's own control strip carries a scope chooser and two toggles. The chooser sits left of them and reads This workspace · Open workspaces · All local branches · Remote · Everything — plain wording rather than git's, since the audience is people who don't think in refs. The toggles are ref badges on their own untruncated line, and dimming of commits that carry no unreferenced ref. The strip renders even when the window is empty, so a scope that returns nothing can't take away the control that would let you leave it.

Notable decisions

The "unreferenced" filter dims, it does not remove. Lanes and edges are assigned across the whole commit list; dropping rows would leave the surviving edges pointing at rows that aren't there, and filtering before lane assignment yields a graph of disconnected nodes.

Selected rows and range-interior rows share a tint alpha. Selection also carries --accent; the range interior doesn't. Same alpha therefore keeps an endpoint strictly brighter than the band it anchors, in both themes, with no per-theme numbers.

The row size cache is keyed by commit hash, not by list index. Refs move while the tab is open, so a refetch that prepends a commit shifts every index — under the virtualizer's default index key a row inherits the cached height of whatever previously sat in its slot, which in two-line mode misfits by a whole badge line and shows up as overlapping rows or a blank band. Keying by hash lets a cached size travel with its commit; rows is also in the remeasure deps for the case the key can't catch, a ref landing on a commit whose hash doesn't change. Only reproduces on a repo with live churn.

"Open workspaces" is read off git worktree list, not the workspaces table. The worker has no DB access, and every Superset workspace is a worktree, so the worktree list already answers the question. The known ceiling: a worktree created outside Superset counts as an open workspace. Joining against DB rows in the coordinator is the upgrade path if that ever matters.

The header toggles read through a live query, not .get(). They write to v2SidebarProjects, and .get() is a non-reactive snapshot — the row changed in localStorage while the header kept rendering the stale flag until some unrelated re-render happened to run, which reads as a multi-second lag on a write that already completed. No spinner: the write is synchronous and local, so a pending indicator would animate over an already-finished state change. The toggles also ensureProjectInSidebar first, matching useV2ProjectDefaultApp — a project with no sidebar row yet would otherwise swallow the write entirely.

The strip's controls are not in SidebarHeader's actions slot. That slot is a shrink-0 island in a fixed 40px row whose four tab buttons are each flex-1 with no min-w-0; anything rendered there clips the last tab label instead of shrinking it. The chooser and toggles live on the graph's own control strip instead.

How I tested it

Gate Result
bun run lint 0 (5832 files)
bun run typecheck passes except @superset/ui, which fails on a duplicate @types/hast resolution in node_modules. No packages/ui file, package.json, or bun.lock is in this branch's diff, so it is not from here
bun run test 14/15 packages green
apps/desktop tests 2617 pass, 1 fail — getProcessEnvWithShellPath, environment-dependent and untouched by this branch
packages/host-service tests 998 pass, 0 fail, 14 skip

refScope has its own integration test: a branch with no worktree is absent under head and open-workspaces but present under local and all; a worktree checked out on that branch whose directory is then deleted stays excluded from open-workspaces, which fails without the prunable filter; and remote excludes the local-only branch until refs/remotes/origin/side exists, then includes it. Cursor handling is covered both ways — a local cursor pages forward, and the same cursor handed to head restarts at page one instead of windowing rows that traversal never produced. buildTipSet is unit-tested per scope, including that all and remote emit a flag rather than a refname list.

Integration test covers all five ref states against a fixture repo (open workspace, worktree with no workspace row, orphan branch, prunable worktree with its path deleted) plus a merge commit, a tag, and a branch merged outside the commit window. Lane assignment is unit-tested for linear / merge / octopus / criss-cross / root / lane reuse / parent outside the window, including determinism when commits are appended.

Rendering was checked against the running app at 260px and 400px sidebar widths, in both toggle states, with the measurements alongside the screenshots (total virtual size, per-row height, badge clipping). The toggle-reactivity fix was verified end-to-end on the running app.

The row-height regression is pinned by a test that prepends a commit to a mixed-height list and asserts every commit present in both lists keeps the same (key, size) pair, looked up by hash rather than by slot. It was confirmed red against the pre-fix behaviour: restoring the index key fails it. The size and key derivation was extracted into a pure graphRowSizer to make that testable — this repo has no jsdom or testing-library, and adding one for two callbacks wasn't worth the dependency.

It was also verified live: with the Graph tab open on a repo of ~1600 commits, a commit landing on HEAD prepended a two-line row and shifted every index by one. Row offsets stayed consistent with rendered heights across the shift and across the subsequent removal — 0 mismatches in both directions, where the pre-fix build produced an overlap at the slot that grew and a blank band at the slot that shrank.

Screenshots

Captured from the dev app over CDP (RENDERER_REMOTE_DEBUG_PORT=9222 bun dev, renderer matched to this workspace's DESKTOP_VITE_PORT), driving the real controls with dispatched mouse input. The clip is the sidebar column only; the rest of the window is unchanged by this PR. The branches below main are a throwaway topology built for these shots and deleted afterwards — the repo's own history is a straight line, which shows none of the cases the tab exists for.

Inline refs (default) — four concurrent lanes, two merges and an octopus. The fork elbow leaves the node horizontally then bends down; the merge descends then bends in, so the two read apart at a 3.5px node. Row tint carries lane membership without tracing the line. The dashed, struck-through red badge is a branch whose worktree directory is gone (prunable); badges truncate to fit 260px.

Commit graph with four lanes, merge and fork elbows, a tag badge, HEAD and main, and a struck-through prunable branch badge

Scope chooser — the five scopes as the user reads them, All local branches selected. Plain wording rather than git's, because the audience is people who don't think in refs.

Graph tab with the scope menu open, showing This workspace / Open workspaces / All local branches / Remote / Everything

Refs on their own line (toggle on) — same commits, badges move above the row they decorate and stop truncating: origin/main and the long branch names read in full. That is the trade the toggle exists for, and the reason row height has to come from the commit's own refs.

Same graph with ref badges on their own line above each commit, showing full branch and tag names

Highlight unreferenced (toggle on) — the point of the tab. Rows carrying a branch or worktree with no open workspace behind it stay bright; everything already accounted for dims. Nothing is removed, so the lanes and edges still connect.

Same graph dimmed except the rows carrying unreferenced branches, which stay at full contrast

Checklist

  • PR title follows conventional commits (type(scope): subject)
  • bun run lint passes with 0 output (CI fails on warnings too)
  • bun run typecheck — green for every package this PR touches; @superset/ui fails on a pre-existing duplicate @types/hast resolution, see the table above
  • "Allow edits from maintainers" is checked on fork PRs

Known gaps

  • Pagination is best-effort within a scope: the cursor is a --skip offset, so pages re-anchor if refs move between fetches. Crossing scopes is handled (the cursor is dropped), moving refs are not. The truncation banner surfaces when a repo has more history than one page.
  • Everything is now largely All local branches ∪ Remote ∪ tags, so it may not earn its slot. Left in for one release to see whether anyone picks it.
  • remote reflects the last fetch. Nothing in this PR fetches, so a stale remote reads as a stale graph.
  • All local branches is the longest label and squeezes the "Showing first N of M" text beside it at the narrowest sidebar widths.
  • Light theme is covered by unit tests on the pure colour helpers; the store's delivery path was verified in dark only.

Summary by CodeRabbit

  • New Features
    • Added a Git Graph tab to the workspace sidebar with a visual, lane-based commit history.
    • Added reference-scope filtering, pagination, two-line badges, and unreferenced-reference highlighting.
    • Added commit and range selection, diff previews, pane pinning, context-menu actions, and commit metadata.
    • Added responsive layouts and loading, empty, error, and truncated-history states.
    • Added theme-aware lane colors and indicators for branches, tags, HEAD, worktrees, merged, and prunable references.

acseven added 3 commits August 7, 2026 13:37
A new read-only procedure alongside listCommits, which is untouched. It
returns a topologically ordered commit window plus per-ref decorations, so
the renderer can draw lanes without spawning git itself.

- Tips are seeded from git's own refs, never from workspace records.
  `refScope` picks the breadth: head, open-workspaces, local, remote, all.
  `remote` and `all` pass one flag (--remotes, --exclude=refs/stash --all)
  rather than enumerating refnames, so a tag-heavy repo cannot overflow the
  command line. Detached worktree heads are still named explicitly — they
  are not refs.
- Local branches are classified open / detached-worktree / prunable (+
  reason) / merged / orphan-branch. Containment comes from `git branch
  --merged <baseRef>`, not from graph topology, so a branch merged far
  outside the window still reports merged. `prunable` comes from git's own
  porcelain flag, so classification costs no synchronous fs calls on the
  host event loop.
- "Open workspaces" is read off `git worktree list`, not the workspaces
  table: the worker has no DB access and every Superset workspace is a
  worktree. Worktrees git marks prunable are excluded.
- Every git spawn happens in the worker task, including base resolution, so
  stdout draining and parsing stay off the host event loop. Overlapping
  traversals coalesce, keyed by workspace and scope, so a git:changed storm
  during a rebase does not fan out into duplicate walks.
- Cursors carry the scope they were issued for. --skip counts rows of one
  traversal, so a foreign cursor restarts at page one rather than serving a
  window the traversal never produced.
- getCommitFiles now also returns the commit's own metadata, so a diff pane
  opened from the graph renders its header without a second spawn.
A fourth sidebar tab whose primary value is ref classification, not the
lanes. Commit history was only reachable as a flat base..HEAD dropdown, with
no view of the branches and worktrees that accumulate outside the open
workspaces — a branch whose worktree was deleted was invisible until it
caused a problem. Read-only: no checkout, cherry-pick, revert or prune.

- assignLanes is pure and tested, no React or tRPC. An active-lane sweep
  over topo-ordered input, with lane identity pinned to the originating
  branch so a refetch does not reshuffle the graph.
- Virtualized list with per-row inline SVG rather than one tall canvas.
  Row height derives from the commit's own refs, no measurement pass, and
  the size cache is keyed by commit hash so a cached size travels with its
  commit when a refetch shifts every index.
- Lane colours are additive --graph-lane-1..8 tokens in both themes at equal
  chroma and lightness. No component hardcodes a colour; the hex in
  globals.css is only the pre-hydration fallback.
- Ref badges encode state in texture as well as colour — fill, dashes,
  strike — so they still read at 260px and for a colourblind viewer,
  leaving hue free for lane identity.
- Commit clicks open the existing diff pane kind. The graph owns its own
  persisted selection, separate from the Changes tab's filter, so the two
  surfaces no longer fight over one piece of state. Shift-click ranges,
  cmd/ctrl-click forces a new pane, double-click pins.
- The control strip carries a scope chooser (This workspace / Open
  workspaces / All local branches / Remote / Everything) and two toggles:
  ref badges on their own untruncated line, and dimming of commits carrying
  no unreferenced ref. It renders even when the window is empty, so a scope
  returning nothing cannot take away the control that would let you leave
  it. The strip is not SidebarHeader's actions slot — that slot clips the
  last tab label instead of shrinking it.
- The strip reads through a live query, not .get(): .get() is a
  non-reactive snapshot, which showed as a multi-second lag on a write that
  had already completed.
Both documents are shipped, so they go straight to plans/done/ per the
plan-placement rule. The follow-up doc records the verification results the
implementation was checked against, including the one-off large-repo bench
whose script was removed before merge.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b5d4c5f8-fc92-45a6-b60c-7201d2603ac2

📥 Commits

Reviewing files that changed from the base of the PR and between 7837067 and 7d31854.

📒 Files selected for processing (2)
  • .gitignore
  • packages/host-service/src/trpc/router/git/git.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/host-service/src/trpc/router/git/git.ts

📝 Walkthrough

Walkthrough

Adds a read-only Git graph sidebar with scoped pagination, branch and worktree states, lane-based rendering, responsive layouts, persisted controls, commit selection, and graph-owned diff panes with commit metadata.

Changes

Git graph data and traversal

Layer / File(s) Summary
Graph data contracts and Git traversal
packages/host-service/src/trpc/router/git/..., packages/host-service/src/workers/tasks/git.ts, packages/host-service/test/integration/*
Adds graph types, ref parsing, scope-based tip selection, worktree mapping, branch-state classification, gitGraphLogTask, and git.listGraph pagination. Commit-file responses now include metadata.

Graph rendering and interaction

Layer / File(s) Summary
Lane assignment and graph row rendering
apps/desktop/src/renderer/routes/.../useGraphTab/...
Adds lane assignment for merges, forks, roots, and stubs. Adds SVG lanes, commit nodes, reference badges, virtualized rows, selection, range highlighting, context menus, and graph header controls.
Theme colors and responsive graph layout
apps/desktop/src/shared/themes/*, apps/desktop/src/renderer/stores/theme/*, apps/desktop/src/renderer/globals.css, apps/desktop/plans/*visual-spec.md
Adds eight theme-derived lane colors, CSS fallbacks, responsive lane caps, row geometry, two-line references, and visual component specifications.

Sidebar and diff integration

Layer / File(s) Summary
Sidebar tab, settings, and graph state
apps/desktop/src/renderer/routes/.../WorkspaceSidebar/*, apps/desktop/src/renderer/routes/.../schema.ts
Adds the Graph sidebar tab, persisted graph scope and display settings, graph-owned commit/range selection, Git-change invalidation, and responsive sidebar inputs.
Commit previews and diff-pane integration
apps/desktop/src/renderer/routes/.../usePaneRegistry/*, apps/desktop/src/renderer/routes/.../useWorkspacePaneOpeners/*, apps/desktop/src/renderer/routes/.../page.tsx
Adds graph-owned diff references, pane reuse and pinning, contextual pane titles and icons, and a commit metadata header.

Implementation and verification records

Layer / File(s) Summary
Implementation and verification records
plans/done/*commit-graph*.md, apps/desktop/plans/*visual-spec.md, .gitignore
Records the implementation plan, visual specification, follow-up behavior, verification coverage, deferred graph features, and Bun build-artifact exclusion.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: kitenite

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses conventional commit format and clearly summarizes the main change: adding a commit graph to the workspace sidebar.
Description check ✅ Passed The description is detailed and covers the change rationale, testing results, checklist status, screenshots, and known gaps.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@acseven

acseven commented Aug 7, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/DiffPane.tsx (1)

315-320: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Render commit metadata for empty changesets.

Lines 315-320 return before line 337 renders CommitMetadataHeader. An empty commit or a range with no file changes therefore loses its commit identity and displays only “No changes”.

Keep the metadata header outside the empty-files return. Render the empty state below it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/DiffPane/DiffPane.tsx
around lines 315 - 320, Update the DiffPane empty-files branch so
CommitMetadataHeader still renders when files.length is zero. Move the
empty-state return below the metadata header, preserving the existing
loading/“No changes” message while ensuring empty commits and unchanged ranges
retain their commit identity.
🧹 Nitpick comments (11)
packages/host-service/src/workers/tasks/git.ts (1)

102-120: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Align the field-count guard with the 7-field format.

The format string emits 7 tab-separated fields. The guard rejects fewer than 6. With exactly 6 fields the parser returns a record whose message is empty and whose date may hold body text. Use < 7 so a truncated line is rejected instead of producing a partly wrong record.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/workers/tasks/git.ts` around lines 102 - 120,
Update the field-count guard in parseCommitMetadata to reject any input with
fewer than 7 tab-separated fields, preserving parsing only for the complete
seven-field format.
packages/host-service/test/integration/git-graph.integration.test.ts (1)

196-205: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Preserve inherited environment when using git.env().

simple-git object form replaces the child-process environment, so these later git.commit() calls lose PATH and any identity configuration from the scenario. Set per-commit date options or spread process.env into .env() instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/test/integration/git-graph.integration.test.ts` around
lines 196 - 205, Update the commit setup around the git.env calls for m2 and m3
(backdated) so each commit preserves the inherited process environment, either
by spreading process.env into the environment object or by passing the dates as
per-commit options. Keep the distinct author and committer dates for both
commits unchanged.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/RefBadge/RefBadge.tsx (1)

8-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type laneColor as GraphLaneColor.

The doc comment states the range is 1-8, but the type is number. Lines 74, 75 and 87 interpolate the value into --graph-lane-${laneColor}. An out-of-range value resolves to an undefined CSS variable and the badge loses its tint with no error. GraphLaneColor is already exported from ../../types, and the caller passes row.color, which already has that type.

♻️ Proposed fix
-import type { GraphRef, GraphRefState } from "../../types";
+import type { GraphLaneColor, GraphRef, GraphRefState } from "../../types";
@@
 	/** 1-8; only `open` badges tint themselves with their lane's colour. */
-	laneColor: number;
+	laneColor: GraphLaneColor;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/RefBadge/RefBadge.tsx
around lines 8 - 9, Update the RefBadgeProps laneColor property to use the
exported GraphLaneColor type from ../../types instead of number, preserving the
documented 1–8 constraint and existing CSS variable interpolation.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/assignLanes.test.ts (1)

11-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

full does not produce a 40-character hash.

FULL holds 34 zeros. For a one-character label, full("a") returns 35 characters, so the comment at Line 13 is wrong. The tests still pass, because present.has and findRow compare the same padded strings. The fragile part is findRow at Line 79, which matches by startsWith: two labels where one is a prefix of the other (for example "m" and "m1") would resolve to the wrong row. padEnd removes both problems.

♻️ Proposed fix
-const FULL = "0000000000000000000000000000000000";
-
 /** Pads a short label into a 40-char hash so parent refs match commit hashes. */
-const full = (h: string): string => `${h}${FULL}`.slice(0, 40);
+const full = (h: string): string => h.padEnd(40, "0").slice(0, 40);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/assignLanes.test.ts
around lines 11 - 14, Update the full helper to use padEnd(40, "0") so every
label is exactly 40 characters, replacing the current FULL-based concatenation;
preserve the existing label-to-hash behavior used by present and findRow.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/GraphLanes.test.tsx (2)

135-142: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive the lane x and the viewBox width from graphGeometry.

The literals 7 and 18 restate geo.pitch / 2 and 1 * geo.pitch + 4. The file already imports graphGeometry. If the pitch changes, these assertions fail for a reason unrelated to the behavior under test.

♻️ Proposed fix
+		const x0 = geo.pitch / 2;
-		expect(incoming).toContain(`d="M7,0 V${yc}"`);
+		expect(incoming).toContain(`d="M${x0},0 V${yc}"`);

Also applies to: 180-187

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/GraphLanes.test.tsx
around lines 135 - 142, Update the affected GraphLanes test assertions around
the incoming and outgoing path checks, including the similar assertions near the
referenced second range, to derive the lane x-coordinate and viewBox width from
the imported graphGeometry values instead of hardcoded 7 and 18 literals.
Preserve the existing path expectations while making them adapt when geometry
pitch changes.

87-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The merge assertion does not test the fill.

Both node branches render exactly one <circle>, so the count at Line 90 and Line 93 does not separate a donut from a filled node. The lane-color check at Line 94 also passes for the donut, because the donut sets stroke to the same variable. Assert the distinguishing attributes instead.

♻️ Proposed stronger assertions
-		const plain = render(row({ isMerge: false, isRoot: false }));
-		expect(count(plain, "<circle")).toBe(1);
-		// Merge: filled node.
-		const merge = render(row({ isMerge: true }));
-		expect(count(merge, "<circle")).toBe(1);
-		expect(merge).toContain("var(--graph-lane-1)");
+		const plain = render(row({ isMerge: false, isRoot: false }));
+		expect(count(plain, "<circle")).toBe(1);
+		expect(plain).toContain('stroke="var(--graph-lane-1)"');
+		expect(plain).toContain("fill-sidebar");
+		// Merge: filled node, no ring.
+		const merge = render(row({ isMerge: true }));
+		expect(count(merge, "<circle")).toBe(1);
+		expect(merge).toContain('fill="var(--graph-lane-1)"');
+		expect(merge).not.toContain("fill-sidebar");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/GraphLanes.test.tsx
around lines 87 - 98, Update the merge-case assertions in the test “renders a
donut node for a plain commit, a filled node for merges/roots” to verify
distinguishing SVG attributes rather than only circle count or lane color.
Assert that the plain commit uses the lane color as stroke without a separate
fill, while the merge uses the lane color as fill and does not rely on the
shared stroke assertion; keep the root circle-count behavior unchanged.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.tsx (2)

124-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose the selected state to assistive technology.

The row communicates selection only through the tint and the lane-coloured rail. The rail carries aria-hidden="true". A screen reader therefore reports no difference between a selected row and an unselected row.

Add aria-pressed={selected} (or aria-current) to the button.

♿ Proposed change
 		<button
 			type="button"
 			onClick={onSelect}
 			onDoubleClick={onDoubleClick}
+			aria-pressed={selected}
 			title={commit.message}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.tsx
around lines 124 - 154, Add aria-pressed={selected} to the button rendered by
GraphRow so its selected state is exposed to assistive technology, while
preserving the existing visual selection behavior.

69-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider sharing the two-line rule with isTwoLineRow.

ownLine re-implements the rule that isTwoLineRow in GraphTabContent.tsx already defines (twoLineRefs && refs.length > 0). The virtualizer estimates row height from isTwoLineRow, and GraphRow renders height from ownLine. If one rule changes, the estimated height and the rendered height diverge and rows overlap.

Move the predicate into GraphLanes/constants.ts and import it in both places. That keeps a single rule and avoids a circular import between GraphRow and GraphTabContent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.tsx
around lines 69 - 78, Extract the shared two-line predicate currently
represented by ownLine and isTwoLineRow into a named helper in constants.ts,
then import and use it in both GraphRow and GraphTabContent. Replace the local
rule without changing the existing row-height estimation or rendering behavior,
and avoid introducing imports between those components.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/GraphTabContent.tsx (3)

92-99: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Build indexByHash only for the range case.

The map is built at lines 92-93, but the kind === "commit" branch returns at line 94 without reading it. Move the map construction below that branch.

♻️ Proposed change
-	const indexByHash = new Map<string, number>();
-	for (let i = 0; i < hashes.length; i++) indexByHash.set(hashes[i], i);
 	if (selection.kind === "commit") {
 		return {
 			selectedSet: new Set([selection.hash]),
 			inRangeSet: new Set<string>(),
 		};
 	}
+	const indexByHash = new Map<string, number>();
+	for (let i = 0; i < hashes.length; i++) indexByHash.set(hashes[i], i);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/GraphTabContent.tsx
around lines 92 - 99, Move the indexByHash Map construction and population below
the selection.kind === "commit" early-return in the surrounding selection-range
logic, so the commit branch returns without creating an unused map while range
handling retains the existing lookup behavior.

348-400: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The copy actions have no keyboard path.

The menu opens only from onContextMenu. Keyboard users cannot reach Copy Hash, Copy Short Hash, Copy Subject, or Open in New Tab. Consider opening the same menu on Shift+F10 or on the context-menu key when a row has focus.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/GraphTabContent.tsx
around lines 348 - 400, Add keyboard context-menu handling to the graph row
focus interaction so Shift+F10 and the Context Menu key open the existing menu
state used by onContextMenu, positioned for the focused row. Preserve the
current mouse context-menu behavior and ensure the existing Copy Hash, Copy
Short Hash, Copy Subject, and Open in New Tab actions remain available through
the keyboard.

264-282: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The truncation notice depends on three unrelated callbacks.

Line 264 gates the whole control strip on onSelectRefScope && onToggleTwoLineRefs && onToggleUnreferencedOnly. All three props are optional. If a caller omits one toggle, the scope chooser and the "Showing first N commits" notice both disappear. The comment above states that the strip must render even with zero rows, so this coupling works against the stated intent.

Make the three callbacks required in GraphTabContentProps, or render the strip unconditionally and gate only GraphHeaderActions on the callbacks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/GraphTabContent.tsx
around lines 264 - 282, Decouple the truncation notice and control strip from
the optional callbacks in GraphTabContent: update GraphTabContentProps to make
onSelectRefScope, onToggleTwoLineRefs, and onToggleUnreferencedOnly required, or
render the strip unconditionally while conditionally rendering
GraphHeaderActions only when its callbacks are available. Ensure the strip and
“Showing first N commits” notice still render with zero rows or when any
callback is omitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/plans/20260801-1200-git-graph-visual-spec.md`:
- Around line 202-206: Add the text language identifier to the opening fences of
both diagram code blocks in the visual specification, including the
Wide/Standard/Compact diagram and the corresponding block around the second
referenced section, to resolve MD040 warnings.
- Around line 93-155: Align the lane-colour specification with the actual
implementation: either document the theme-derived ANSI palette and
color-mix-based lanes 7–8, including the correct fallback-variable location, or
update the implementation to provide the specified fixed equal-lightness OKLCH
tokens in both themes. Apply the same correction to the corresponding later
section and ensure the documented token source matches the implementation.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphHeaderActions/GraphHeaderActions.tsx:
- Around line 109-131: Update the TooltipContent in the unreferenced toggle
within GraphHeaderActions to switch its text based on unreferencedOnly, matching
the existing state-dependent aria-label and the neighboring toggle behavior:
show the action to stop highlighting when active and the action to highlight
when inactive.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.test.tsx:
- Around line 228-232: Remove the self-comparison assertion in the “changes
nothing when the toggle is off” test, or replace it with a comparison against a
known single-line baseline; retain the existing “+5” and “max-w-[12ch]”
assertions.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.tsx:
- Around line 194-198: Update the showDate rendering in GraphRow to validate the
result of Date.parse(commit.date) before passing it to formatRelativeTime.
Render a reliable fallback when the date is unparseable, while preserving the
existing relative-time output for valid timestamps.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/GraphTabContent.test.ts:
- Around line 35-41: Rename the test covering resolveRowSelection(commit("zzz"),
H) to describe that an out-of-window commit remains in selectedSet while
matching no row, keeping its existing assertions unchanged.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/assignLanes.ts:
- Around line 140-169: Update the fork-parent handling in the assignLanes loop
so its free-lane search cannot select mine after the first parent terminated or
cleared that lane. Exclude mine when searching lanes.indexOf(null), allocate or
reuse another lane instead, and preserve the existing edge creation and parent
assignment behavior for other lanes.

In `@packages/host-service/src/workers/tasks/git.ts`:
- Around line 82-96: Validate commitHash and fromHash in the router input schema
before they reach gitCommitFilesTask, rejecting values beginning with “-” and
accepting only valid revision/hash shapes. Ensure both getChangedFilesForDiff
and git show receive validated revisions; apply the same constraint consistently
across other call sites identified by the search.

In `@plans/done/20260801-commit-graph-sidebar.md`:
- Line 147: Add language identifiers to every affected Markdown fence: use bash
at plans/done/20260801-commit-graph-sidebar.md lines 147-147, text at lines
183-183, and bash or text at plans/done/20260803-commit-graph-followups.md lines
43-43.
- Around line 222-226: Update the touched-files list and both palette sections
in the plan to reference apps/desktop/src/renderer/globals.css instead of
packages/ui/src/globals.css, preserving the existing graph token details.
- Around line 169-176: Update the GraphRow edge documentation for the
assignLanes utilities to match the shipped GraphEdge kind union defined in
types.ts, including in-straight, out-straight, pass, out-stub, and in-merge
alongside the existing kinds. Keep the documented edge shape and lane behavior
unchanged.
- Around line 311-317: Update the Phase 1 acceptance criteria to state that all
five ref states are returned correctly, matching the contract and integration
test coverage for open, detached-worktree, orphan-branch, prunable, and merged.
- Around line 15-16: Mark the original phase-5 design in this plan as superseded
by the persisted graphSelection approach and commit metadata rendered inside
DiffPane. Update the affected entries, including the changesFilter-based
selection and CommitDetailPanel references, or add an explicit superseded note
pointing to the follow-up plan; ensure all referenced sections consistently
reflect the replacement design.

In `@plans/done/20260803-commit-graph-followups.md`:
- Around line 1-8: Move the still-pending commit graph follow-up plan out of
plans/done/ and into plans/ while its listed defects, controls, verification
work, and deferred tasks remain open; return it to plans/done/ only after all
documented work ships.
- Around line 14-20: Reconcile the verification status in the current-state
table and Sections 5.1, 5.2, and 5.6 so they consistently reflect that the
profiling script ran and was removed, and that GraphLanes and RefBadge component
tests exist. Remove or update the outdated debt entries rather than leaving
contradictory claims.

---

Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/DiffPane/DiffPane.tsx:
- Around line 315-320: Update the DiffPane empty-files branch so
CommitMetadataHeader still renders when files.length is zero. Move the
empty-state return below the metadata header, preserving the existing
loading/“No changes” message while ensuring empty commits and unchanged ranges
retain their commit identity.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/GraphLanes.test.tsx:
- Around line 135-142: Update the affected GraphLanes test assertions around the
incoming and outgoing path checks, including the similar assertions near the
referenced second range, to derive the lane x-coordinate and viewBox width from
the imported graphGeometry values instead of hardcoded 7 and 18 literals.
Preserve the existing path expectations while making them adapt when geometry
pitch changes.
- Around line 87-98: Update the merge-case assertions in the test “renders a
donut node for a plain commit, a filled node for merges/roots” to verify
distinguishing SVG attributes rather than only circle count or lane color.
Assert that the plain commit uses the lane color as stroke without a separate
fill, while the merge uses the lane color as fill and does not rely on the
shared stroke assertion; keep the root circle-count behavior unchanged.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.tsx:
- Around line 124-154: Add aria-pressed={selected} to the button rendered by
GraphRow so its selected state is exposed to assistive technology, while
preserving the existing visual selection behavior.
- Around line 69-78: Extract the shared two-line predicate currently represented
by ownLine and isTwoLineRow into a named helper in constants.ts, then import and
use it in both GraphRow and GraphTabContent. Replace the local rule without
changing the existing row-height estimation or rendering behavior, and avoid
introducing imports between those components.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/GraphTabContent.tsx:
- Around line 92-99: Move the indexByHash Map construction and population below
the selection.kind === "commit" early-return in the surrounding selection-range
logic, so the commit branch returns without creating an unused map while range
handling retains the existing lookup behavior.
- Around line 348-400: Add keyboard context-menu handling to the graph row focus
interaction so Shift+F10 and the Context Menu key open the existing menu state
used by onContextMenu, positioned for the focused row. Preserve the current
mouse context-menu behavior and ensure the existing Copy Hash, Copy Short Hash,
Copy Subject, and Open in New Tab actions remain available through the keyboard.
- Around line 264-282: Decouple the truncation notice and control strip from the
optional callbacks in GraphTabContent: update GraphTabContentProps to make
onSelectRefScope, onToggleTwoLineRefs, and onToggleUnreferencedOnly required, or
render the strip unconditionally while conditionally rendering
GraphHeaderActions only when its callbacks are available. Ensure the strip and
“Showing first N commits” notice still render with zero rows or when any
callback is omitted.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/RefBadge/RefBadge.tsx:
- Around line 8-9: Update the RefBadgeProps laneColor property to use the
exported GraphLaneColor type from ../../types instead of number, preserving the
documented 1–8 constraint and existing CSS variable interpolation.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/assignLanes.test.ts:
- Around line 11-14: Update the full helper to use padEnd(40, "0") so every
label is exactly 40 characters, replacing the current FULL-based concatenation;
preserve the existing label-to-hash behavior used by present and findRow.

In `@packages/host-service/src/workers/tasks/git.ts`:
- Around line 102-120: Update the field-count guard in parseCommitMetadata to
reject any input with fewer than 7 tab-separated fields, preserving parsing only
for the complete seven-field format.

In `@packages/host-service/test/integration/git-graph.integration.test.ts`:
- Around line 196-205: Update the commit setup around the git.env calls for m2
and m3 (backdated) so each commit preserves the inherited process environment,
either by spreading process.env into the environment object or by passing the
dates as per-commit options. Keep the distinct author and committer dates for
both commits unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b7ed9b55-a6c5-4f73-be3c-212d009fc361

📥 Commits

Reviewing files that changed from the base of the PR and between ddd3d48 and 761c685.

📒 Files selected for processing (48)
  • apps/desktop/plans/20260801-1200-git-graph-visual-spec.md
  • apps/desktop/src/renderer/globals.css
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphHeaderActions/GraphHeaderActions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphHeaderActions/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/GraphLanes.test.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/GraphLanes.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/constants.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.test.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/GraphTabContent.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/GraphTabContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/RefBadge/RefBadge.test.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/RefBadge/RefBadge.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/RefBadge/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/types.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/useGraphTab.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/assignLanes.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/assignLanes.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useChangeset/useChangeset.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/DiffPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/components/CommitMetadataHeader/CommitMetadataHeader.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/components/CommitMetadataHeader/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspacePaneOpeners/useWorkspacePaneOpeners.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts
  • apps/desktop/src/renderer/stores/theme/store.ts
  • apps/desktop/src/renderer/stores/theme/utils/css-variables.ts
  • apps/desktop/src/renderer/stores/theme/utils/index.ts
  • apps/desktop/src/shared/themes/graph-lanes.test.ts
  • apps/desktop/src/shared/themes/graph-lanes.ts
  • packages/host-service/src/index.ts
  • packages/host-service/src/trpc/router/git/git.ts
  • packages/host-service/src/trpc/router/git/types.ts
  • packages/host-service/src/trpc/router/git/utils/graph-log.test.ts
  • packages/host-service/src/trpc/router/git/utils/graph-log.ts
  • packages/host-service/src/workers/tasks/git.ts
  • packages/host-service/test/integration/git-graph.integration.test.ts
  • packages/host-service/test/integration/git-history.integration.test.ts
  • plans/done/20260801-commit-graph-sidebar.md
  • plans/done/20260803-commit-graph-followups.md

Comment on lines +194 to +198
{showDate && (
<span className="min-w-10 shrink-0 text-right font-mono text-[11px] text-muted-foreground tabular-nums">
{formatRelativeTime(Date.parse(commit.date))}
</span>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the graph log format string and the date field it produces.
fd -t f 'graph-log' packages/host-service | xargs rg -n -C4 '%c|%a|pretty|format|date'

# Inspect formatRelativeTime's handling of NaN input.
fd -t f 'formatRelativeTime' apps/desktop/src/renderer/lib --exec cat -n {}

Repository: superset-sh/superset

Length of output: 10924


Guard against unparseable commit.date before rendering relative time.

graph-log.ts declares the date as a string but does not emit it; Date.parse(commit.date) can still receive values outside the host parser’s control. Use a validated timestamp before calling formatRelativeTime, and render a reliable fallback if parsing fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.tsx
around lines 194 - 198, Update the showDate rendering in GraphRow to validate
the result of Date.parse(commit.date) before passing it to formatRelativeTime.
Render a reliable fallback when the date is unparseable, while preserving the
existing relative-time output for valid timestamps.

Comment on lines +1 to +8
# Commit Graph — Pending Work

Follow-up to `plans/` sibling work on the sidebar commit graph. The graph ships and renders live;
this document covers what is left. Every item states the files, the acceptance bar, and why the
decision went the way it did.

Predecessor plan: `plans/20260801-commit-graph-sidebar.md` (phases 0–6). Phases 0–4 are landed;
phase 5 is **superseded** by §2 below; phase 6 is partially landed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep pending plans under plans/ until they ship.

This file is under plans/done/, but it documents open defects, queued controls, verification debt, and deferred work. Move it to plans/ while these items remain open, then move it to plans/done/ after completion.

As per coding guidelines, “Place cross-cutting implementation plans in plans/ and move shipped plans to plans/done/.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plans/done/20260803-commit-graph-followups.md` around lines 1 - 8, Move the
still-pending commit graph follow-up plan out of plans/done/ and into plans/
while its listed defects, controls, verification work, and deferred tasks remain
open; return it to plans/done/ only after all documented work ships.

Source: Coding guidelines

acseven added 2 commits August 7, 2026 15:17
- assignLanes: an extra parent could reclaim the lane the same row had just
  stubbed, drawing a terminator and a live edge on one lane. Skip the row's
  own lane when looking for a free slot; regression test added.
- git.getCommitFiles / git.getDiff: constrain commitHash and fromHash to a
  hash shape. They reach git positionally, and git reads a leading dash as an
  option — 'git show --output=<path>' writes an arbitrary file.
- GraphHeaderActions: the unreferenced tooltip stayed 'Highlight unreferenced'
  while its aria-label flipped with state.
- Tighten two tests that asserted less than they read as: the two-line-refs
  off case compared a render against itself, and the out-of-window selection
  test contradicted its own name.
Five ref states, not four; the lane tokens live in the desktop renderer's
globals.css, not packages/ui; the shipped GraphEdge kinds are the six-way
split, not straight/fork/merge. The visual spec gets a superseded note for the
lane ramp, which now derives from the active theme's ANSI palette. Fenced
blocks get languages.
@acseven

acseven commented Aug 7, 2026

Copy link
Copy Markdown
Author

Thanks — went through all of it. Pushed 40a0bc7 (code) and 7837067 (docs).

Fixed

  • assignLanes lane reclaim. Confirmed, and it is reachable on a real graph: when a row's first parent falls outside the fetch window, the row stubs its own lane and frees the slot, and the loop over extra parents could then pick that same slot back up via lanes.indexOf(null) — one lane carrying both an out-stub terminator and a live edge. Now lanes.findIndex((slot, i) => slot === null && i !== mine). New test does not fork an extra parent back onto the lane it just stubbed fails against the old line and passes against the new one. (The root-commit half of the finding doesn't apply — a root has no extra parents to loop over.)
  • Option-like revisions reaching git. Confirmed by reproduction: git show -s --format=%H "--output=/tmp/x/PWNED" writes the file under git 2.50.1, so commitHash was an arbitrary-file-write primitive. commitHash/fromHash on getCommitFiles and getDiff are now /^[0-9a-f]{4,40}$/i (plus the "" the renderer parks in a disabled query's input). Integration test asserts the option-shaped input is rejected and no file lands.
  • Unreferenced tooltip now flips with the button, matching the aria-label.
  • Two weak tests. changes nothing when the toggle is off compared a render against itself — it now asserts the one-line layout and that it differs from the two-line render. commit outside the window is simply absent asserted the opposite of its name; renamed to what it checks, with the "no row in the window is marked" assertion the name implied.
  • Docs drift: five ref states not four; the lane tokens live in apps/desktop/src/renderer/globals.css, not packages/ui; the GraphEdge kind list is the shipped six-way split. The visual spec gets a superseded note for the lane ramp — it now derives from the active theme's ANSI palette rather than the fixed OKLCH list, so a user-imported theme isn't ignored. Fenced blocks got languages.

Not fixed

  • Guarding commit.date against an empty string. The premise doesn't hold: the graph log format is %H%x09%h%x09%P%x09%an%x09%ae%x09%aI%x09%s (graph-log.ts:104), so the date is always emitted and always ISO-8601. git show's metadata path uses %aI too. The screenshots above render real relative times off that field.
  • Moving 20260803-commit-graph-followups.md out of plans/done/. Fair reading of the convention, but the placement was a deliberate call. Instead the doc now says outright that §5.6 is the authoritative status, which is what made it read as contradictory.

bun run lint is 0. Graph tests 62/62, host-service git suites 107/107. typecheck is unchanged: still only the pre-existing duplicate @types/hast resolution, no file in this branch.

@acseven
acseven marked this pull request as ready for review August 7, 2026 14:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/host-service/src/trpc/router/git/git.ts (1)

364-369: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include all traversal inputs in the coalescing key.

Lines 350-359 pass baseBranch, limit, and skip to the worker. Lines 364-369 omit them from dedupeKey.

If two requests use the same workspace and scope with different cursors, limits, or base branches, one request can receive the other request's page. The returned commits and nextCursor then become incorrect.

Proposed fix
-					dedupeKey: `${input.workspaceId}:graph-log:${input.refScope}`,
+					dedupeKey: JSON.stringify({
+						workspaceId: input.workspaceId,
+						baseBranch: input.baseBranch ?? null,
+						refScope: input.refScope,
+						limit: input.limit,
+						skip,
+					}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/trpc/router/git/git.ts` around lines 364 - 369,
Update the dedupeKey in the git graph-log traversal request to include every
traversal input passed to the worker: refScope, baseBranch, limit, and skip, in
addition to workspaceId. Preserve coalescing only for requests with identical
traversal parameters.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/host-service/src/trpc/router/git/git.ts`:
- Around line 364-369: Update the dedupeKey in the git graph-log traversal
request to include every traversal input passed to the worker: refScope,
baseBranch, limit, and skip, in addition to workspaceId. Preserve coalescing
only for requests with identical traversal parameters.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 13a3aedf-be83-4c37-843e-585da3cdc440

📥 Commits

Reviewing files that changed from the base of the PR and between 761c685 and 7837067.

📒 Files selected for processing (10)
  • apps/desktop/plans/20260801-1200-git-graph-visual-spec.md
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphHeaderActions/GraphHeaderActions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.test.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/GraphTabContent.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/assignLanes.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/assignLanes.ts
  • packages/host-service/src/trpc/router/git/git.ts
  • packages/host-service/test/integration/git-history.integration.test.ts
  • plans/done/20260801-commit-graph-sidebar.md
  • plans/done/20260803-commit-graph-followups.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.test.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphHeaderActions/GraphHeaderActions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/assignLanes.ts
  • plans/done/20260803-commit-graph-followups.md
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/assignLanes.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/GraphTabContent.test.ts
  • plans/done/20260801-commit-graph-sidebar.md
  • apps/desktop/plans/20260801-1200-git-graph-visual-spec.md

The dedupe key carried only workspace + refScope, so two overlapping
listGraph calls that differ in baseBranch, limit or skip were served one
another's rows. baseBranch resolves a tick after the graph mounts, so a
cold open reliably issues two such calls.
@acseven
acseven force-pushed the feat/git-graph-sidebar branch from 6a4d297 to 8e8d380 Compare August 7, 2026 15:01
@acseven

acseven commented Aug 7, 2026

Copy link
Copy Markdown
Author

Second review — one finding, confirmed and fixed in 8e8d380.

Include all traversal inputs in the coalescing key (git.ts:364-369) — real. WorkerTaskRunner.runTask returns the in-flight promise verbatim on a dedupeKey hit, so any overlapping call sharing workspaceId + refScope was served the other call's rows while still computing nextCursor from its own skip.

The paginating case CodeRabbit describes is the obvious one, but the reliable one is a cold open: useGraphTab fires listGraph with baseBranch: undefined while getBaseBranch is still loading, then refires with the resolved base. baseBranch is what feeds git branch --merged in gitGraphLogTask, so the second call could inherit a traversal that classified no branch as merged, and React Query then cached that under the resolved key for staleTime.

Key now carries scope, base, limit and skip. The two sibling coalescers (base-ref, commit-files) already key on their full payloads — checked, unchanged.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/host-service/src/trpc/router/git/git.ts">

<violation number="1" location="packages/host-service/src/trpc/router/git/git.ts:372">
P2: Overlapping requests with `baseBranch: undefined` and `baseBranch: ""` are incorrectly coalesced because both produce the same key, even though base resolution and `merged` ref classification differ; encoding the key as a structured tuple (or otherwise distinguishing `undefined`) would prevent one caller from receiving the other's result.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

// `baseBranch` alone decides the `merged` classification, and the
// base resolves a tick after mount, so the first two calls of a
// cold open genuinely differ.
dedupeKey: `${input.workspaceId}:graph-log:${input.refScope}:${input.baseBranch ?? ""}:${input.limit}:${skip}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Overlapping requests with baseBranch: undefined and baseBranch: "" are incorrectly coalesced because both produce the same key, even though base resolution and merged ref classification differ; encoding the key as a structured tuple (or otherwise distinguishing undefined) would prevent one caller from receiving the other's result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/git/git.ts, line 372:

<comment>Overlapping requests with `baseBranch: undefined` and `baseBranch: ""` are incorrectly coalesced because both produce the same key, even though base resolution and `merged` ref classification differ; encoding the key as a structured tuple (or otherwise distinguishing `undefined`) would prevent one caller from receiving the other's result.</comment>

<file context>
@@ -363,9 +363,13 @@ export const gitRouter = router({
+					// `baseBranch` alone decides the `merged` classification, and the
+					// base resolves a tick after mount, so the first two calls of a
+					// cold open genuinely differ.
+					dedupeKey: `${input.workspaceId}:graph-log:${input.refScope}:${input.baseBranch ?? ""}:${input.limit}:${skip}`,
 				},
 			);
</file context>
Suggested change
dedupeKey: `${input.workspaceId}:graph-log:${input.refScope}:${input.baseBranch ?? ""}:${input.limit}:${skip}`,
dedupeKey: JSON.stringify([
input.workspaceId,
"graph-log",
input.refScope,
input.baseBranch,
input.limit,
skip,
]),

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

23 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/host-service/src/trpc/router/git/utils/graph-log.ts">

<violation number="1" location="packages/host-service/src/trpc/router/git/utils/graph-log.ts:88">
P2: A valid remote branch named `feature/HEAD` will never appear as a graph ref badge because the symbolic-HEAD filter matches any nested ref ending in `/HEAD`; restrict the filter to exactly `refs/remotes/<remote>/HEAD` so nested branch names remain visible.</violation>

<violation number="2" location="packages/host-service/src/trpc/router/git/utils/graph-log.ts:113">
P2: Graph metadata is corrupted for commits whose author name contains a tab: the field split shifts the email and date because only the subject is treated as tab-safe. Use an unambiguous delimiter or escaping for every structured field in the worker format and parser.</violation>
</file>

<file name="apps/desktop/plans/20260801-1200-git-graph-visual-spec.md">

<violation number="1" location="apps/desktop/plans/20260801-1200-git-graph-visual-spec.md:74">
P3: The edge-geometry table header says `assignLanes` emits five edge kinds, but the signature actually produces six — `out-stub` (the dashed parent-outside-window edge) is documented only in the paragraph below and not counted. The implementation plan lists all six, so the spec and the plan disagree about the row contract. Small, but this file is the frozen seam definition; aligning the count avoids a reader treating `out-stub` as non-contractual.</violation>

<violation number="2" location="apps/desktop/plans/20260801-1200-git-graph-visual-spec.md:147">
P2: The §3 'Assignment' paragraph still describes lane colour as `hash(originatingBranchName) % 8`, but that is precisely the scheme the shipped `assignLanes` replaced (rotating cursor with live-lane skip) after the §1.2 collision defect was confirmed — 15 of 42 live rows painted the same colour. Since this spec is the 'signed off' design contract that owns colour, keeping the old algorithm here will mislead anyone applying the spec later. Update the Assignment paragraph to describe the rotating live-skip cursor (or point at `assignLanes`' `pickColor`) so the spec stops contradicting the implementation and the follow-up.</violation>

<violation number="3" location="apps/desktop/plans/20260801-1200-git-graph-visual-spec.md:290">
P3: The §6 Scope control entry is stale and contradicts the shipped code: it claims the select sits in the header showing "Local refs" with "the three not-yet-implemented scopes, disabled". All five scopes are implemented and enabled, the default scope's label is "All local branches" (not "Local refs"), and the control lives on the graph's own strip rather than the header. Since this spec is the design contract for the tab, a reader following §6 would be misled about the shipped scope surface.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx:139">
P2: Every workspace now performs a 500-commit `git.listGraph` traversal when its sidebar mounts, even if the user stays on Files, Changes, or Review. Passing the active-tab state through to disable this query until Graph is selected, or lazy-mounting the tab content, would avoid adding this host-service/git cost to the normal sidebar path.</violation>

<violation number="2" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx:188">
P3: Graph is rendered as the third sidebar tab because it is inserted before Review, despite the feature being specified as the fourth tab. Appending Graph after `reviewTab` preserves the existing tab order and makes Graph the new fourth tab.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts:158">
P3: The new persisted state is untested: the heal-workspace test that asserts each sidebarState default skips the added `graphSelection`, and none of the new fields (`graphRefScope`, `graphTwoLineRefs`, `graphUnreferencedOnly`, and the `graphSelection` range/commit discriminated union) get a parse or heal assertion. Since these add a defaulted enum, a new selection shape, and a new `activeTab` value, a round-trip/heal test for each would catch regressions on read/write healing.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/GraphTabContent.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphTabContent/GraphTabContent.tsx:151">
P3: The `limit` prop is discarded as an unused `_limit`, leaving a dead prop contract that can drift from the component's actual pagination behavior; removing it from this component and its caller would keep the API honest.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/components/CommitMetadataHeader/CommitMetadataHeader.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/components/CommitMetadataHeader/CommitMetadataHeader.tsx:23">
P3: Indented commit bodies lose their leading spaces in the metadata pane because `body` is passed through `.trim()`, making formatted commit messages inaccurate. Remove only the separator newline while preserving body indentation.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/RefBadge/RefBadge.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/RefBadge/RefBadge.tsx:93">
P2: Long ref names can be clipped without an ellipsis in the inline layout, making the intended capped badge harder to read; allowing the label flex item to shrink lets its existing `text-ellipsis` work as intended.</violation>
</file>

<file name="packages/host-service/src/workers/tasks/git.ts">

<violation number="1" location="packages/host-service/src/workers/tasks/git.ts:347">
P2: The `open-workspaces` scope includes commits reachable only from deleted detached worktrees, so stale history appears as an open workspace. Excluding `w.prunable` here would match the existing branch-worktree filtering and the documented scope semantics.</violation>
</file>

<file name="apps/desktop/src/renderer/globals.css">

<violation number="1" location="apps/desktop/src/renderer/globals.css:63">
P3: Dark-theme fallback lanes use the generic xterm bright ANSI values (#729fcf/#8ae234/#ef2929/#34e2e2/#ad7fa8/#fce94f), but the default ember theme ships its own bright palette (#7ec0f5/#98d1a8/#e88888/#73c7d3/#d494e6/#ecd08f). The light fallback in this same diff matches light.ts exactly, so the dark set is the only mismatch — before applyGraphLaneColors runs, hydrated graph lanes would flash to colors that never match the actual theme. Consider mirroring ember's bright ANSI values to keep the fallback consistent with the live set.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphRow/GraphRow.tsx:73">
P2: Commits carrying several long refs can lose their subject entirely at standard sidebar widths because the fixed three-badge allowance and non-shrinking ref strip consume all space before the subject. Reserve a minimum subject width or make the ref summary adapt to available width by collapsing additional badges.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/useGraphTab.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/useGraphTab.tsx:97">
P2: A ref change in one workspace invalidates all cached graph queries because this call omits the workspace input, so unrelated graph observers or later workspace revisits can trigger costly traversals. Scoping the invalidation to `{ workspaceId }` keeps refreshes local to the event.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/assignLanes.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/utils/assignLanes/assignLanes.ts:34">
P2: Existing branch lanes change color when a newly added earlier tip or a scope/refetch changes traversal order, so row tints and edge colors no longer identify the same branch across updates. Derive the lane color from the originating branch ref and propagate that identity through forks, using the documented lane-index fallback only when no branch name exists.</violation>
</file>

<file name="plans/done/20260803-commit-graph-followups.md">

<violation number="1" location="plans/done/20260803-commit-graph-followups.md:98">
P3: §2.1 says to 'Add a graphSelection field to sidebarState' and points at `schema.ts:132`, but the schema already defines `graphSelectionSchema` (line 117) with a nullable `graphSelection` default (lines 158/202), and the real file path is under renderer/routes/_authenticated/. Update the section to reflect that this wiring already exists rather than presenting it as work to do.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/GraphLanes.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/GraphLanes.tsx:42">
P2: Overflowed merge and fork edges can render as backward hooks when both endpoints collapse onto the last visible lane. Using a zero direction when `x0 === x1` would make the collapsed path vertical rather than extending past and back over the same lane.</violation>

<violation number="2" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/GraphLanes.tsx:91">
P3: Each row's svg is marked `aria-hidden="true"` but still carries an inert `<title>Commit graph lanes</title>`. Since the element is hidden from the accessibility tree, the title never reaches assistive tech, and in this virtualized list the identical text node is duplicated into every rendered row's DOM for no benefit while adding per-row DOM weight. It would be cleaner to drop the `<title>` (the column is purely decorative under aria-hidden), or if a label is genuinely wanted, make it meaningful and move it outside the aria-hidden subtree.</violation>

<violation number="3" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/GraphLanes.tsx:114">
P2: Plain commit nodes keep the base sidebar color inside their rings even when the row is hovered or selected, creating a visible square/spot mismatch with the row background. The node fill should track the row's effective ground color rather than using a fixed `fill-sidebar` token.</violation>
</file>

<file name="apps/desktop/src/shared/themes/graph-lanes.ts">

<violation number="1" location="apps/desktop/src/shared/themes/graph-lanes.ts:39">
P2: The light-theme lane ramp includes ANSI yellow without contrast normalization, making lane 6 a low-contrast 1.5px stroke on the light sidebar. Deriving the palette through a contrast-checked lightness adjustment (while preserving each hue) would keep all lanes readable.</violation>
</file>

<file name="packages/host-service/src/trpc/router/git/git.ts">

<violation number="1" location="packages/host-service/src/trpc/router/git/git.ts:335">
P2: A workspace whose worktree was deleted is returned as a successful empty graph because this path bypasses `resolveWorktreePath`; using the shared resolver preserves the expected `NOT_FOUND`/`WORKTREE_MISSING` response.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/DiffPane.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/DiffPane.tsx:337">
P2: Empty commits selected from the Graph pane show only “No changes” and never display their commit metadata, even though `getCommitFiles` returns a `commit` for them. Rendering the header before the empty-file early returns would preserve the commit subject, author, and hash for valid zero-file commits.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

if (
type === "remote" &&
fullRef.startsWith("refs/remotes/") &&
fullRef.endsWith("/HEAD")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A valid remote branch named feature/HEAD will never appear as a graph ref badge because the symbolic-HEAD filter matches any nested ref ending in /HEAD; restrict the filter to exactly refs/remotes/<remote>/HEAD so nested branch names remain visible.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/git/utils/graph-log.ts, line 88:

<comment>A valid remote branch named `feature/HEAD` will never appear as a graph ref badge because the symbolic-HEAD filter matches any nested ref ending in `/HEAD`; restrict the filter to exactly `refs/remotes/<remote>/HEAD` so nested branch names remain visible.</comment>

<file context>
@@ -0,0 +1,212 @@
+		if (
+			type === "remote" &&
+			fullRef.startsWith("refs/remotes/") &&
+			fullRef.endsWith("/HEAD")
+		) {
+			continue;
</file context>
Suggested change
fullRef.endsWith("/HEAD")
\t\t\t\t/^refs\/remotes\/[^/]+\/HEAD$/.test(fullRef)

const commits: RawGraphCommit[] = [];
for (const line of raw.split("\n")) {
if (!line) continue;
const parts = line.split("\t");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Graph metadata is corrupted for commits whose author name contains a tab: the field split shifts the email and date because only the subject is treated as tab-safe. Use an unambiguous delimiter or escaping for every structured field in the worker format and parser.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/git/utils/graph-log.ts, line 113:

<comment>Graph metadata is corrupted for commits whose author name contains a tab: the field split shifts the email and date because only the subject is treated as tab-safe. Use an unambiguous delimiter or escaping for every structured field in the worker format and parser.</comment>

<file context>
@@ -0,0 +1,212 @@
+	const commits: RawGraphCommit[] = [];
+	for (const line of raw.split("\n")) {
+		if (!line) continue;
+		const parts = line.split("\t");
+		if (parts.length < 7) continue;
+		const [hash, shortHash, parentsRaw, author, authorEmail, date] = parts;
</file context>

Index order is deliberately *not* the hue order — adjacent lanes on screen get maximally separated
hues (262 → 152 → 25 → 197), which matters far more than the ramp reading as a gradient when listed.

**Assignment.** Lane colour is `hash(originatingBranchName) % 8`, not `laneIndex % 8`. A lane keeps

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The §3 'Assignment' paragraph still describes lane colour as hash(originatingBranchName) % 8, but that is precisely the scheme the shipped assignLanes replaced (rotating cursor with live-lane skip) after the §1.2 collision defect was confirmed — 15 of 42 live rows painted the same colour. Since this spec is the 'signed off' design contract that owns colour, keeping the old algorithm here will mislead anyone applying the spec later. Update the Assignment paragraph to describe the rotating live-skip cursor (or point at assignLanes' pickColor) so the spec stops contradicting the implementation and the follow-up.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/plans/20260801-1200-git-graph-visual-spec.md, line 147:

<comment>The §3 'Assignment' paragraph still describes lane colour as `hash(originatingBranchName) % 8`, but that is precisely the scheme the shipped `assignLanes` replaced (rotating cursor with live-lane skip) after the §1.2 collision defect was confirmed — 15 of 42 live rows painted the same colour. Since this spec is the 'signed off' design contract that owns colour, keeping the old algorithm here will mislead anyone applying the spec later. Update the Assignment paragraph to describe the rotating live-skip cursor (or point at `assignLanes`' `pickColor`) so the spec stops contradicting the implementation and the follow-up.</comment>

<file context>
@@ -0,0 +1,348 @@
+Index order is deliberately *not* the hue order — adjacent lanes on screen get maximally separated
+hues (262 → 152 → 25 → 197), which matters far more than the ramp reading as a gradient when listed.
+
+**Assignment.** Lane colour is `hash(originatingBranchName) % 8`, not `laneIndex % 8`. A lane keeps
+its colour when the graph refetches and lane indices shift. Fallback for a lane with no branch name:
+`laneIndex % 8`.
</file context>

icon: LuGitCompareArrows,
};

const graphTabDef = useGraphTab({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Every workspace now performs a 500-commit git.listGraph traversal when its sidebar mounts, even if the user stays on Files, Changes, or Review. Passing the active-tab state through to disable this query until Graph is selected, or lazy-mounting the tab content, would avoid adding this host-service/git cost to the normal sidebar path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx, line 139:

<comment>Every workspace now performs a 500-commit `git.listGraph` traversal when its sidebar mounts, even if the user stays on Files, Changes, or Review. Passing the active-tab state through to disable this query until Graph is selected, or lazy-mounting the tab content, would avoid adding this host-service/git cost to the normal sidebar path.</comment>

<file context>
@@ -112,6 +136,19 @@ export function WorkspaceSidebar({
 		icon: LuGitCompareArrows,
 	};
 
+	const graphTabDef = useGraphTab({
+		workspaceId,
+		compact,
</file context>

/>
<span
className={cn(
"overflow-hidden text-ellipsis",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Long ref names can be clipped without an ellipsis in the inline layout, making the intended capped badge harder to read; allowing the label flex item to shrink lets its existing text-ellipsis work as intended.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/RefBadge/RefBadge.tsx, line 93:

<comment>Long ref names can be clipped without an ellipsis in the inline layout, making the intended capped badge harder to read; allowing the label flex item to shrink lets its existing `text-ellipsis` work as intended.</comment>

<file context>
@@ -0,0 +1,101 @@
+			/>
+			<span
+				className={cn(
+					"overflow-hidden text-ellipsis",
+					graphRef.state === "prunable" && "line-through",
+				)}
</file context>

const subject =
newlineIdx === -1 ? commit.message : commit.message.slice(0, newlineIdx);
const body =
newlineIdx === -1 ? "" : commit.message.slice(newlineIdx + 1).trim();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Indented commit bodies lose their leading spaces in the metadata pane because body is passed through .trim(), making formatted commit messages inaccurate. Remove only the separator newline while preserving body indentation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/components/CommitMetadataHeader/CommitMetadataHeader.tsx, line 23:

<comment>Indented commit bodies lose their leading spaces in the metadata pane because `body` is passed through `.trim()`, making formatted commit messages inaccurate. Remove only the separator newline while preserving body indentation.</comment>

<file context>
@@ -0,0 +1,67 @@
+	const subject =
+		newlineIdx === -1 ? commit.message : commit.message.slice(0, newlineIdx);
+	const body =
+		newlineIdx === -1 ? "" : commit.message.slice(newlineIdx + 1).trim();
+
+	return (
</file context>

Comment on lines +290 to +292
- **Scope control** — the header carries the `refScope` select (`Local refs` / the three
not-yet-implemented scopes, disabled). Persisted per project, so it does not belong in the row spec
beyond leaving the header 24px.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The §6 Scope control entry is stale and contradicts the shipped code: it claims the select sits in the header showing "Local refs" with "the three not-yet-implemented scopes, disabled". All five scopes are implemented and enabled, the default scope's label is "All local branches" (not "Local refs"), and the control lives on the graph's own strip rather than the header. Since this spec is the design contract for the tab, a reader following §6 would be misled about the shipped scope surface.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/plans/20260801-1200-git-graph-visual-spec.md, line 290:

<comment>The §6 Scope control entry is stale and contradicts the shipped code: it claims the select sits in the header showing "Local refs" with "the three not-yet-implemented scopes, disabled". All five scopes are implemented and enabled, the default scope's label is "All local branches" (not "Local refs"), and the control lives on the graph's own strip rather than the header. Since this spec is the design contract for the tab, a reader following §6 would be misled about the shipped scope surface.</comment>

<file context>
@@ -0,0 +1,348 @@
+- **Truncated** — sticky footer row, 24px, `--muted` background, hairline top border:
+  **"Showing 500 of 12,431 commits"** with a **Load more** button on the right. Numbers use
+  `tabular-nums`. Renders only when `nextCursor !== null`.
+- **Scope control** — the header carries the `refScope` select (`Local refs` / the three
+  not-yet-implemented scopes, disabled). Persisted per project, so it does not belong in the row spec
+  beyond leaving the header 24px.
</file context>
Suggested change
- **Scope control** — the header carries the `refScope` select (`Local refs` / the three
not-yet-implemented scopes, disabled). Persisted per project, so it does not belong in the row spec
beyond leaving the header 24px.
- **Scope control** — the graph's own control strip carries the `refScope` select (This workspace / Open workspaces / All local branches / Remote / Everything; default All local branches), plus the two-line-refs and unreferenced-only toggles. Persisted per project, so it does not belong in the row spec beyond leaving the strip rows at their set heights.

--fill-selected: color-mix(in oklab, var(--foreground) 10%, transparent);
/* Commit-graph lanes: pre-hydration fallback for the bright ANSI set.
The live values come from shared/themes/graph-lanes via applyGraphLaneColors. */
--graph-lane-1: #729fcf;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Dark-theme fallback lanes use the generic xterm bright ANSI values (#729fcf/#8ae234/#ef2929/#34e2e2/#ad7fa8/#fce94f), but the default ember theme ships its own bright palette (#7ec0f5/#98d1a8/#e88888/#73c7d3/#d494e6/#ecd08f). The light fallback in this same diff matches light.ts exactly, so the dark set is the only mismatch — before applyGraphLaneColors runs, hydrated graph lanes would flash to colors that never match the actual theme. Consider mirroring ember's bright ANSI values to keep the fallback consistent with the live set.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/globals.css, line 63:

<comment>Dark-theme fallback lanes use the generic xterm bright ANSI values (#729fcf/#8ae234/#ef2929/#34e2e2/#ad7fa8/#fce94f), but the default ember theme ships its own bright palette (#7ec0f5/#98d1a8/#e88888/#73c7d3/#d494e6/#ecd08f). The light fallback in this same diff matches light.ts exactly, so the dark set is the only mismatch — before applyGraphLaneColors runs, hydrated graph lanes would flash to colors that never match the actual theme. Consider mirroring ember's bright ANSI values to keep the fallback consistent with the live set.</comment>

<file context>
@@ -58,6 +58,16 @@
 	--fill-selected: color-mix(in oklab, var(--foreground) 10%, transparent);
+	/* Commit-graph lanes: pre-hydration fallback for the bright ANSI set.
+	   The live values come from shared/themes/graph-lanes via applyGraphLaneColors. */
+	--graph-lane-1: #729fcf;
+	--graph-lane-2: #8ae234;
+	--graph-lane-3: #ef2929;
</file context>

(`useGraphTab.tsx:76-86`), and the shift-click anchor is read out of the same field
(`useGraphTab.tsx:95-110`). Stop writing `changesFilter` and the graph stops highlighting itself.

Add a `graphSelection` field to `sidebarState`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: §2.1 says to 'Add a graphSelection field to sidebarState' and points at schema.ts:132, but the schema already defines graphSelectionSchema (line 117) with a nullable graphSelection default (lines 158/202), and the real file path is under renderer/routes/_authenticated/. Update the section to reflect that this wiring already exists rather than presenting it as work to do.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plans/done/20260803-commit-graph-followups.md, line 98:

<comment>§2.1 says to 'Add a graphSelection field to sidebarState' and points at `schema.ts:132`, but the schema already defines `graphSelectionSchema` (line 117) with a nullable `graphSelection` default (lines 158/202), and the real file path is under renderer/routes/_authenticated/. Update the section to reflect that this wiring already exists rather than presenting it as work to do.</comment>

<file context>
@@ -0,0 +1,359 @@
+(`useGraphTab.tsx:76-86`), and the shift-click anchor is read out of the same field
+(`useGraphTab.tsx:95-110`). Stop writing `changesFilter` and the graph stops highlighting itself.
+
+Add a `graphSelection` field to `sidebarState`
+(`CollectionsProvider/dashboardSidebarLocal/schema.ts:132`), same discriminated shape as
+`changesFilterSchema` minus the `all` case:
</file context>

height={height}
viewBox={`0 0 ${width} ${height}`}
>
<title>Commit graph lanes</title>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Each row's svg is marked aria-hidden="true" but still carries an inert <title>Commit graph lanes</title>. Since the element is hidden from the accessibility tree, the title never reaches assistive tech, and in this virtualized list the identical text node is duplicated into every rendered row's DOM for no benefit while adding per-row DOM weight. It would be cleaner to drop the <title> (the column is purely decorative under aria-hidden), or if a label is genuinely wanted, make it meaningful and move it outside the aria-hidden subtree.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useGraphTab/components/GraphLanes/GraphLanes.tsx, line 91:

<comment>Each row's svg is marked `aria-hidden="true"` but still carries an inert `<title>Commit graph lanes</title>`. Since the element is hidden from the accessibility tree, the title never reaches assistive tech, and in this virtualized list the identical text node is duplicated into every rendered row's DOM for no benefit while adding per-row DOM weight. It would be cleaner to drop the `<title>` (the column is purely decorative under aria-hidden), or if a label is genuinely wanted, make it meaningful and move it outside the aria-hidden subtree.</comment>

<file context>
@@ -0,0 +1,132 @@
+			height={height}
+			viewBox={`0 0 ${width} ${height}`}
+		>
+			<title>Commit graph lanes</title>
+			{row.edges.map((edge) => (
+				<path
</file context>

@github-actions

Copy link
Copy Markdown
Contributor

This PR has had no activity for 53 days and will close in 7 days unless there is new activity. If this contribution is still important or relevant, please leave a comment to keep it open. If it closes, you are welcome to reopen it or ask a maintainer to reopen it. Thank you for contributing!

@github-actions github-actions Bot added the stale label Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant