Skip to content

feat(desktop): scaffold PR action header in v2 sidebar (gated)#3777

Merged
AviPeltz merged 9 commits into
mainfrom
pull-diff-editor-branch-changes-into-current-branc
Apr 27, 2026
Merged

feat(desktop): scaffold PR action header in v2 sidebar (gated)#3777
AviPeltz merged 9 commits into
mainfrom
pull-diff-editor-branch-changes-into-current-branc

Conversation

@AviPeltz
Copy link
Copy Markdown
Collaborator

@AviPeltz AviPeltz commented Apr 26, 2026

Summary

  • Cherry-picks the wip PR-flow scaffolding from the local diff-editor branch into v2 (PRActionHeader, usePRFlowState, usePRFlowDispatch, buildPRContext, getPRFlowState, agent slash command, plan doc).
  • Integrates the wip alongside the recent v2 refactor: restores v2UserPreferencesSchema + pendingWorkspaceSchema fields the wip dropped, points the sidebar-open reader at user prefs (matches what the toggle hotkey writes), adds focusRequestId route param + getDefaultBranchName import the wip referenced without declaring, drops the removed changesSubtab write.
  • Gates rendering behind PR_ACTION_HEADER_ENABLED = false since the flow currently dispatches to a chat session and v2 doesn't have chat yet — an empty 48px bar still renders so the sidebar layout doesn't shift when the flag flips.

Why gated

The PRActionHeader's "Create PR" / "Create draft PR" buttons hand off to a v2 chat pane via onOpenChat/pr/create-pr slash command + a synthesized pr-context.md attachment. v2 doesn't have the chat pane yet, so clicking would no-op. Flip to true once chat lands and the agent flow is verified end-to-end.

Test plan

  • Sidebar still opens/closes via the toggle hotkey on v2 workspaces (regression check on the useV2UserPreferences.rightSidebarOpen rewire).
  • Empty header bar renders above the Changes/Files/Review tabs at the right height (no layout shift when flag flips).
  • Set PR_ACTION_HEADER_ENABLED = true locally and confirm the button + dropdown render and dispatch (chat pane will be the no-op blocker).
  • bun run typecheck clean.

Summary by cubic

Added an icon-only PR action header to the v2 sidebar with a state‑tinted PR badge, a CI status dot, a hover detail card, and a merge dropdown; Create PR still dispatches to chat and is gated by CREATE_PR_BUTTON_ENABLED = false. The sidebar now renders full height via a portal, persists its width, adds overflow guards and a Changes tab refresh, and the tab order is now Files → Changes → Review.

  • New Features

    • PRActionHeader: icon-only state machine; PR link shows a CI status dot and a hover detail card; merge dropdown (squash/merge/rebase) wired to workspaceTrpc.github.mergePR, then refreshes via pullRequests.refreshByWorkspaces.
    • Create PR (and draft) dispatches /pr/create-pr with a synthesized pr-context.md from getPRFlowState and buildPRContext (with tests); gated by CREATE_PR_BUTTON_ENABLED = false.
    • Changes tab: refresh button invalidates getStatus, getDiff, listCommits, listBranches, and getBaseBranch.
  • Bug Fixes

    • Workspace: restore min-w-[320px] so the editor can’t collapse; initialize the sidebar portal slot synchronously to avoid a 1-frame flash.
    • PR merge flow: catch and toast refresh failures after a successful merge instead of failing silently.
    • Changes tab: spinner + error handling for the refresh action.
    • Backend: git.getBranchSyncStatus now logs git.status() failures and falls back safely; removed dead per-workspace sidebar fields.
    • Safety: use TextEncoder for base64 in dispatch; quote refs in the create-pr command doc; buildPRContext defers missing default-branch resolution to gh repo view.

Written for commit fc0c4f5. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • New Features

    • PR action header in the workspace sidebar with PR status/link, merge actions, and Create PR (including draft) flow that builds a PR context and opens a chat-based creation flow.
    • Compact PR detail card, status indicators (CI/reviews), and a retry/refresh affordance.
  • Improvements

    • Unified sidebar tabs (Changes, Files, Review), right sidebar portal with persistent resizable width, layout refinements, and a manual refresh button for Changes.
    • Streamlined header/tab styling and simplified change-header counts.
  • Documentation & Tests

    • New agent command docs, plan, and tests for PR context generation and PR-flow state.

Wires the cherry-picked wip into v2 alongside the recent main refactor:
restores `v2UserPreferencesSchema` + `pendingWorkspaceSchema` fields the
wip dropped, points the sidebar-open read at user prefs (where the toggle
hotkey writes), adds `focusRequestId` route param + `getDefaultBranchName`
import the wip referenced without declaring, and drops the removed
`changesSubtab` write. PR action header rendering is gated behind a
`PR_ACTION_HEADER_ENABLED = false` constant — empty bar still renders so
sidebar layout stays consistent. Flip to true once chat lands in v2 and
the slash-command flow is verified end-to-end.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an agent-driven PR creation flow: new agent command docs, PR flow state machine and selectors, UI components (PRActionHeader, PRStatusGroup, PRDetailCard), hooks to build/send pr-context.md to a chat agent, branch-sync TRPC endpoint, right-sidebar portal/persisted width, schema changes, utilities for checks rollup, tests, and barrels.

Changes

Cohort / File(s) Summary
Agent command & plan docs
.agents/commands/pr/create-pr.md, apps/desktop/plans/20260420-1045-agent-driven-pr-flow.md
New agent command documentation and high-level plan describing preconditions, commit/push/gh steps, draft handling, chat dispatch with pr-context.md, guardrails, and data-layer requirements/MVP.
PR flow model & selectors
.../PRActionHeader/utils/getPRFlowState/getPRFlowState.ts, ...getPRFlowState.test.ts, ...getPRFlowState/index.ts
Adds discriminated PRFlowState, unavailable reasons, selectors (selectActionButton, selectPRLink, selectStatusBadge) and tests mapping state → UI variants.
PR context builder & tests
.../PRActionHeader/utils/buildPRContext/buildPRContext.ts, ...buildPRContext.test.ts, ...buildPRContext/index.ts
Adds buildPRContext(state) to produce pr-context.md markdown (sync status, preconditions, example gh pr create), plus tests validating content, draft/base handling, and loading stub.
Dispatch hook & plan logic
.../hooks/usePRFlowDispatch/usePRFlowDispatch.ts, ...usePRFlowDispatch.test.ts, ...usePRFlowDispatch/index.ts
Introduces usePRFlowDispatch and planDispatch that build a base64 pr-context.md attachment and open chat with /pr/create-pr[ --draft]; includes encodeAsDataUrl and tests for plan/attachment.
State hook integration
.../hooks/usePRFlowState/usePRFlowState.ts, .../usePRFlowState/index.ts
Adds usePRFlowState(workspaceId) querying getPullRequest and getBranchSyncStatus, deriving PRFlowState, polling/refetch behavior, and onRetry.
PRActionHeader & Status UI
.../PRActionHeader/PRActionHeader.tsx, .../PRActionHeader/index.ts, .../PRStatusGroup/..., PRDetailCard, PRStatusIndicators files
New PRActionHeader component that selects action variant, renders create button/tooltip, PRStatusGroup with merge dropdown (TRPC merge), PRDetailCard and PRStatusIndicators components; barrel exports added.
Checks rollup util
.../utils/computeChecksStatus/computeChecksStatus.ts, .../index.ts
Adds check-run normalization (coerceCheckStatus) and computeChecksRollup producing counts and overall status.
WorkspaceSidebar & layout refactor
apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx, ...WorkspaceSidebar/WorkspaceSidebar.tsx, ...SidebarHeader/SidebarHeader.tsx, ...page.tsx
Refactors dashboard to horizontal layout with #workspace-right-sidebar-slot portal, moves modals/dialogs, replaces nested changes subtabs with top-level activeTab, adjusts compact resize behavior and layout classes.
Tabs, refresh, minor UX
...useChangesTab/useChangesTab.tsx, ...ChangesTabContent.tsx, ...ChangesHeader/ChangesHeader.tsx, ...useReviewTab/useReviewTab.tsx, apps/desktop/.../RightSidebar/headerTabStyles.ts
Adds refresh control (parallel TRPC invalidation) for Changes tab; removes explicit commitCount prop; adds Review tab icon; tweaks header tab spacing and icon sizes.
Persistent state & preferences
apps/desktop/.../dashboardSidebarLocal/schema.ts, apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts, .../useMigrateV1DataToV2/writeSidebarState.ts
Replaces changesSubtab with sidebarState.activeTab (`changes
Backend: git TRPC
packages/host-service/src/trpc/router/git/git.ts
Adds git.getBranchSyncStatus protected procedure (detached HEAD, default branch, remotes, ahead/behind counts, uncommitted changes, current/default branch) and extends getPullRequest output with repoOwner/repoName.
Tests & barrels
.../PRActionHeader/index.ts, .../buildPRContext/index.ts, .../getPRFlowState/index.ts, various *.test.ts files
Adds multiple Bun/Jest tests for getPRFlowState, buildPRContext, and planDispatch; adds barrel export files for new utilities/components.

Sequence Diagram

sequenceDiagram
    participant User
    participant Sidebar as WorkspaceSidebar
    participant StateHook as usePRFlowState
    participant PRHeader as PRActionHeader
    participant Dispatch as usePRFlowDispatch
    participant Chat as Chat Pane
    participant Agent as PR Agent
    participant Git as Git CLI
    participant Backend as TRPC

    User->>Sidebar: view workspace
    Sidebar->>StateHook: request flowState(workspaceId)
    StateHook->>Backend: getPullRequest(), getBranchSyncStatus()
    Backend-->>StateHook: PR + sync data
    StateHook-->>Sidebar: PRFlowState
    Sidebar->>PRHeader: render(state)
    User->>PRHeader: click "Create PR"
    PRHeader->>Dispatch: dispatch(state, draft?)
    Dispatch->>Chat: open chat with initialPrompt and attachment (pr-context.md)
    Chat->>Agent: user runs /pr/create-pr
    Agent->>Chat: read pr-context.md
    Agent->>Git: run commit/push commands per preconditions
    Git-->>Agent: command results
    Agent->>Backend: create PR (gh or API via TRPC)
    Backend-->>Agent: PR created (URL)
    Agent->>Chat: report PR URL
    Chat->>User: display PR link
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through branches, I stamped a file,

Built markdown context and pushed with a smile.
Buttons that shimmer, an agent that sings,
Merges and toasts and tiny bright things.
Carrots for reviewers — PRs take flight!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning PR description lacks required template sections and is overly detailed with internal implementation notes rather than following the standardized format. Rewrite the PR description to match the template structure: use the specified 'Description', 'Related Issues', 'Type of Change', 'Testing', 'Screenshots', and 'Additional Notes' sections with clear, concise information.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(desktop): scaffold PR action header in v2 sidebar (gated)' accurately summarizes the main change—adding a PR action header component to the v2 sidebar with the feature gated behind a flag.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pull-diff-editor-branch-changes-into-current-branc

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 and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 26, 2026

Greptile Summary

This PR cherry-picks and integrates the WIP PR-action-header scaffold (state machine, dispatcher, buildPRContext, getBranchSyncStatus tRPC procedure) into the v2 sidebar, and simultaneously refactors the sidebar layout to render via a createPortal into a slot div in the parent layout. The feature itself is correctly gated behind PR_ACTION_HEADER_ENABLED = false.

  • P1 — revealPath writes to the wrong store: revealPath in page.tsx calls collections.v2WorkspaceLocalState.update(..., draft.rightSidebarOpen = true), but sidebar visibility is read from v2UserPreferences.rightSidebarOpen. The two state trees are independent, so "reveal in sidebar" will silently fail to open the sidebar.

Confidence Score: 4/5

Safe to merge for the feature-flag-gated PR scaffold, but the revealPath sidebar-open regression should be fixed before this ships.

One P1 regression: revealPath writes to workspace-local state but the sidebar open state is read from user preferences, so revealing a file in the sidebar won't open the sidebar panel. The new PR-flow infrastructure (state machine, dispatcher, buildPRContext, tRPC procedure) is clean and well-tested; the remaining finding is a P2 style issue.

apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx — revealPath writes rightSidebarOpen to wrong state tree

Important Files Changed

Filename Overview
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx Major refactor: inlines several hooks, migrates sidebar to a portal, adds openChatPane; contains a P1 bug where revealPath writes rightSidebarOpen to workspace-local state but sidebar visibility reads from user prefs
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx Refactored: splits Changes/Review/Files into separate top-level tabs, adds hysteresis to compact breakpoint, wires usePRFlowState/usePRFlowDispatch, gates PRActionHeader behind feature flag
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.ts New pure state reducer + selectors for the PR action header; well-structured, exhaustive switch cases, correctly scoped to MVP UnavailableReason variants
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowDispatch/usePRFlowDispatch.ts New hook that builds and dispatches the chat launch config; logic is correct, but encodeAsDataUrl uses deprecated unescape()
packages/host-service/src/trpc/router/git/git.ts Adds getBranchSyncStatus tRPC procedure; correctly parses git rev-list --left-right --count @{upstream}...HEAD (left=pull, right=push)
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts Migrates changesSubtab to activeTab (3-way enum), adds rightSidebarOpen and rightSidebarWidth fields; rightSidebarOpen is written by revealPath in page.tsx but never read for sidebar visibility
apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx Adds #workspace-right-sidebar-slot div for the portal target, removes migration hooks, restructures layout nesting
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx New gated UI component; renders correctly from PRFlowState selectors; split-button and disabled-tooltip cases are implemented cleanly
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/buildPRContext/buildPRContext.ts New pure function that generates the pr-context.md markdown attachment; well-tested with snapshot assertions for all relevant branch states

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["User clicks 'Create PR' or 'Create draft PR'"] --> B["PRActionHeader\n(gated: PR_ACTION_HEADER_ENABLED)"]
    B --> C["usePRFlowDispatch\nplanDispatch(state, {draft})"]
    C --> D["buildPRContext(state)\n→ pr-context.md markdown"]
    D --> E["encodeAsDataUrl()\ndata:text/markdown;base64,..."]
    E --> F["onOpenChat(launchConfig)\ninitialPrompt: /pr/create-pr [--draft]"]
    F --> G["store.addTab\n{kind: chat, data: {launchConfig}}"]
    G --> H["Chat pane opens\nAgent reads pr-context.md"]
    H --> I["Agent: commit/push/gh pr create"]
    J["usePRFlowState(workspaceId)"] --> K["workspaceTrpc.git.getPullRequest"]
    J --> L["workspaceTrpc.git.getBranchSyncStatus"]
    K & L --> M["getPRFlowState()\npure reducer"]
    M --> N["PRFlowState\nloading / unavailable / no-pr / pr-exists / busy / error"]
    N --> B
    subgraph Selectors
      N --> O["selectActionButton"]
      N --> P["selectPRLink"]
      N --> Q["selectStatusBadge"]
    end
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx, line 393-396 (link)

    P2 Portal target queried once — no retry if DOM not ready

    document.getElementById("workspace-right-sidebar-slot") runs in a one-time useEffect with no dependency array. If the layout component defers rendering the slot div (e.g., due to a concurrent render or a future conditional), sidebarSlotEl stays null and the sidebar portal never mounts with no error or fallback. Consider a small MutationObserver guard or a shared ref to the slot element passed through context so the lookup is guaranteed to succeed.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
    Line: 393-396
    
    Comment:
    **Portal target queried once — no retry if DOM not ready**
    
    `document.getElementById("workspace-right-sidebar-slot")` runs in a one-time `useEffect` with no dependency array. If the layout component defers rendering the slot div (e.g., due to a concurrent render or a future conditional), `sidebarSlotEl` stays `null` and the sidebar portal never mounts with no error or fallback. Consider a small `MutationObserver` guard or a shared ref to the slot element passed through context so the lookup is guaranteed to succeed.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
Line: 208-213

Comment:
**`revealPath` writes to the wrong state tree — sidebar won't open**

`revealPath` sets `draft.rightSidebarOpen = true` on the *workspace local state* (via `collections.v2WorkspaceLocalState.update`), but sidebar visibility is controlled by `v2UserPreferences.rightSidebarOpen` (user prefs). Because these are two independent state trees, clicking "reveal in sidebar" will update a value that nobody reads for sidebar visibility, so the sidebar stays closed.

The old code called `setRightSidebarOpen(true)` from `useV2UserPreferences`, which wrote to the correct store. Either switch back to that call here, or update the sidebar open reader to consume the workspace-local field that is already being written.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowDispatch/usePRFlowDispatch.ts
Line: 70-76

Comment:
**`unescape` is deprecated — use `TextEncoder` for UTF-8 base64**

`unescape(encodeURIComponent(content))` encodes non-ASCII characters correctly but relies on the deprecated `unescape` global (removed from the WHATWG specification; may trigger linter errors or warnings). Branch names and commit messages can easily contain non-ASCII characters. The `TextEncoder` API is available in both browsers and Node 18+:

```ts
function encodeAsDataUrl(content: string, mediaType: string): string {
  const bytes = new TextEncoder().encode(content);
  const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
  const base64 = btoa(binary);
  return `data:${mediaType};base64,${base64}`;
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
Line: 393-396

Comment:
**Portal target queried once — no retry if DOM not ready**

`document.getElementById("workspace-right-sidebar-slot")` runs in a one-time `useEffect` with no dependency array. If the layout component defers rendering the slot div (e.g., due to a concurrent render or a future conditional), `sidebarSlotEl` stays `null` and the sidebar portal never mounts with no error or fallback. Consider a small `MutationObserver` guard or a shared ref to the slot element passed through context so the lookup is guaranteed to succeed.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "chore(desktop): integrate PR action head..." | Re-trigger Greptile

Comment on lines +208 to +213
}
state.openPane({
pane: {
kind: "file",
data: {
filePath,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 revealPath writes to the wrong state tree — sidebar won't open

revealPath sets draft.rightSidebarOpen = true on the workspace local state (via collections.v2WorkspaceLocalState.update), but sidebar visibility is controlled by v2UserPreferences.rightSidebarOpen (user prefs). Because these are two independent state trees, clicking "reveal in sidebar" will update a value that nobody reads for sidebar visibility, so the sidebar stays closed.

The old code called setRightSidebarOpen(true) from useV2UserPreferences, which wrote to the correct store. Either switch back to that call here, or update the sidebar open reader to consume the workspace-local field that is already being written.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
Line: 208-213

Comment:
**`revealPath` writes to the wrong state tree — sidebar won't open**

`revealPath` sets `draft.rightSidebarOpen = true` on the *workspace local state* (via `collections.v2WorkspaceLocalState.update`), but sidebar visibility is controlled by `v2UserPreferences.rightSidebarOpen` (user prefs). Because these are two independent state trees, clicking "reveal in sidebar" will update a value that nobody reads for sidebar visibility, so the sidebar stays closed.

The old code called `setRightSidebarOpen(true)` from `useV2UserPreferences`, which wrote to the correct store. Either switch back to that call here, or update the sidebar open reader to consume the workspace-local field that is already being written.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +70 to +76
default:
return null;
}
}

function encodeAsDataUrl(content: string, mediaType: string): string {
const base64 =
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 unescape is deprecated — use TextEncoder for UTF-8 base64

unescape(encodeURIComponent(content)) encodes non-ASCII characters correctly but relies on the deprecated unescape global (removed from the WHATWG specification; may trigger linter errors or warnings). Branch names and commit messages can easily contain non-ASCII characters. The TextEncoder API is available in both browsers and Node 18+:

function encodeAsDataUrl(content: string, mediaType: string): string {
  const bytes = new TextEncoder().encode(content);
  const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
  const base64 = btoa(binary);
  return `data:${mediaType};base64,${base64}`;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowDispatch/usePRFlowDispatch.ts
Line: 70-76

Comment:
**`unescape` is deprecated — use `TextEncoder` for UTF-8 base64**

`unescape(encodeURIComponent(content))` encodes non-ASCII characters correctly but relies on the deprecated `unescape` global (removed from the WHATWG specification; may trigger linter errors or warnings). Branch names and commit messages can easily contain non-ASCII characters. The `TextEncoder` API is available in both browsers and Node 18+:

```ts
function encodeAsDataUrl(content: string, mediaType: string): string {
  const bytes = new TextEncoder().encode(content);
  const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
  const base64 = btoa(binary);
  return `data:${mediaType};base64,${base64}`;
}
```

How can I resolve this? If you propose a fix, please make it concise.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 26, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

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/components/WorkspaceSidebar/WorkspaceSidebar.tsx (1)

122-155: ⚠️ Potential issue | 🟠 Major

PR-flow hooks poll even when the feature flag is off.

usePRFlowState issues two tRPC queries (git.getPullRequest, git.getBranchSyncStatus) with refetchInterval: 10_000 and refetchOnWindowFocus: true, and runs unconditionally here. With PR_ACTION_HEADER_ENABLED = false (the shipping state of this PR), the data and dispatch are never used, but every open v2 workspace will still hit both procedures every 10 seconds. Given the PR description notes the gate exists specifically because the chat pane it dispatches to doesn't yet exist in v2, the polling is pure overhead until the flag flips.

Two cheap options:

  • Plumb the flag into usePRFlowState and pass it through to each query's enabled (e.g., enabled: PR_ACTION_HEADER_ENABLED && !!workspaceId), and short-circuit usePRFlowDispatch's callers behind the same flag.
  • Or, only call these hooks from inside PRActionHeader and lift the flag check above the hook calls in this component.

Hooks rules are still satisfied as long as the flag is a stable module constant.

Sketch of option 1
-	const { flowState, onRetry } = usePRFlowState(workspaceId);
-	const dispatch = usePRFlowDispatch({
-		onOpenChat: onOpenChat ?? (() => {}),
-	});
+	const { flowState, onRetry } = usePRFlowState(workspaceId, {
+		enabled: PR_ACTION_HEADER_ENABLED,
+	});
+	const dispatch = usePRFlowDispatch({
+		onOpenChat: onOpenChat ?? (() => {}),
+	});

…with usePRFlowState forwarding enabled into both useQuery calls.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx
around lines 122 - 155, usePRFlowState and usePRFlowDispatch are called
unconditionally causing git.getPullRequest and git.getBranchSyncStatus to poll
even when PR_ACTION_HEADER_ENABLED is false; either pass the flag into
usePRFlowState (e.g., add an enabled parameter and forward it to both useQuery
calls for git.getPullRequest and git.getBranchSyncStatus as enabled:
PR_ACTION_HEADER_ENABLED && !!workspaceId) and short-circuit usePRFlowDispatch
callers when the flag is false, or lift the PR_ACTION_HEADER_ENABLED check above
the hooks and only call usePRFlowState/usePRFlowDispatch inside PRActionHeader
(move hook calls into PRActionHeader) so polling only runs when the feature flag
is true; reference usePRFlowState, usePRFlowDispatch, PRActionHeader,
PR_ACTION_HEADER_ENABLED, workspaceId, git.getPullRequest, and
git.getBranchSyncStatus when making the change.
🧹 Nitpick comments (7)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/SidebarHeader/SidebarHeader.tsx (1)

25-60: Duplicate non-compact button render — btn is unused when compact is false.

btn is constructed for every tab (lines 25–37) but is only consumed inside the compact branch's TooltipTrigger. The non-compact branch (lines 50–60) re-renders an essentially identical button instead of just returning btn. This is what forced the size-3 icon update to be applied in two places in this PR (lines 34 and 57) and will keep doubling future class/handler edits.

♻️ Suggested simplification
-				{tabs.map((tab) => {
-					const isActive = activeTab === tab.id;
-					const btn = (
-						<button
-							type="button"
-							onClick={() => onTabChange(tab.id)}
-							className={getSidebarHeaderTabButtonClassName({
-								isActive,
-								compact,
-							})}
-						>
-							{tab.icon && <tab.icon className="size-3" />}
-							{!compact && tab.label}
-						</button>
-					);
-
-					if (compact) {
-						return (
-							<Tooltip key={tab.id}>
-								<TooltipTrigger asChild>{btn}</TooltipTrigger>
-								<TooltipContent side="bottom" showArrow={false}>
-									{tab.label}
-								</TooltipContent>
-							</Tooltip>
-						);
-					}
-
-					return (
-						<button
-							key={tab.id}
-							type="button"
-							onClick={() => onTabChange(tab.id)}
-							className={getSidebarHeaderTabButtonClassName({ isActive })}
-						>
-							{tab.icon && <tab.icon className="size-3" />}
-							{tab.label}
-						</button>
-					);
-				})}
+				{tabs.map((tab) => {
+					const isActive = activeTab === tab.id;
+					const btn = (
+						<button
+							key={tab.id}
+							type="button"
+							onClick={() => onTabChange(tab.id)}
+							className={getSidebarHeaderTabButtonClassName({
+								isActive,
+								compact,
+							})}
+						>
+							{tab.icon && <tab.icon className="size-3" />}
+							{!compact && tab.label}
+						</button>
+					);
+
+					if (compact) {
+						return (
+							<Tooltip key={tab.id}>
+								<TooltipTrigger asChild>{btn}</TooltipTrigger>
+								<TooltipContent side="bottom" showArrow={false}>
+									{tab.label}
+								</TooltipContent>
+							</Tooltip>
+						);
+					}
+
+					return btn;
+				})}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/SidebarHeader/SidebarHeader.tsx
around lines 25 - 60, The code builds a button into the local variable btn but
then duplicates the same button in the non-compact return, causing drift and
duplicated edits; instead reuse btn for both branches. Change the btn
declaration in SidebarHeader.tsx to include the key prop (e.g., add key={tab.id}
to the button created in btn) and return btn directly in the non-compact branch
(remove the duplicated button JSX); keep using TooltipTrigger asChild and
TooltipContent for the compact branch, and ensure onTabChange,
getSidebarHeaderTabButtonClassName, tab.icon and tab.label remain unchanged.
.agents/commands/pr/create-pr.md (1)

65-70: Add a language to the gh pr create fenced block (MD040).

markdownlint is flagging the unlabeled fence. Use bash (or sh) to satisfy the rule and get proper highlighting.

📝 Proposed fix
-```
+```bash
 gh pr create \
   --base <defaultBranch> \
   --title "<title>" \
   --body "<body>"
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @.agents/commands/pr/create-pr.md around lines 65 - 70, The fenced code block
showing the gh pr create command is unlabeled and triggers markdownlint MD040;
update that code fence in .agents/commands/pr/create-pr.md by adding a language
tag (e.g., bash) to the opening triple-backtick so the block becomes ```bash and
provides proper syntax highlighting for the gh pr create snippet.


</details>

</blockquote></details>
<details>
<summary>apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx (1)</summary><blockquote>

`80-82`: **Unnecessary `as SidebarTabId` cast.**

`workspaceLocalStateSchema.sidebarState.activeTab` is a `z.enum(["changes","files","review"])` with a default, so `localState?.sidebarState?.activeTab` should already be typed as `"changes" | "files" | "review" | undefined` from the inferred row type. The `as SidebarTabId | undefined` assertion can be dropped, which restores actual type checking if the schema enum ever changes.

```diff
-	const activeTab: SidebarTabId =
-		(localState?.sidebarState?.activeTab as SidebarTabId | undefined) ??
-		"changes";
+	const activeTab: SidebarTabId =
+		localState?.sidebarState?.activeTab ?? "changes";
```

As per coding guidelines, "Avoid using `any` type; prefer explicit type safety in TypeScript."

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx
around lines 80 - 82, Remove the unnecessary type assertion on the activeTab
expression: drop the "as SidebarTabId | undefined" cast and let the inferred
type from localState?.sidebarState?.activeTab (the z.enum union) flow. Keep the
nullish coalescing to default to "changes" so the final declaration const
activeTab: SidebarTabId = localState?.sidebarState?.activeTab ?? "changes";
remains valid and benefits from actual schema-driven types; update the
WorkspaceSidebar's activeTab initialization accordingly.
```

</details>

</blockquote></details>
<details>
<summary>apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowState/usePRFlowState.ts (1)</summary><blockquote>

`35-55`: **Drop the type assertions on tRPC query outputs.**

`prQuery.data`, `prQuery.error`, and `syncQuery.error` are already typed by tRPC's inferred types. Casting them to `FlowPullRequest | null` / `Error | null` masks any future schema drift between `git.getPullRequest`'s server type and `FlowPullRequest`, and `prQuery.error` is a `TRPCClientErrorLike`, not a plain `Error` — the `as Error` cast claims a type the value may not satisfy.

Prefer either (a) defining `FlowPullRequest` as the inferred output of `git.getPullRequest` so no cast is needed, or (b) writing a small narrowing function that maps the tRPC payload into `FlowPullRequest`. For errors, accept `unknown`/`TRPCClientErrorLike` in `getPRFlowState`'s `loadError` field rather than asserting `Error`.

As per coding guidelines, "Avoid using `any` type; prefer explicit type safety in TypeScript."

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowState/usePRFlowState.ts
around lines 35 - 55, The current usePRFlowState hook uses unsafe casts
(prQuery.data as FlowPullRequest | null and prQuery.error as Error | null and
syncQuery.error as Error | null); remove these assertions and pass the raw tRPC
outputs (prQuery.data, prQuery.error, syncQuery.error) into getPRFlowState, and
either (a) change getPRFlowState's parameter types (e.g., allow pr?:
FlowPullRequest | undefined or the tRPC-inferred output type and change
loadError to unknown | TRPCClientErrorLike) or (b) add a small mapper function
(e.g., normalizePR and normalizeError) that explicitly converts/narrows tRPC
results into FlowPullRequest and an Error-like shape before calling
getPRFlowState; update references to getPRFlowState and usePRFlowState
accordingly so no `as` assertions remain.
```

</details>

</blockquote></details>
<details>
<summary>apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.ts (1)</summary><blockquote>

`71-91`: **Consider an `assertNever` default for forward-compat.**

`selectActionButton`, `selectStatusBadge`, and `getPRFromState` are all exhaustive over the current `PRFlowState` kinds, but adding new kinds (the plan enumerates 12+ post-PR variants for later phases) won't get a compile error here without an `assertNever(state)` default. Optional polish to catch future omissions at type-check time.



<details>
<summary>♻️ Sketch</summary>

```ts
function assertNever(x: never): never {
  throw new Error(`Unhandled PRFlowState: ${JSON.stringify(x)}`);
}
// then in each switch:
default:
  return assertNever(state);
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.ts
around lines 71 - 91, Add an assertNever guard to the exhaustive switches to
catch future PRFlowState variants: implement a helper assertNever(x: never):
never that throws, then add a default: return assertNever(state); to the switch
statements in selectActionButton as well as selectStatusBadge and getPRFromState
so the compiler will error if a new PRFlowState kind is added without handling
it.
```

</details>

</blockquote></details>
<details>
<summary>apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx (1)</summary><blockquote>

`82-195`: **Split `ActionButton` and `CreatePRSplitButton` into their own files.**

Per the repo guideline, each component lives in its own file (and ideally its own folder). `PRActionHeader.tsx` currently declares three components. Recommend extracting `ActionButton` and `CreatePRSplitButton` into co-located sibling folders (e.g., `PRActionHeader/components/ActionButton/ActionButton.tsx` + `index.ts`, same for `CreatePRSplitButton`).

As per coding guidelines: "Do not create multi-component files; use one component per file." and "Use one folder per component with structure: `ComponentName/ComponentName.tsx` + `index.ts` for barrel export."

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx
around lines 82 - 195, PRActionHeader currently defines multiple components in
one file; extract the ActionButton and CreatePRSplitButton components into their
own component folders/files per guideline. Create folders/components
ActionButton/ActionButton.tsx and CreatePRSplitButton/CreatePRSplitButton.tsx
(with index.ts barrel exports) that export the respective React components
(preserving props types: ActionButton expects variant, state, dispatch, onRetry,
onCancelBusy; CreatePRSplitButton expects state, dispatch), update
PRActionHeader.tsx to import these two components and remove their inline
definitions, and keep all JSX and event handlers identical so behavior is
unchanged.
```

</details>

</blockquote></details>
<details>
<summary>apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowDispatch/usePRFlowDispatch.ts (1)</summary><blockquote>

`75-81`: **Replace deprecated `unescape` with a modern UTF-8 → base64 approach.**

`unescape` is an Annex B legacy function and flagged as deprecated. The `unescape(encodeURIComponent(...))` pattern works but is not recommended. In the Electron renderer, use `TextEncoder` + `btoa` (natively available) for the browser path instead:

<details>
<summary>♻️ Suggested replacement</summary>

```diff
 function encodeAsDataUrl(content: string, mediaType: string): string {
-	const base64 =
-		typeof btoa === "function"
-			? btoa(unescape(encodeURIComponent(content)))
-			: Buffer.from(content, "utf-8").toString("base64");
+	let base64: string;
+	if (typeof btoa === "function") {
+		const bytes = new TextEncoder().encode(content);
+		let binary = "";
+		for (let i = 0; i < bytes.length; i++) {
+			binary += String.fromCharCode(bytes[i]);
+		}
+		base64 = btoa(binary);
+	} else {
+		base64 = Buffer.from(content, "utf-8").toString("base64");
+	}
 	return `data:${mediaType};base64,${base64}`;
 }
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowDispatch/usePRFlowDispatch.ts
around lines 75 - 81, The encodeAsDataUrl function uses the deprecated
unescape(encodeURIComponent(...)) pattern for UTF-8 → base64; replace that with
a modern approach: for the browser branch (when typeof btoa === "function") use
TextEncoder to encode content to a Uint8Array and convert that byte array to a
binary string (e.g., via String.fromCharCode.apply or a small loop) before
calling btoa, and keep the existing Buffer.from(content,
"utf-8").toString("base64") path for the non-browser branch; preserve the return
value format `data:${mediaType};base64,${base64}` and refer to encodeAsDataUrl
to implement the change.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @apps/desktop/plans/20260420-1045-agent-driven-pr-flow.md:

  • Around line 84-110: The fenced ASCII diagram block needs a language tag to
    satisfy markdownlint MD040; update the triple-backtick fence for the diagram
    (the block containing PRActionHeader, getPRFlowState, usePRFlowDispatch,
    buildPRContext, ensureChatPane, ChatLaunchConfig) from totext (or
    another appropriate tag) so the diagram is treated as plain text.

In
@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/buildPRContext/buildPRContext.ts:

  • Around line 60-67: The generated PR command uses sync.defaultBranch and emits
    a literal "" when null; in buildPRContext (the function that pushes
    lines via lines.push) change the logic so the gh pr create suggestion is only
    added when sync.defaultBranch is truthy (use sync.defaultBranch directly in the
    template), and when sync.defaultBranch is null/undefined replace that
    single-line suggestion with a clear guidance line (e.g., instruct to determine
    the base branch with gh repo view --json defaultBranchRef or to set the base
    manually) so the output never shows the literal "" and the "Base"
    unknown state remains consistent with the command guidance.

In
@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx:

  • Around line 151-155: The placeholder rendered when PR_ACTION_HEADER_ENABLED is
    false should be an invisible layout spacer rather than a visible muted/bordered
    bar: in the conditional that renders PRActionHeader (the PRActionHeader
    component controlled by PR_ACTION_HEADER_ENABLED), replace the div with one that
    only reserves space (e.g., className="h-12 shrink-0" or use "h-12 invisible" /
    "aria-hidden") and remove the border-b, border-border, bg-muted/45,
    dark:bg-muted/35, gap-2 and px-2 classes so it becomes a no-op whitespace spacer
    instead of a visible strip.

In
@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx:

  • Around line 222-231: The revealPath callback currently writes
    draft.rightSidebarOpen on collections.v2WorkspaceLocalState (via
    collections.v2WorkspaceLocalState.update) but the page now reads sidebarOpen
    from v2UserPreferences.rightSidebarOpen, so clicking reveal won't open the
    sidebar; change revealPath to call the user-preferences setter returned by
    useV2UserPreferences (instead of writing to v2WorkspaceLocalState) to set
    rightSidebarOpen = true and keep the existing setSelectedFilePath(path)
    behavior; locate the revealPath function and replace the
    v2WorkspaceLocalState.update write with the appropriate useV2UserPreferences
    setter call to toggle rightSidebarOpen.
  • Line 226: The update in revealPath calls
    v2WorkspaceLocalState.update(workspaceId, ...) and writes
    draft.sidebarState.activeTab without ensuring draft.sidebarState exists, which
    can throw if the row predates the schema; fix by either (A) adding a guard
    before the update—use collections.v2WorkspaceLocalState.get(workspaceId) and
    return early if falsy—or inside the updater ensure draft.sidebarState is
    initialized (e.g., draft.sidebarState = draft.sidebarState || { projectId: ...,
    tabOrder: 0, sectionId: null, changesFilter: { kind: "all" }, activeTab:
    "changes" }) before setting activeTab; alternatively (B) add .default({...}) to
    the sidebarState field in the v2WorkspaceLocalState schema so the parent object
    is always present. Target the revealPath handler, the call to
    v2WorkspaceLocalState.update, and the sidebarState definition in the
    v2WorkspaceLocalState schema when applying the change.

In
@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts:

  • Around line 42-43: Remove the dead rightSidebarOpen field from
    workspaceLocalStateSchema and stop writing to it in page.tsx: delete the
    rightSidebarOpen entry in workspaceLocalStateSchema (the
    z.boolean().default(false) line) and remove or update any assignments in
    page.tsx that set workspaceLocalState.rightSidebarOpen so that code uses
    v2UserPreferences.rightSidebarOpen exclusively; keep workspaceLocalStateSchema
    and page.tsx consistent to avoid orphaned writes.

Outside diff comments:
In
@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx:

  • Around line 122-155: usePRFlowState and usePRFlowDispatch are called
    unconditionally causing git.getPullRequest and git.getBranchSyncStatus to poll
    even when PR_ACTION_HEADER_ENABLED is false; either pass the flag into
    usePRFlowState (e.g., add an enabled parameter and forward it to both useQuery
    calls for git.getPullRequest and git.getBranchSyncStatus as enabled:
    PR_ACTION_HEADER_ENABLED && !!workspaceId) and short-circuit usePRFlowDispatch
    callers when the flag is false, or lift the PR_ACTION_HEADER_ENABLED check above
    the hooks and only call usePRFlowState/usePRFlowDispatch inside PRActionHeader
    (move hook calls into PRActionHeader) so polling only runs when the feature flag
    is true; reference usePRFlowState, usePRFlowDispatch, PRActionHeader,
    PR_ACTION_HEADER_ENABLED, workspaceId, git.getPullRequest, and
    git.getBranchSyncStatus when making the change.

Nitpick comments:
In @.agents/commands/pr/create-pr.md:

  • Around line 65-70: The fenced code block showing the gh pr create command is
    unlabeled and triggers markdownlint MD040; update that code fence in
    .agents/commands/pr/create-pr.md by adding a language tag (e.g., bash) to the
    opening triple-backtick so the block becomes ```bash and provides proper syntax
    highlighting for the gh pr create snippet.

In
@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx:

  • Around line 82-195: PRActionHeader currently defines multiple components in
    one file; extract the ActionButton and CreatePRSplitButton components into their
    own component folders/files per guideline. Create folders/components
    ActionButton/ActionButton.tsx and CreatePRSplitButton/CreatePRSplitButton.tsx
    (with index.ts barrel exports) that export the respective React components
    (preserving props types: ActionButton expects variant, state, dispatch, onRetry,
    onCancelBusy; CreatePRSplitButton expects state, dispatch), update
    PRActionHeader.tsx to import these two components and remove their inline
    definitions, and keep all JSX and event handlers identical so behavior is
    unchanged.

In
@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.ts:

  • Around line 71-91: Add an assertNever guard to the exhaustive switches to
    catch future PRFlowState variants: implement a helper assertNever(x: never):
    never that throws, then add a default: return assertNever(state); to the switch
    statements in selectActionButton as well as selectStatusBadge and getPRFromState
    so the compiler will error if a new PRFlowState kind is added without handling
    it.

In
@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/SidebarHeader/SidebarHeader.tsx:

  • Around line 25-60: The code builds a button into the local variable btn but
    then duplicates the same button in the non-compact return, causing drift and
    duplicated edits; instead reuse btn for both branches. Change the btn
    declaration in SidebarHeader.tsx to include the key prop (e.g., add key={tab.id}
    to the button created in btn) and return btn directly in the non-compact branch
    (remove the duplicated button JSX); keep using TooltipTrigger asChild and
    TooltipContent for the compact branch, and ensure onTabChange,
    getSidebarHeaderTabButtonClassName, tab.icon and tab.label remain unchanged.

In
@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowDispatch/usePRFlowDispatch.ts:

  • Around line 75-81: The encodeAsDataUrl function uses the deprecated
    unescape(encodeURIComponent(...)) pattern for UTF-8 → base64; replace that with
    a modern approach: for the browser branch (when typeof btoa === "function") use
    TextEncoder to encode content to a Uint8Array and convert that byte array to a
    binary string (e.g., via String.fromCharCode.apply or a small loop) before
    calling btoa, and keep the existing Buffer.from(content,
    "utf-8").toString("base64") path for the non-browser branch; preserve the return
    value format data:${mediaType};base64,${base64} and refer to encodeAsDataUrl
    to implement the change.

In
@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowState/usePRFlowState.ts:

  • Around line 35-55: The current usePRFlowState hook uses unsafe casts
    (prQuery.data as FlowPullRequest | null and prQuery.error as Error | null and
    syncQuery.error as Error | null); remove these assertions and pass the raw tRPC
    outputs (prQuery.data, prQuery.error, syncQuery.error) into getPRFlowState, and
    either (a) change getPRFlowState's parameter types (e.g., allow pr?:
    FlowPullRequest | undefined or the tRPC-inferred output type and change
    loadError to unknown | TRPCClientErrorLike) or (b) add a small mapper function
    (e.g., normalizePR and normalizeError) that explicitly converts/narrows tRPC
    results into FlowPullRequest and an Error-like shape before calling
    getPRFlowState; update references to getPRFlowState and usePRFlowState
    accordingly so no as assertions remain.

In
@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx:

  • Around line 80-82: Remove the unnecessary type assertion on the activeTab
    expression: drop the "as SidebarTabId | undefined" cast and let the inferred
    type from localState?.sidebarState?.activeTab (the z.enum union) flow. Keep the
    nullish coalescing to default to "changes" so the final declaration const
    activeTab: SidebarTabId = localState?.sidebarState?.activeTab ?? "changes";
    remains valid and benefits from actual schema-driven types; update the
    WorkspaceSidebar's activeTab initialization accordingly.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `d94e361b-1bc7-42f5-8620-9b734cc0eb61`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b6db24710d3932e80ed13cc0f281142b5fa79905 and dce8eab08f77e5859ac7fb78c2afda3eb23f6a58.

</details>

<details>
<summary>📒 Files selected for processing (25)</summary>

* `.agents/commands/pr/create-pr.md`
* `apps/desktop/plans/20260420-1045-agent-driven-pr-flow.md`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx`
* `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/components/PRActionHeader/PRActionHeader.tsx`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/index.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/buildPRContext/buildPRContext.test.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/buildPRContext/buildPRContext.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/buildPRContext/index.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.test.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/index.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/SidebarHeader/SidebarHeader.tsx`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/ChangesHeader.tsx`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowDispatch/index.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowDispatch/usePRFlowDispatch.test.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowDispatch/usePRFlowDispatch.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowState/index.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/usePRFlowState/usePRFlowState.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useReviewTab/useReviewTab.tsx`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx`
* `apps/desktop/src/renderer/routes/_authenticated/hooks/useMigrateV1DataToV2/writeSidebarState.ts`
* `apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts`
* `apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/headerTabStyles.ts`
* `packages/host-service/src/trpc/router/git/git.ts`

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* apps/desktop/src/renderer/routes/_authenticated/hooks/useMigrateV1DataToV2/writeSidebarState.ts

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +84 to +110
```
┌─────────────────────────────────────────┐
│ PRActionHeader (new) │ top of right sidebar
│ [#NNN ↗pr-link] [status] [▶action] │
└──────────────────┬──────────────────────┘
│ click
┌───────────────────────────────┐
│ getPRFlowState (pure) │ reducer over PR + branch + checks
└──────────────┬────────────────┘
┌───────────────────────────────┐
│ usePRFlowDispatch │
│ 1. build pr-context.md │ buildPRContext (pure)
│ 2. data-URL encode │
│ 3. ensureChatPane │ new tab OR reuse
│ 4. addTab / openPane with │
│ ChatLaunchConfig │
└──────────────┬────────────────┘
chat pane opens with
initialPrompt = "/<skill>"
initialFiles = [pr-context.md]
agent runs skill, calling existing
tRPC mutations + gh CLI as tools
```
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.

⚠️ Potential issue | 🟡 Minor

Add a language tag to the fenced code block.

markdownlint flagged MD040 on Line 84. Use text (or similar) for the ASCII architecture diagram.

📝 Diff
-```
+```text
 ┌─────────────────────────────────────────┐
 │ PRActionHeader (new)                    │   top of right sidebar
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 84-84: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/plans/20260420-1045-agent-driven-pr-flow.md` around lines 84 -
110, The fenced ASCII diagram block needs a language tag to satisfy markdownlint
MD040; update the triple-backtick fence for the diagram (the block containing
PRActionHeader, getPRFlowState, usePRFlowDispatch, buildPRContext,
ensureChatPane, ChatLaunchConfig) from ``` to ```text (or another appropriate
tag) so the diagram is treated as plain text.

Comment on lines +151 to +155
{PR_ACTION_HEADER_ENABLED ? (
<PRActionHeader state={flowState} dispatch={dispatch} onRetry={onRetry} />
) : (
<div className="flex h-12 shrink-0 items-center gap-2 border-b border-border bg-muted/45 px-2 dark:bg-muted/35" />
)}
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.

⚠️ Potential issue | 🟡 Minor

Empty 48px gated bar will visibly render as a styled strip.

When the flag is off, the placeholder still has border-b border-border bg-muted/45 dark:bg-muted/35 and gap-2 px-2. That's not a no-op preserve-layout spacer — it's a visible muted/bordered band at the top of every v2 workspace sidebar. If the only goal is to reserve 48px so toggling the flag doesn't shift content, drop the background/border on the placeholder so it reads as empty whitespace; if the band is intentional product UI, ignore.

-			) : (
-				<div className="flex h-12 shrink-0 items-center gap-2 border-b border-border bg-muted/45 px-2 dark:bg-muted/35" />
-			)}
+			) : (
+				<div aria-hidden className="h-12 shrink-0" />
+			)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{PR_ACTION_HEADER_ENABLED ? (
<PRActionHeader state={flowState} dispatch={dispatch} onRetry={onRetry} />
) : (
<div className="flex h-12 shrink-0 items-center gap-2 border-b border-border bg-muted/45 px-2 dark:bg-muted/35" />
)}
{PR_ACTION_HEADER_ENABLED ? (
<PRActionHeader state={flowState} dispatch={dispatch} onRetry={onRetry} />
) : (
<div aria-hidden className="h-12 shrink-0" />
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx
around lines 151 - 155, The placeholder rendered when PR_ACTION_HEADER_ENABLED
is false should be an invisible layout spacer rather than a visible
muted/bordered bar: in the conditional that renders PRActionHeader (the
PRActionHeader component controlled by PR_ACTION_HEADER_ENABLED), replace the
div with one that only reserves space (e.g., className="h-12 shrink-0" or use
"h-12 invisible" / "aria-hidden") and remove the border-b, border-border,
bg-muted/45, dark:bg-muted/35, gap-2 and px-2 classes so it becomes a no-op
whitespace spacer instead of a visible strip.

Comment on lines +42 to +43
rightSidebarOpen: z.boolean().default(false),
rightSidebarWidth: z.number().default(340),
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find readers/writers of `rightSidebarOpen` across the repo to confirm whether
# the per-workspace field is still consumed after pointing the reader at user prefs.
rg -nP --type=ts --type=tsx -C2 '\brightSidebarOpen\b'

Repository: superset-sh/superset

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Search for rightSidebarOpen in .ts and .tsx files across the repo
rg --type ts 'rightSidebarOpen' -C 2

Repository: superset-sh/superset

Length of output: 6102


🏁 Script executed:

#!/bin/bash
# Find and examine the schema files mentioned in the review
fd -e ts -e tsx | grep -E '(schema|workspaceLocalState|v2UserPreferences)' | head -20

Repository: superset-sh/superset

Length of output: 941


🏁 Script executed:

#!/bin/bash
# Look directly at the file in question and surrounding context
cat -n apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts | head -60

Repository: superset-sh/superset

Length of output: 2356


Remove unused rightSidebarOpen from workspaceLocalStateSchema.

rightSidebarOpen on workspaceLocalStateSchema is dead state: it's written to in page.tsx but never read. All actual reads use v2UserPreferences.rightSidebarOpen instead. The orphaned write should be removed, and the schema field deleted to avoid confusing future contributors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts`
around lines 42 - 43, Remove the dead rightSidebarOpen field from
workspaceLocalStateSchema and stop writing to it in page.tsx: delete the
rightSidebarOpen entry in workspaceLocalStateSchema (the
z.boolean().default(false) line) and remove or update any assignments in
page.tsx that set workspaceLocalState.rightSidebarOpen so that code uses
v2UserPreferences.rightSidebarOpen exclusively; keep workspaceLocalStateSchema
and page.tsx consistent to avoid orphaned writes.

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

7 issues found across 25 files

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=".agents/commands/pr/create-pr.md">

<violation number="1" location=".agents/commands/pr/create-pr.md:45">
P1: Unquoted dynamic Git refs in shell command templates allow shell injection via crafted branch names. Quote refs (and use `--` where applicable) before passing them to commands.</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:476">
P2: Do not swallow `git.status()` errors here; returning `hasUncommitted: false` on failure can misreport a dirty repo as clean.</violation>
</file>

<file name="apps/desktop/plans/20260420-1045-agent-driven-pr-flow.md">

<violation number="1" location="apps/desktop/plans/20260420-1045-agent-driven-pr-flow.md:222">
P3: The plan states there are 15 main states, but the defined state union/table include 16. This inconsistency can cause implementation and test coverage gaps.</violation>

<violation number="2" location="apps/desktop/plans/20260420-1045-agent-driven-pr-flow.md:366">
P3: The documented skill count is incorrect: the list contains 13 skills, not 12.</violation>

<violation number="3" location="apps/desktop/plans/20260420-1045-agent-driven-pr-flow.md:471">
P3: The manual test references an outdated button/skill name (`Publish & Create PR` and `/pr/publish-and-create`) that conflicts with the documented MVP flow (`Create PR` and `/pr/create-pr`).</violation>
</file>

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

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx:224">
P1: `revealPath` sets `rightSidebarOpen` on `v2WorkspaceLocalState`, but the sidebar renders based on `v2UserPreferences.rightSidebarOpen`. These are different stores, so "reveal in sidebar" will not actually open the sidebar. Use the `setRightSidebarOpen` setter from `useV2UserPreferences` instead.</violation>

<violation number="2" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx:454">
P2: `.split("/").pop()` won't extract the filename correctly on Windows paths that use backslashes. Use a cross-platform path utility or split on both separators.

(Based on your team's feedback about using cross-platform path utilities instead of split.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread .agents/commands/pr/create-pr.md Outdated
Comment thread packages/host-service/src/trpc/router/git/git.ts Outdated
}): PRFlowState;
```

15 main states, down from 33. Precedence (short-circuit in this order):
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 26, 2026

Choose a reason for hiding this comment

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

P3: The plan states there are 15 main states, but the defined state union/table include 16. This inconsistency can cause implementation and test coverage gaps.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/plans/20260420-1045-agent-driven-pr-flow.md, line 222:

<comment>The plan states there are 15 main states, but the defined state union/table include 16. This inconsistency can cause implementation and test coverage gaps.</comment>

<file context>
@@ -0,0 +1,521 @@
+}): PRFlowState;
+```
+
+15 main states, down from 33. Precedence (short-circuit in this order):
+
+1. `error` if `loadError` and no last-known data
</file context>
Suggested change
15 main states, down from 33. Precedence (short-circuit in this order):
16 main states, down from 33. Precedence (short-circuit in this order):
Fix with Cubic

a short body describing the goal and the allowed tool calls. The agent
consumes `pr-context.md` as an attachment.

Skills to write (12):
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 26, 2026

Choose a reason for hiding this comment

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

P3: The documented skill count is incorrect: the list contains 13 skills, not 12.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/plans/20260420-1045-agent-driven-pr-flow.md, line 366:

<comment>The documented skill count is incorrect: the list contains 13 skills, not 12.</comment>

<file context>
@@ -0,0 +1,521 @@
+a short body describing the goal and the allowed tool calls. The agent
+consumes `pr-context.md` as an attachment.
+
+Skills to write (12):
+
+- `pr/create-pr.md` — one skill for the whole pre-PR path (commit,
</file context>
Suggested change
Skills to write (12):
Skills to write (13):
Fix with Cubic

assert `addTab` called with the right `initialPrompt` and a
base64-decodable `pr-context.md`.
- **Manual:** unpublished branch → click **Publish & Create PR** →
verify chat pane opens with `/pr/publish-and-create` + attachment and
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 26, 2026

Choose a reason for hiding this comment

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

P3: The manual test references an outdated button/skill name (Publish & Create PR and /pr/publish-and-create) that conflicts with the documented MVP flow (Create PR and /pr/create-pr).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/plans/20260420-1045-agent-driven-pr-flow.md, line 471:

<comment>The manual test references an outdated button/skill name (`Publish & Create PR` and `/pr/publish-and-create`) that conflicts with the documented MVP flow (`Create PR` and `/pr/create-pr`).</comment>

<file context>
@@ -0,0 +1,521 @@
+  assert `addTab` called with the right `initialPrompt` and a
+  base64-decodable `pr-context.md`.
+- **Manual:** unpublished branch → click **Publish & Create PR** →
+  verify chat pane opens with `/pr/publish-and-create` + attachment and
+  the agent completes the flow.
+
</file context>
Fix with Cubic

Restores main's page.tsx (PR #3747 refactor) — the wip's inline
openFilePane stored unconverted paths and tripped workspace-fs's
ensureWithinRoot for callers that didn't pre-normalize. Re-adds the
pendingReveal prop and widens onSelectDiffFile on WorkspaceSidebar so
main's page.tsx props line up.

PRActionHeader now mirrors v1's PRButton: when a PR exists and is open,
the right side renders a link + chevron dropdown with squash / merge /
rebase, wired to workspaceTrpc.github.mergePR. After a successful merge,
triggers pullRequests.refreshByWorkspaces and refetches the local
queries so the UI reflects merged state immediately instead of waiting
for the 30s background sync. Closed/merged/draft PRs render the link
without the dropdown.

Gating: renamed PR_ACTION_HEADER_ENABLED → CREATE_PR_BUTTON_ENABLED to
reflect that only the create-PR dispatch is gated (chat doesn't exist
in v2 yet). The status group + merge dropdown always render.

Also: refresh button on the Changes tab that invalidates getStatus,
getDiff, listCommits, listBranches, and getBaseBranch in parallel and
spins while in flight. getPullRequest now returns repoOwner/repoName
(needed for the merge mutation).
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (6)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.test.ts (2)

202-208: Test name mentions error but only asserts busy.

The label says "during busy/error when PR known" but the assertion only covers kind: "busy". Consider adding an error case so the test matches its description, or tighten the name.

♻️ Optional addition
 	test("PR link still visible during busy/error when PR known", () => {
 		const p = pr();
 		expect(selectPRLink({ kind: "busy", pr: p })).toMatchObject({
 			kind: "pr-link",
 		});
+		expect(
+			selectPRLink({ kind: "error", pr: p, message: "x" }),
+		).toMatchObject({ kind: "pr-link" });
 	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.test.ts
around lines 202 - 208, The test named "PR link still visible during busy/error
when PR known" is only asserting the busy state; update the test for consistency
by either renaming it to reflect only the busy case or adding an assertion for
the error case: call selectPRLink with { kind: "error", pr: p } (reuse the
existing pr() fixture and the selectPRLink helper) and expect the result
toMatchObject({ kind: "pr-link" }) as well, so both busy and error states are
covered while keeping the original busy assertion.

210-246: Consider adding pull-only and pr-exists status-badge coverage.

The no-pr badge cases skip the pull-only path (e.g. pullCount: 2, pushCount: 0) which is a common branch alongside push-only and diverged. Additionally, selectStatusBadge for kind: "pr-exists" (and other non-no-pr kinds, if supported) isn't exercised here — worth a quick test or two if those branches exist in the implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.test.ts
around lines 210 - 246, Add missing unit tests for the "pull-only" branch and
for non-"no-pr" kinds: extend the test suite for selectStatusBadge by adding a
case where kind: "no-pr" with sync({ pullCount: 2, pushCount: 0 }) asserts the
expected "pulls to fetch" badge (or whatever the implementation returns for
pull-only), and add at least one test for kind: "pr-exists" (using the same
sync(...) helper) to assert the badge string returned for PR-present state;
locate tests around selectStatusBadge and the sync(...) factory to add these
assertions so the pull-only and pr-exists code-paths are covered.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx (2)

87-89: Use the existing isSidebarTabId guard when reading persisted activeTab.

The read path as SidebarTabId | undefined is an unchecked cast — if a stale persisted value (older "diffs"/"review" subtab name, or any string from a future schema migration) is on disk, activeTab will hold a value outside the SidebarTabId union, tabs.find will return undefined, and the sidebar body silently renders nothing. The setActiveTab writer already validates via isSidebarTabId; mirror that on the read so the union is actually enforced.

♻️ Suggested change
-	const activeTab: SidebarTabId =
-		(localState?.sidebarState?.activeTab as SidebarTabId | undefined) ??
-		"changes";
+	const persistedTab = localState?.sidebarState?.activeTab;
+	const activeTab: SidebarTabId =
+		persistedTab && isSidebarTabId(persistedTab) ? persistedTab : "changes";

As per coding guidelines: "Avoid using any type; prefer explicit type safety in TypeScript." The same intent applies to as casts that bypass the union check when a runtime guard is already available.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx
around lines 87 - 89, The current read casts localState?.sidebarState?.activeTab
to SidebarTabId unchecked; instead validate the persisted value with the
existing isSidebarTabId guard before using it so you never assign an
out-of-union string to activeTab. In the activeTab initialization (where
localState is read), call isSidebarTabId(localState?.sidebarState?.activeTab)
and if true use that value, otherwise fall back to "changes"; keep setActiveTab
unchanged (it already validates). This ensures tabs.find won't get an invalid
key and the sidebar body won't render empty.

129-132: onOpenChat ?? (() => {}) allocates a new no-op each render.

The inline fallback creates a fresh function identity on every render of WorkspaceSidebar. If usePRFlowDispatch memoizes its returned dispatch against onOpenChat (typical for a useCallback-style hook), that memoization is broken whenever the parent doesn't pass onOpenChat, defeating downstream React.memo/useEffect deps in PRActionHeader. Hoist a stable no-op:

♻️ Suggested change
+const NOOP_OPEN_CHAT: OpenChatFn = () => {};
+
 export function WorkspaceSidebar({
   ...
 }: WorkspaceSidebarProps) {
   ...
-	const dispatch = usePRFlowDispatch({
-		onOpenChat: onOpenChat ?? (() => {}),
-	});
+	const dispatch = usePRFlowDispatch({
+		onOpenChat: onOpenChat ?? NOOP_OPEN_CHAT,
+	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx
around lines 129 - 132, The inline fallback onOpenChat ?? (() => {}) creates a
new function identity each render and can break memoization in usePRFlowDispatch
and downstream components like PRActionHeader; fix by providing a stable no-op
instead: hoist a single stable no-op function (e.g., a module-level const NOOP
or a useCallback(() => {}, []) inside WorkspaceSidebar) and pass that stable
reference into usePRFlowDispatch when onOpenChat is undefined so dispatch
memoization remains valid.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx (1)

126-141: Consider functional setIsRefreshing and surfacing invalidation errors.

Two minor nits on handleRefresh:

  1. isRefreshing is in the dependency array purely for the early-return guard, which means handleRefresh's identity flips on every refresh cycle and re-creates the bound onClick. Reading it via a useRef (or just relying on disabled={isRefreshing} on the Button, which already prevents double-clicks here) lets you drop it from the deps.
  2. If any of the invalidate calls reject, the rejection escapes through void handleRefresh() as an unhandled rejection — the user sees the spinner stop but gets no feedback. Wrapping in a try/catch and surfacing via the existing toast (already imported) keeps refresh failures observable.
♻️ Suggested tweak
-	const [isRefreshing, setIsRefreshing] = useState(false);
-	const handleRefresh = useCallback(async () => {
-		if (isRefreshing) return;
-		setIsRefreshing(true);
-		try {
-			await Promise.all([
-				utils.git.getStatus.invalidate({ workspaceId }),
-				utils.git.getDiff.invalidate({ workspaceId }),
-				utils.git.listCommits.invalidate({ workspaceId }),
-				utils.git.listBranches.invalidate({ workspaceId }),
-				utils.git.getBaseBranch.invalidate({ workspaceId }),
-			]);
-		} finally {
-			setIsRefreshing(false);
-		}
-	}, [utils, workspaceId, isRefreshing]);
+	const [isRefreshing, setIsRefreshing] = useState(false);
+	const handleRefresh = useCallback(async () => {
+		setIsRefreshing(true);
+		try {
+			await Promise.all([
+				utils.git.getStatus.invalidate({ workspaceId }),
+				utils.git.getDiff.invalidate({ workspaceId }),
+				utils.git.listCommits.invalidate({ workspaceId }),
+				utils.git.listBranches.invalidate({ workspaceId }),
+				utils.git.getBaseBranch.invalidate({ workspaceId }),
+			]);
+		} catch (err) {
+			toast.error(
+				err instanceof Error ? err.message : "Failed to refresh changes",
+			);
+		} finally {
+			setIsRefreshing(false);
+		}
+	}, [utils, workspaceId]);

The disabled={isRefreshing} on the trigger Button already prevents concurrent invocations from the UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx
around lines 126 - 141, The handleRefresh function currently depends on
isRefreshing which flips identity on every refresh and can cause needless
re-renders; remove isRefreshing from the dependency array by reading it from a
ref or by relying on the Button's disabled={isRefreshing} and use the functional
form setIsRefreshing(prev => true/false) to update state. Also wrap the
Promise.all(...) invalidate calls in a try/catch inside handleRefresh and call
the existing toast to surface any error (e.g., toast.error(...)) before clearing
state so failures aren't swallowed; keep references to
utils.git.getStatus.invalidate, utils.git.getDiff.invalidate,
utils.git.listCommits.invalidate, utils.git.listBranches.invalidate, and
utils.git.getBaseBranch.invalidate when implementing this change.
packages/host-service/src/trpc/router/git/git.ts (1)

442-477: Optional: parallelize independent git operations.

currentBranch, defaultBranch, remotes, the upstream tracking probe, and status() are all independent. Running them sequentially can add noticeable latency on slower disks/large repos, especially since this query is likely invoked frequently to drive the PR header's badges. Consider Promise.all to issue them concurrently.

♻️ Suggested parallelization
-			const currentBranch = (
-				await git.revparse(["--abbrev-ref", "HEAD"]).catch(() => "")
-			).trim();
-			const isDetached = !currentBranch || currentBranch === "HEAD";
-
-			const defaultBranch = await getDefaultBranchName(git);
-			const isDefaultBranch =
-				!isDetached && !!defaultBranch && currentBranch === defaultBranch;
-
-			const remotes = await git.getRemotes(false).catch(() => []);
-			const hasRepo = remotes.length > 0;
-
-			let hasUpstream = false;
-			let pushCount = 0;
-			let pullCount = 0;
-			try {
-				await git.raw(["rev-parse", "--abbrev-ref", "@{upstream}"]);
-				hasUpstream = true;
-				const tracking = await git.raw([
-					"rev-list",
-					"--left-right",
-					"--count",
-					"@{upstream}...HEAD",
-				]);
-				const [pullStr, pushStr] = tracking.trim().split(/\s+/);
-				pullCount = Number.parseInt(pullStr || "0", 10);
-				pushCount = Number.parseInt(pushStr || "0", 10);
-			} catch {
-				// no upstream — counts stay zero
-			}
-
-			const status = await git.status().catch(() => null);
-			const hasUncommitted = status ? status.files.length > 0 : false;
+			const [currentBranchRaw, defaultBranch, remotes, tracking, status] =
+				await Promise.all([
+					git.revparse(["--abbrev-ref", "HEAD"]).catch(() => ""),
+					getDefaultBranchName(git),
+					git.getRemotes(false).catch(() => []),
+					git
+						.raw([
+							"rev-list",
+							"--left-right",
+							"--count",
+							"@{upstream}...HEAD",
+						])
+						.catch(() => null),
+					git.status().catch(() => null),
+				]);
+
+			const currentBranch = currentBranchRaw.trim();
+			const isDetached = !currentBranch || currentBranch === "HEAD";
+			const isDefaultBranch =
+				!isDetached && !!defaultBranch && currentBranch === defaultBranch;
+			const hasRepo = remotes.length > 0;
+
+			let hasUpstream = false;
+			let pullCount = 0;
+			let pushCount = 0;
+			if (tracking !== null) {
+				hasUpstream = true;
+				const [pullStr, pushStr] = tracking.trim().split(/\s+/);
+				pullCount = Number.parseInt(pullStr || "0", 10);
+				pushCount = Number.parseInt(pushStr || "0", 10);
+			}
+
+			const hasUncommitted = status ? status.files.length > 0 : false;

Note: this drops the explicit git rev-parse --abbrev-ref @{upstream} probe since rev-list already fails when no upstream is configured, which is sufficient to derive hasUpstream.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/host-service/src/trpc/router/git/git.ts` around lines 442 - 477,
Multiple independent git calls (git.revparse for currentBranch,
getDefaultBranchName(git), git.getRemotes, the upstream tracking rev-list, and
git.status) are run sequentially causing unnecessary latency; change the code to
run these in parallel with Promise.all: concurrently call
git.revparse(["--abbrev-ref","HEAD"]) to get currentBranch,
getDefaultBranchName(git) for defaultBranch, git.getRemotes(false) for remotes,
git.raw(["rev-list","--left-right","--count","@{upstream}...HEAD"]) for tracking
(drop the separate rev-parse upstream probe and derive hasUpstream from whether
rev-list succeeds), and git.status() for status; then compute isDetached,
isDefaultBranch, hasRepo, hasUpstream (based on whether rev-list threw),
pullCount/pushCount (parse rev-list output or default to 0), and hasUncommitted
from status, wrapping each promise with catch/defaults so failures of one do not
break others.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.test.ts:
- Around line 202-208: The test named "PR link still visible during busy/error
when PR known" is only asserting the busy state; update the test for consistency
by either renaming it to reflect only the busy case or adding an assertion for
the error case: call selectPRLink with { kind: "error", pr: p } (reuse the
existing pr() fixture and the selectPRLink helper) and expect the result
toMatchObject({ kind: "pr-link" }) as well, so both busy and error states are
covered while keeping the original busy assertion.
- Around line 210-246: Add missing unit tests for the "pull-only" branch and for
non-"no-pr" kinds: extend the test suite for selectStatusBadge by adding a case
where kind: "no-pr" with sync({ pullCount: 2, pushCount: 0 }) asserts the
expected "pulls to fetch" badge (or whatever the implementation returns for
pull-only), and add at least one test for kind: "pr-exists" (using the same
sync(...) helper) to assert the badge string returned for PR-present state;
locate tests around selectStatusBadge and the sync(...) factory to add these
assertions so the pull-only and pr-exists code-paths are covered.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx:
- Around line 126-141: The handleRefresh function currently depends on
isRefreshing which flips identity on every refresh and can cause needless
re-renders; remove isRefreshing from the dependency array by reading it from a
ref or by relying on the Button's disabled={isRefreshing} and use the functional
form setIsRefreshing(prev => true/false) to update state. Also wrap the
Promise.all(...) invalidate calls in a try/catch inside handleRefresh and call
the existing toast to surface any error (e.g., toast.error(...)) before clearing
state so failures aren't swallowed; keep references to
utils.git.getStatus.invalidate, utils.git.getDiff.invalidate,
utils.git.listCommits.invalidate, utils.git.listBranches.invalidate, and
utils.git.getBaseBranch.invalidate when implementing this change.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx:
- Around line 87-89: The current read casts localState?.sidebarState?.activeTab
to SidebarTabId unchecked; instead validate the persisted value with the
existing isSidebarTabId guard before using it so you never assign an
out-of-union string to activeTab. In the activeTab initialization (where
localState is read), call isSidebarTabId(localState?.sidebarState?.activeTab)
and if true use that value, otherwise fall back to "changes"; keep setActiveTab
unchanged (it already validates). This ensures tabs.find won't get an invalid
key and the sidebar body won't render empty.
- Around line 129-132: The inline fallback onOpenChat ?? (() => {}) creates a
new function identity each render and can break memoization in usePRFlowDispatch
and downstream components like PRActionHeader; fix by providing a stable no-op
instead: hoist a single stable no-op function (e.g., a module-level const NOOP
or a useCallback(() => {}, []) inside WorkspaceSidebar) and pass that stable
reference into usePRFlowDispatch when onOpenChat is undefined so dispatch
memoization remains valid.

In `@packages/host-service/src/trpc/router/git/git.ts`:
- Around line 442-477: Multiple independent git calls (git.revparse for
currentBranch, getDefaultBranchName(git), git.getRemotes, the upstream tracking
rev-list, and git.status) are run sequentially causing unnecessary latency;
change the code to run these in parallel with Promise.all: concurrently call
git.revparse(["--abbrev-ref","HEAD"]) to get currentBranch,
getDefaultBranchName(git) for defaultBranch, git.getRemotes(false) for remotes,
git.raw(["rev-list","--left-right","--count","@{upstream}...HEAD"]) for tracking
(drop the separate rev-parse upstream probe and derive hasUpstream from whether
rev-list succeeds), and git.status() for status; then compute isDetached,
isDefaultBranch, hasRepo, hasUpstream (based on whether rev-list threw),
pullCount/pushCount (parse rev-list output or default to 0), and hasUncommitted
from status, wrapping each promise with catch/defaults so failures of one do not
break others.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a3cae52-d5c1-483f-a800-c5008481dd97

📥 Commits

Reviewing files that changed from the base of the PR and between dce8eab and 442d024.

📒 Files selected for processing (5)
  • 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/components/PRActionHeader/PRActionHeader.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx
  • packages/host-service/src/trpc/router/git/git.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 6 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="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx:150">
P2: Handle refresh failures explicitly instead of discarding a potentially rejecting async call from the click handler.

(Based on your team's feedback about handling async rejections explicitly.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Restores the full-height v2 sidebar layout that was on the wip: the
sidebar now portals into the workspace-right-sidebar-slot div in
layout.tsx (next to TopBar) instead of rendering inside the page below
TopBar via ResizablePanelGroup.

Width is persisted on v2UserPreferences.rightSidebarWidth (new field,
default 340) with a setter on useV2UserPreferences. The renderer's
explicit-width ResizablePanel handles drag-resize and double-click reset.
Resize dragging is forwarded to useBrowserShellInteractionPassthrough so
the browser pane keeps ignoring clicks during resize.

The workspace area no longer needs the resizable panel group since it's
the only thing in the page now — it expands to fill flex-1, and the
sidebar floats next to it via portal.

Also drops the unused commitCount prop on ChangesHeader (caught by lint
after the wip's refactor stopped using it).
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx (1)

199-213: Extract the default sidebar width to a shared constant.

340 appears as both the fallback (Line 203) and the double-click reset target (Line 287). If the default ever shifts, the two sites can drift. Promote it to a named constant (and consider re-using it from useV2UserPreferences if the same default lives there) so the fallback and reset always agree.

♻️ Suggested refactor
+const DEFAULT_RIGHT_SIDEBAR_WIDTH = 340;
+
@@
-	const sidebarWidth = v2UserPreferences.rightSidebarWidth ?? 340;
+	const sidebarWidth =
+		v2UserPreferences.rightSidebarWidth ?? DEFAULT_RIGHT_SIDEBAR_WIDTH;
@@
-						onDoubleClickHandle={() => setRightSidebarWidth(340)}
+						onDoubleClickHandle={() =>
+							setRightSidebarWidth(DEFAULT_RIGHT_SIDEBAR_WIDTH)
+						}

Also applies to: 276-301

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/page.tsx
around lines 199 - 213, The hardcoded default sidebar width (340) is duplicated;
extract it into a shared constant (e.g., DEFAULT_RIGHT_SIDEBAR_WIDTH) and
replace the literal in the fallback for v2UserPreferences.rightSidebarWidth
(used to compute sidebarWidth) and the double-click reset logic so both sites
use the same named value; if useV2UserPreferences already exports a default
width, reuse that symbol instead of introducing a new one to avoid divergence.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx (1)

209-249: Reuse the existing selectors instead of recomputing PR + link state.

getPRFlowState.ts already exports selectPRLink(state) which returns { state, number, url } derived from the same pr.isDraft / pr.state ladder you re-implement here at lines 243–249. The PR-extraction at lines 209–214 also re-implements the private getPRFromState helper from that file. Drift between the two copies (e.g., a future PR lifecycle value) will silently desync the link rendering from selectStatusBadge. Consider consuming selectPRLink here (and exporting getPRFromState if you still need raw PR fields like repoOwner/repoName/url for the merge call).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx
around lines 209 - 249, Replace the local PR extraction and linkState ladder
with the shared selector: stop re-implementing getPRFromState and the
pr.isDraft/pr.state logic and instead call selectPRLink(state) to derive the
link info (and use its .state/.url/.number as needed); if you still need raw PR
fields for mergePRMutation (repoOwner/repoName/url), export and import
getPRFromState from getPRFlowState.ts and use that to obtain the raw PR, then
pass those fields into mergePRMutation—this ensures PR link rendering
(previously computed into linkState) and status badge logic remain consistent
with selectPRLink/selectStatusBadge.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx:
- Around line 171-193: CreatePRSplitButton currently only dispatches a non-draft
create and misleads by naming; either implement the split behavior or rename:
Option A — implement a true split button by adding a secondary
trigger/chevron/menu in CreatePRSplitButton that dispatches dispatch({ state,
draft: true }) for the draft option while keeping the primary button dispatch({
state, draft: false }), and ensure the menu/trigger UI matches existing
tooltip/ARIA patterns; Option B — rename CreatePRSplitButton to CreatePRButton
and update all call sites to use the new name, leaving the single-button
dispatch({ state, draft: false }) as-is; in both cases keep PRFlowDispatch and
PRFlowState usage consistent and adjust tests/refs to the component name or new
draft dispatch as appropriate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.ts:
- Around line 149-158: The helper _unavailableTooltip in getPRFlowState.ts is a
duplicate of unavailableTooltip in PRActionHeader.tsx; export the shared tooltip
function from getPRFlowState (keep the UnavailableReason signature and strings
in sync with unavailableBadge/enum), remove the duplicate local copy in
PRActionHeader.tsx, import the exported tooltip function there, and handle the
extra "create-disabled" case that existed only in the PRActionHeader caller
(e.g., map "create-disabled" to an appropriate UnavailableReason or handle it
before calling unavailableTooltip).

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/page.tsx:
- Around line 218-221: The flash happens because sidebarSlotEl is initialized
null and populated only in useEffect, so the conditional that renders the portal
(sidebarOpen && sidebarSlotEl && createPortal(...)) is false on first paint; fix
by initializing sidebarSlotEl synchronously (use useState(() =>
document.getElementById("workspace-right-sidebar-slot") as HTMLElement | null)
instead of null and removing the setSidebarSlotEl useEffect) or, if you prefer a
post-mount approach, switch the effect to useLayoutEffect to populate
sidebarSlotEl before paint; update references to sidebarSlotEl,
setSidebarSlotEl, useState/useEffect (or useLayoutEffect) and ensure
createPortal conditional relies on the synchronous lookup.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx:
- Around line 209-249: Replace the local PR extraction and linkState ladder with
the shared selector: stop re-implementing getPRFromState and the
pr.isDraft/pr.state logic and instead call selectPRLink(state) to derive the
link info (and use its .state/.url/.number as needed); if you still need raw PR
fields for mergePRMutation (repoOwner/repoName/url), export and import
getPRFromState from getPRFlowState.ts and use that to obtain the raw PR, then
pass those fields into mergePRMutation—this ensures PR link rendering
(previously computed into linkState) and status badge logic remain consistent
with selectPRLink/selectStatusBadge.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/page.tsx:
- Around line 199-213: The hardcoded default sidebar width (340) is duplicated;
extract it into a shared constant (e.g., DEFAULT_RIGHT_SIDEBAR_WIDTH) and
replace the literal in the fallback for v2UserPreferences.rightSidebarWidth
(used to compute sidebarWidth) and the double-click reset logic so both sites
use the same named value; if useV2UserPreferences already exports a default
width, reuse that symbol instead of introducing a new one to avoid divergence.
🪄 Autofix (Beta)

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

Run ID: b3d44ed2-2c55-407f-a9b0-b22191ef2eb7

📥 Commits

Reviewing files that changed from the base of the PR and between 7e775ca and 1105396.

📒 Files selected for processing (5)
  • 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/components/PRActionHeader/PRActionHeader.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useReviewTab/components/ReviewTabContent/ReviewTabContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useReviewTab/components/ReviewTabContent/ReviewTabContent.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx

Comment on lines +149 to +158
function _unavailableTooltip(reason: UnavailableReason): string {
switch (reason) {
case "no-repo":
return "No GitHub repository connected";
case "default-branch":
return "Switch to a feature branch to create a pull request";
case "detached-head":
return "Checkout a branch to create a pull request";
}
}
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.

🛠️ Refactor suggestion | 🟠 Major

_unavailableTooltip is dead code and is a byte-for-byte duplicate of unavailableTooltip in PRActionHeader.tsx (lines 156–169).

The leading underscore signals "intentionally unused", but the identical function actually lives in the consumer file. This looks like a copy that got orphaned during the cherry-pick — pick one of:

♻️ Option A — export from here, drop the copy in PRActionHeader.tsx
-function _unavailableTooltip(reason: UnavailableReason): string {
+export function unavailableTooltip(reason: UnavailableReason): string {
 	switch (reason) {
 		case "no-repo":
 			return "No GitHub repository connected";
 		case "default-branch":
 			return "Switch to a feature branch to create a pull request";
 		case "detached-head":
 			return "Checkout a branch to create a pull request";
 	}
 }

Then in PRActionHeader.tsx, import it and delete the local copy; the "create-disabled" branch can be handled in the caller (it's the only extra case the local version had).

♻️ Option B — just delete the unused helper
-function _unavailableTooltip(reason: UnavailableReason): string {
-	switch (reason) {
-		case "no-repo":
-			return "No GitHub repository connected";
-		case "default-branch":
-			return "Switch to a feature branch to create a pull request";
-		case "detached-head":
-			return "Checkout a branch to create a pull request";
-	}
-}

Option A is preferable since the strings are intended to stay in lockstep with unavailableBadge/the unavailable-reason enum.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function _unavailableTooltip(reason: UnavailableReason): string {
switch (reason) {
case "no-repo":
return "No GitHub repository connected";
case "default-branch":
return "Switch to a feature branch to create a pull request";
case "detached-head":
return "Checkout a branch to create a pull request";
}
}
// _unavailableTooltip function removed
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/getPRFlowState/getPRFlowState.ts
around lines 149 - 158, The helper _unavailableTooltip in getPRFlowState.ts is a
duplicate of unavailableTooltip in PRActionHeader.tsx; export the shared tooltip
function from getPRFlowState (keep the UnavailableReason signature and strings
in sync with unavailableBadge/enum), remove the duplicate local copy in
PRActionHeader.tsx, import the exported tooltip function there, and handle the
extra "create-disabled" case that existed only in the PRActionHeader caller
(e.g., map "create-disabled" to an appropriate UnavailableReason or handle it
before calling unavailableTooltip).

PR badge now carries two ambient indicators next to the number — a CI
rollup dot (success/failure/pending, circle icons) and a review decision
dot (approved/changes-requested/pending, user icons). Distinct icon
families so they read as "build" vs "people" at 12px. Suppressed for
closed/merged PRs (post-state CI is noise) and for PRs with no checks
or no review requested (quiet for trivial PRs).

Hovering the link now opens a 320px card with the title (line-clamped),
state pill, branch, full check rollup ("8 of 10 passing"), review
status, "Updated 2h ago", and a "View on GitHub" link. Pulled in via
@superset/ui/hover-card so animations + portal placement come for free.

Extracted computeChecksRollup + coerceCheckStatus to a shared util so
useReviewTab and the new dots draw from the same source — keeps the
sidebar tab and the badge from drifting on edge cases.

Co-located the new pieces under PRActionHeader/components/PRStatusGroup
following the project's per-component-folder convention.
…ch-changes-into-current-branc

# Conflicts:
#	apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx
#	apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (6)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx (2)

69-117: Add an exhaustiveness check on the variant.kind switch.

ActionSlot has no explicit return type and no default arm. If a new ActionButtonVariant.kind is added later (e.g. a future merge-conflict state), this switch will silently fall through and the slot will render undefined — easy to miss in review. A never-typed default makes it a build error:

-		case "retry":
-			return (
-				<button …>…</button>
-			);
-	}
+		case "retry":
+			return (
+				<button …>…</button>
+			);
+		default: {
+			const _exhaustive: never = variant;
+			return _exhaustive;
+		}
+	}

The same pattern would benefit unavailableTooltip (Lines 140-153).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx
around lines 69 - 117, The switch over variant.kind in PRActionHeader must be
made exhaustive: add a final default case that narrows to never (e.g., const
_exhaustiveCheck: never = variant; throw new Error(...)) so the compiler errors
if a new ActionButtonVariant.kind is added; apply the same pattern to the
unavailableTooltip switch to force a compile-time exhaustiveness check. Locate
the switch block that returns
PRStatusGroup/CreatePRIconButton/UnavailableIcon/etc. and the unavailableTooltip
switch and add the never-typed default assertion with a clear error message to
fail the build when an unhandled variant.kind appears.

120-177: Extract UnavailableIcon and CreatePRIconButton into their own files.

Both are real React components defined alongside PRActionHeader in the same file. They should live under their own component folders.

As per coding guidelines: "Do not create multi-component files; use one component per file."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx
around lines 120 - 177, Move the UnavailableIcon component (and its helper
unavailableTooltip) and the CreatePRIconButton component into separate
files/components and update imports where used: create e.g. UnavailableIcon.tsx
exporting the UnavailableIcon component (export default) and exporting or
keeping unavailableTooltip private inside that file, and CreatePRIconButton.tsx
exporting CreatePRIconButton; ensure both new files import the required types
(UnavailableReason, PRFlowState, PRFlowDispatch) and UI primitives (Tooltip,
TooltipTrigger, TooltipContent, VscGitPullRequest) used in the original file,
then replace the inline definitions in PRActionHeader.tsx with imports of
UnavailableIcon and CreatePRIconButton. Ensure props/signatures remain identical
and update any relative import paths accordingly.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRDetailCard/PRDetailCard.tsx (2)

102-222: Split sibling components (ChecksLine, ReviewLine, DetailLine) into their own files.

These are exported-style React components living in PRDetailCard.tsx. Promote each into its own folder under PRDetailCard/components/<Name>/<Name>.tsx (with an index.ts); stateLabelToPillClass can stay co-located as a util but the components shouldn't share a file.

As per coding guidelines: "Do not create multi-component files; use one component per file." and "Use one folder per component with structure: ComponentName/ComponentName.tsx + index.ts for barrel export."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRDetailCard/PRDetailCard.tsx
around lines 102 - 222, The file currently contains three sibling components
(ChecksLine, ReviewLine, DetailLine) inside PRDetailCard.tsx; split each into
its own folder under PRDetailCard/components: create folders
ChecksLine/ChecksLine.tsx + index.ts, ReviewLine/ReviewLine.tsx + index.ts, and
DetailLine/DetailLine.tsx + index.ts, move the corresponding function
implementation into each file, export the component as default (or named) from
its index, and update PRDetailCard.tsx to import the three components from their
new barrel index files; keep any shared util like stateLabelToPillClass
co-located or in a util file but ensure no multi-component file remains.

27-34: stateLabel re-derives what linkState already encodes.

linkState is passed in and is already the result of the same isDraft / pr.state ladder computed by PRStatusGroup. Map directly off linkState so the two callsites can't drift:

-	const stateLabel = pr.isDraft
-		? "Draft"
-		: pr.state === "merged"
-			? "Merged"
-			: pr.state === "closed"
-				? "Closed"
-				: "Open";
+	const stateLabel = LINK_STATE_LABELS[linkState];

with a small Record<PRState, string> constant. (See related comment on PRStatusGroup Lines 84-92.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRDetailCard/PRDetailCard.tsx
around lines 27 - 34, PRDetailCard currently recomputes stateLabel from
pr.isDraft/pr.state which duplicates logic encoded by the incoming linkState;
stop re-deriving and map directly from linkState using a small Record mapping.
Replace the stateLabel calculation in PRDetailCard with a const Record<PRState,
string> (e.g., STATUS_LABELS) and derive stateLabel = STATUS_LABELS[linkState];
keep statePillClass = stateLabelToPillClass(linkState) as-is so both label and
pill use the same source of truth (reference symbols: PRDetailCard, stateLabel,
linkState, statePillClass, stateLabelToPillClass, PRState).
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRStatusIndicators/PRStatusIndicators.tsx (1)

41-93: Split ChecksDot and ReviewDot into their own files.

ChecksDot and ReviewDot are defined alongside PRStatusIndicators in the same file. Per the repo's component guideline these should each live in their own folder with a barrel export.

As per coding guidelines: "Do not create multi-component files; use one component per file." and "Use one folder per component with structure: ComponentName/ComponentName.tsx + index.ts for barrel export."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRStatusIndicators/PRStatusIndicators.tsx
around lines 41 - 93, Extract the two subcomponents ChecksDot and ReviewDot from
PRStatusIndicators.tsx into their own component folders (ChecksDot/ChecksDot.tsx
+ ChecksDot/index.ts and ReviewDot/ReviewDot.tsx + ReviewDot/index.ts) with
default exports, preserving their prop types (ChecksDot({ status }: { status:
ChecksRollup["overall"] }) and ReviewDot({ decision }: { decision:
NonNullable<PullRequest["reviewDecision"]> }). Replace the inline definitions in
PRStatusIndicators.tsx with imports from the new barrels (import ChecksDot from
'./ChecksDot' and import ReviewDot from './ReviewDot') and ensure any
classnames, icons (LuCircleCheck, LuCircleX, LuCircleDashed, LuUserCheck,
LuUserX, LuUserMinus), and aria attributes are kept identical so behavior and
types remain unchanged.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/PRStatusGroup.tsx (1)

84-92: Duplicate state-derivation between PRStatusGroup and PRDetailCard.

The pr.isDraft → "draft" / pr.state → "merged"|"closed"|"open" ladder is computed here and again as stateLabel inside PRDetailCard. Since linkState is already passed into PRDetailCard, derive the human label there from linkState (e.g. a small linkStateToLabel map) so both places stay in sync if a new state is ever added.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/PRStatusGroup.tsx
around lines 84 - 92, PRStatusGroup currently computes linkState
(draft/merged/closed/open) and PRDetailCard recomputes the same ladder into
stateLabel; instead, keep the existing linkState from PRStatusGroup and remove
the duplicate derivation in PRDetailCard by mapping linkState → human label
there (e.g. add a linkStateToLabel map inside PRDetailCard and derive stateLabel
= linkStateToLabel[linkState] with a sensible default). Update PRDetailCard to
accept and use the linkState prop (instead of recomputing from
pr.isDraft/pr.state) so both components stay in sync; reference PRStatusGroup,
PRDetailCard, linkState, and stateLabel when making changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/PRStatusGroup.tsx:
- Around line 64-75: The onSuccess handler shows the "PR merged" toast then
calls refreshPRMutation.mutateAsync without catching rejections, which can leave
the UI stale; update the onSuccess for the merge mutation so the call to
refreshPRMutation.mutateAsync({ workspaceIds: [workspaceId] }) is wrapped in
try/catch (keeping the current finally that calls onRefresh?.()), and in the
catch log the error and show a softer user-facing message (e.g., toast.error or
toast.info like "Couldn't refresh PR state") so refresh failures are surfaced to
users while still preserving the original success flow; reference the onSuccess
function, refreshPRMutation.mutateAsync, workspaceId and onRefresh in your
change.
- Around line 91-101: Add a persistent mergeable state and use it in the UI: add
a new column to the pullRequests table to store MergeableState ("mergeable" |
"conflicting" | "unknown"), update your DB migration and ORM model, populate
that column when fetching PR details from GitHub in getPullRequest (use the
API's mergeable field), and persist it. Then update the frontend canMerge logic
in PRStatusGroup (replace the current canMerge check) to require pr.state ===
"open" && !pr.isDraft && pr.mergeable === "mergeable" so the merge button (and
handleMerge/mergePRMutation path) is disabled/unavailable for "conflicting" or
non-mergeable states. Ensure the MergeableState type is used consistently across
getPullRequest, the DB model, and the PRStatusGroup component.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRDetailCard/PRDetailCard.tsx:
- Around line 102-222: The file currently contains three sibling components
(ChecksLine, ReviewLine, DetailLine) inside PRDetailCard.tsx; split each into
its own folder under PRDetailCard/components: create folders
ChecksLine/ChecksLine.tsx + index.ts, ReviewLine/ReviewLine.tsx + index.ts, and
DetailLine/DetailLine.tsx + index.ts, move the corresponding function
implementation into each file, export the component as default (or named) from
its index, and update PRDetailCard.tsx to import the three components from their
new barrel index files; keep any shared util like stateLabelToPillClass
co-located or in a util file but ensure no multi-component file remains.
- Around line 27-34: PRDetailCard currently recomputes stateLabel from
pr.isDraft/pr.state which duplicates logic encoded by the incoming linkState;
stop re-deriving and map directly from linkState using a small Record mapping.
Replace the stateLabel calculation in PRDetailCard with a const Record<PRState,
string> (e.g., STATUS_LABELS) and derive stateLabel = STATUS_LABELS[linkState];
keep statePillClass = stateLabelToPillClass(linkState) as-is so both label and
pill use the same source of truth (reference symbols: PRDetailCard, stateLabel,
linkState, statePillClass, stateLabelToPillClass, PRState).

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRStatusIndicators/PRStatusIndicators.tsx:
- Around line 41-93: Extract the two subcomponents ChecksDot and ReviewDot from
PRStatusIndicators.tsx into their own component folders (ChecksDot/ChecksDot.tsx
+ ChecksDot/index.ts and ReviewDot/ReviewDot.tsx + ReviewDot/index.ts) with
default exports, preserving their prop types (ChecksDot({ status }: { status:
ChecksRollup["overall"] }) and ReviewDot({ decision }: { decision:
NonNullable<PullRequest["reviewDecision"]> }). Replace the inline definitions in
PRStatusIndicators.tsx with imports from the new barrels (import ChecksDot from
'./ChecksDot' and import ReviewDot from './ReviewDot') and ensure any
classnames, icons (LuCircleCheck, LuCircleX, LuCircleDashed, LuUserCheck,
LuUserX, LuUserMinus), and aria attributes are kept identical so behavior and
types remain unchanged.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/PRStatusGroup.tsx:
- Around line 84-92: PRStatusGroup currently computes linkState
(draft/merged/closed/open) and PRDetailCard recomputes the same ladder into
stateLabel; instead, keep the existing linkState from PRStatusGroup and remove
the duplicate derivation in PRDetailCard by mapping linkState → human label
there (e.g. add a linkStateToLabel map inside PRDetailCard and derive stateLabel
= linkStateToLabel[linkState] with a sensible default). Update PRDetailCard to
accept and use the linkState prop (instead of recomputing from
pr.isDraft/pr.state) so both components stay in sync; reference PRStatusGroup,
PRDetailCard, linkState, and stateLabel when making changes.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx:
- Around line 69-117: The switch over variant.kind in PRActionHeader must be
made exhaustive: add a final default case that narrows to never (e.g., const
_exhaustiveCheck: never = variant; throw new Error(...)) so the compiler errors
if a new ActionButtonVariant.kind is added; apply the same pattern to the
unavailableTooltip switch to force a compile-time exhaustiveness check. Locate
the switch block that returns
PRStatusGroup/CreatePRIconButton/UnavailableIcon/etc. and the unavailableTooltip
switch and add the never-typed default assertion with a clear error message to
fail the build when an unhandled variant.kind appears.
- Around line 120-177: Move the UnavailableIcon component (and its helper
unavailableTooltip) and the CreatePRIconButton component into separate
files/components and update imports where used: create e.g. UnavailableIcon.tsx
exporting the UnavailableIcon component (export default) and exporting or
keeping unavailableTooltip private inside that file, and CreatePRIconButton.tsx
exporting CreatePRIconButton; ensure both new files import the required types
(UnavailableReason, PRFlowState, PRFlowDispatch) and UI primitives (Tooltip,
TooltipTrigger, TooltipContent, VscGitPullRequest) used in the original file,
then replace the inline definitions in PRActionHeader.tsx with imports of
UnavailableIcon and CreatePRIconButton. Ensure props/signatures remain identical
and update any relative import paths accordingly.
🪄 Autofix (Beta)

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

Run ID: d5cc6bbe-46d3-4be6-900d-93d8a7c2bde7

📥 Commits

Reviewing files that changed from the base of the PR and between 1105396 and 1bc4c6d.

📒 Files selected for processing (10)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/PRActionHeader.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/PRStatusGroup.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRDetailCard/PRDetailCard.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRDetailCard/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRStatusIndicators/PRStatusIndicators.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRStatusIndicators/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/computeChecksStatus/computeChecksStatus.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/computeChecksStatus/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useReviewTab/useReviewTab.tsx
✅ Files skipped from review due to trivial changes (4)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRDetailCard/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/utils/computeChecksStatus/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRStatusIndicators/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useReviewTab/useReviewTab.tsx

Comment on lines +91 to +101
const canMerge = pr.state === "open" && !pr.isDraft;
const showIndicators = pr.state === "open"; // includes draft

const handleMerge = (mergeMethod: "merge" | "squash" | "rebase") => {
mergePRMutation.mutate({
owner: pr.repoOwner,
repo: pr.repoName,
pullNumber: pr.number,
mergeMethod,
});
};
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the PullRequest type definition that PRStatusGroup consumes.
fd -t f 'getPRFlowState' apps/desktop/src
ast-grep --pattern 'export type PullRequest = $_'
ast-grep --pattern 'type PullRequest = $_'
# Look for mergeable-related fields on PR types/router schemas.
rg -nP --type=ts -C2 '\bmergeable(State|Status)?\b'

Repository: superset-sh/superset

Length of output: 3385


🏁 Script executed:

cat -n packages/host-service/src/trpc/router/git/git.ts | sed -n '520,560p'

Repository: superset-sh/superset

Length of output: 1673


🏁 Script executed:

cat -n packages/host-service/src/trpc/router/git/types.ts | sed -n '40,75p'

Repository: superset-sh/superset

Length of output: 1131


🏁 Script executed:

cat -n apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/\$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/PRStatusGroup.tsx | sed -n '1,30p'

Repository: superset-sh/superset

Length of output: 1236


🏁 Script executed:

# Check if mergeable field is being populated from GitHub API or hardcoded
rg -n "mergeable" packages/host-service/src/trpc/router/git/git.ts -B5 -A5

# Check the full context of the getPullRequest procedure
cat -n packages/host-service/src/trpc/router/git/git.ts | sed -n '475,550p'

Repository: superset-sh/superset

Length of output: 3185


🏁 Script executed:

# Check if mergeable is ever populated from GitHub API elsewhere in the codebase
rg -n "mergeable" packages/host-service/src --type ts -B3 -A3

# Check the pullRequests schema to see if mergeable is stored
fd -t f "schema" packages/host-service/src --type ts | xargs grep -l "pullRequests\|migration"

# Look for where PR data is fetched from GitHub
rg -n "github|graphql" packages/host-service/src/trpc/router/git/git.ts -i | head -20

Repository: superset-sh/superset

Length of output: 1789


🏁 Script executed:

# Check the database schema for pullRequests table
find packages/host-service/src -name "*.ts" -type f | xargs grep -l "pullRequests\s*:" | head -5

# Look for where pullRequests table is defined
rg -n "pullRequests.*=" packages/host-service/src --type=ts -B2 -A5 | head -60

# Check if mergeable field exists in the database
rg -n "mergeable" packages/host-service/src -C3

Repository: superset-sh/superset

Length of output: 2815


🏁 Script executed:

# Get the full pullRequests table schema
cat -n packages/host-service/src/db/schema.ts | sed -n '49,100p'

Repository: superset-sh/superset

Length of output: 1922


🏁 Script executed:

# Check how mergePRMutation handles failures and displays errors
rg -n "mergePRMutation\|mergePR" apps/desktop/src -B3 -A3 | head -40

# Look for the merge mutation definition and error handling
rg -n "mergePR" packages/host-service/src/trpc/router -B5 -A10

Repository: superset-sh/superset

Length of output: 1415


🏁 Script executed:

# Get the full mergePR mutation implementation
cat -n packages/host-service/src/trpc/router/github/github.ts | sed -n '134,180p'

Repository: superset-sh/superset

Length of output: 692


Merge button doesn't account for merge conflicts or non-mergeable state — but the current architecture doesn't track this information.

The review comment's suggested fix won't work as intended: pr.mergeable is hardcoded to "unknown" in the router (at getPullRequest) and is never populated from GitHub or stored in the database. Checking pr.mergeable !== false would always evaluate to true and have no effect.

However, the underlying concern is valid. When a user clicks merge on a PR with conflicts, failing checks, or blocked status, the mutation will fail with a generic error from GitHub rather than preventing the action upfront.

To address this properly:

  • Add a mergeable column to the pullRequests table schema
  • Fetch and store the mergeable state from GitHub's API when fetching PR details
  • Update canMerge logic to check the actual mergeable state

The current code structure has this field type defined (MergeableState: "mergeable" | "conflicting" | "unknown") but never uses it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/PRStatusGroup.tsx
around lines 91 - 101, Add a persistent mergeable state and use it in the UI:
add a new column to the pullRequests table to store MergeableState ("mergeable"
| "conflicting" | "unknown"), update your DB migration and ORM model, populate
that column when fetching PR details from GitHub in getPullRequest (use the
API's mergeable field), and persist it. Then update the frontend canMerge logic
in PRStatusGroup (replace the current canMerge check) to require pr.state ===
"open" && !pr.isDraft && pr.mergeable === "mergeable" so the merge button (and
handleMerge/mergePRMutation path) is disabled/unavailable for "conflicting" or
non-mergeable states. Ensure the MergeableState type is used consistently across
getPullRequest, the DB model, and the PRStatusGroup component.

… first

PR badge now wears the PR state color: open is emerald-tinted (border +
bg + divider), merged violet, closed rose, draft muted. Tinting the
whole bordered group makes state legible at a glance without the icon
having to carry it alone.

Dropped the review-decision dot and the "Review" line in the hover
detail card — review noise was redundant with the Review tab's own
indicator and made the badge feel busy. Only the CI checks dot remains
next to the PR number.

Sidebar tab order is now Files → Changes → Review (Files first).
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 4 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="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRStatusIndicators/PRStatusIndicators.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PRActionHeader/components/PRStatusGroup/components/PRStatusIndicators/PRStatusIndicators.tsx:14">
P2: Review status is no longer rendered when a PR has no checks, so approved/changes-requested state can be hidden entirely.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

* reported so the row stays quiet for trivial PRs.
*/
export function PRStatusIndicators({ checks }: PRStatusIndicatorsProps) {
if (checks.overall === "none") return null;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 27, 2026

Choose a reason for hiding this comment

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

P2: Review status is no longer rendered when a PR has no checks, so approved/changes-requested state can be hidden entirely.

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/components/PRActionHeader/components/PRStatusGroup/components/PRStatusIndicators/PRStatusIndicators.tsx, line 14:

<comment>Review status is no longer rendered when a PR has no checks, so approved/changes-requested state can be hidden entirely.</comment>

<file context>
@@ -1,40 +1,22 @@
-	const hasReview = reviewDecision !== null;
-	if (!hasChecks && !hasReview) return null;
+export function PRStatusIndicators({ checks }: PRStatusIndicatorsProps) {
+	if (checks.overall === "none") return null;
 
 	return (
</file context>
Fix with Cubic

Bot reviews flagged a mix of stale concerns (already addressed in earlier
commits) and live issues. Fixing the live ones:

- page.tsx: restore min-w-[320px] on the workspace area so dragging the
  sidebar wider can't collapse the editor pane to unusable width (lost
  when we portaled the sidebar out of the resizable panel group)
- page.tsx: initialize sidebarSlotEl synchronously via useState init so
  users with rightSidebarOpen=true persisted don't see a 1-frame flash
  on mount; the slot is mounted by the parent layout before this child
  renders, so a synchronous lookup is safe
- PRStatusGroup: catch refresh failures after a successful merge —
  previously the rejection escaped the async onSuccess silently and
  left the UI stale. Now logs and shows a soft toast
- useChangesTab: catch refresh failures from the Changes-tab refresh
  button — previously void-discarded
- git.ts getBranchSyncStatus: log when git.status() fails instead of
  silently misreporting a dirty repo as clean. Defaults to false on
  failure with a comment explaining why over-reporting on every transient
  blip would be more annoying than briefly under-reporting

Cleanup:
- Drop dead _unavailableTooltip helper in getPRFlowState (byte-for-byte
  copy of the one in PRActionHeader, orphaned during cherry-pick)
- Drop dead rightSidebarOpen + rightSidebarWidth from
  workspaceLocalStateSchema — both moved to v2UserPreferences and the
  per-workspace fields are written by nobody and read by nobody

Dead-code-but-harmless-to-fix-now (will matter once create-PR ships):
- Replace deprecated unescape() with TextEncoder for UTF-8 base64 in
  usePRFlowDispatch.encodeAsDataUrl
- buildPRContext: stop emitting literal `<default>` when defaultBranch
  is null; instruct the agent to resolve via `gh repo view` instead
- create-pr.md: quote refs in `git push -u origin -- "<branch>"` and the
  `<base>..HEAD` ranges to prevent shell injection on crafted names
@AviPeltz AviPeltz merged commit 864a81c into main Apr 27, 2026
14 checks passed
MocA-Love added a commit to MocA-Love/superset that referenced this pull request Apr 28, 2026
upstream merge 2026-04-27 PR-D5: PR action header (superset-sh#3777, scope-limited)
@Kitenite Kitenite deleted the pull-diff-editor-branch-changes-into-current-branc branch May 6, 2026 04:52
MocA-Love added a commit to MocA-Love/superset that referenced this pull request May 8, 2026
…umn doesn't eat main content

upstream superset-sh#3777 added a portal slot div with className 'flex h-full shrink-0' as a sibling of the main content row inside fork's flex-col layout. With no consumer in fork yet, the empty div still claimed 100% height (h-full) of the column and shrink-0 prevented it from yielding, collapsing the main content area to 0px and producing a black screen with only the topbar visible.

Switch to className 'contents' so the empty slot is invisible to layout; future portals can override or wrap their own positioning.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant