feat(desktop): add customizable PR link provider setting#2333
feat(desktop): add customizable PR link provider setting#2333chasemcdo wants to merge 3 commits into
Conversation
Allow users to open PR links in alternative code review tools (BetterHub, Devin) instead of GitHub, with a custom domain option. Transforms PR URLs at render/open time while keeping raw GitHub URLs as the source of truth.
📝 WalkthroughWalkthroughAdds a PR Link Provider feature: DB schema and types, TRPC get/set procedures, a URL transform utility, settings UI and search entry, and integration into PR-open flows to apply provider-based URL transformation. Changes
Sequence DiagramsequenceDiagram
actor User
participant SettingsUI as Settings UI
participant Client as PR Flow / UI
participant TRPC as Settings Router
participant DB as Database
User->>SettingsUI: select provider / enter custom domain
SettingsUI->>TRPC: setPrLinkProvider(provider, customDomain)
TRPC->>DB: upsert settings (pr_link_provider, pr_link_custom_domain)
DB-->>TRPC: ok
TRPC-->>SettingsUI: success
User->>Client: click "Open PR" / press hotkey
Client->>TRPC: getPrLinkProvider()
TRPC->>DB: read settings
DB-->>TRPC: provider, customDomain
TRPC-->>Client: provider, customDomain
Client->>Client: transformPrUrl(githubUrl, provider, customDomain)
Client->>Client: window.open(transformedUrl)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
3 issues found across 13 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="apps/desktop/src/renderer/utils/pr-url/pr-url.ts">
<violation number="1" location="apps/desktop/src/renderer/utils/pr-url/pr-url.ts:20">
P2: Use exact URL origin/hostname validation instead of `startsWith` to avoid transforming non-GitHub domains that share the same prefix.</violation>
</file>
<file name="packages/local-db/src/schema/schema.ts">
<violation number="1" location="packages/local-db/src/schema/schema.ts:212">
P1: New `settings` columns were added in schema without a corresponding SQL migration, which will break existing local DBs at runtime.</violation>
</file>
<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx">
<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx:389">
P2: Add `noopener,noreferrer` when opening PR links from the hotkey to prevent opener access from external pages.</violation>
</file>
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Add one-off context when rerunning by tagging
@cubic-dev-aiwith guidance or docs links (includingllms.txt) - Ask questions if you need clarification on any suggestion
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| worktreeBaseDir: text("worktree_base_dir"), | ||
| openLinksInApp: integer("open_links_in_app", { mode: "boolean" }), | ||
| defaultEditor: text("default_editor").$type<ExternalApp>(), | ||
| prLinkProvider: text("pr_link_provider").$type<PrLinkProvider>(), |
There was a problem hiding this comment.
P1: New settings columns were added in schema without a corresponding SQL migration, which will break existing local DBs at runtime.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/local-db/src/schema/schema.ts, line 212:
<comment>New `settings` columns were added in schema without a corresponding SQL migration, which will break existing local DBs at runtime.</comment>
<file context>
@@ -208,6 +209,8 @@ export const settings = sqliteTable("settings", {
worktreeBaseDir: text("worktree_base_dir"),
openLinksInApp: integer("open_links_in_app", { mode: "boolean" }),
defaultEditor: text("default_editor").$type<ExternalApp>(),
+ prLinkProvider: text("pr_link_provider").$type<PrLinkProvider>(),
+ prLinkCustomDomain: text("pr_link_custom_domain"),
});
</file context>
| if (!githubUrl.includes("/pull/")) return githubUrl; | ||
|
|
||
| const githubBase = "https://github.com"; | ||
| if (!githubUrl.startsWith(githubBase)) return githubUrl; |
There was a problem hiding this comment.
P2: Use exact URL origin/hostname validation instead of startsWith to avoid transforming non-GitHub domains that share the same prefix.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/utils/pr-url/pr-url.ts, line 20:
<comment>Use exact URL origin/hostname validation instead of `startsWith` to avoid transforming non-GitHub domains that share the same prefix.</comment>
<file context>
@@ -0,0 +1,29 @@
+ if (!githubUrl.includes("/pull/")) return githubUrl;
+
+ const githubBase = "https://github.com";
+ if (!githubUrl.startsWith(githubBase)) return githubUrl;
+
+ if (provider === "custom") {
</file context>
| window.open( | ||
| transformPrUrl(pr.url, prProvider, prCustomDomain), | ||
| "_blank", | ||
| ); |
There was a problem hiding this comment.
P2: Add noopener,noreferrer when opening PR links from the hotkey to prevent opener access from external pages.
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/workspace/$workspaceId/page.tsx, line 389:
<comment>Add `noopener,noreferrer` when opening PR links from the hotkey to prevent opener access from external pages.</comment>
<file context>
@@ -376,17 +378,24 @@ function WorkspacePage() {
() => {
if (pr?.url) {
- window.open(pr.url, "_blank");
+ window.open(
+ transformPrUrl(pr.url, prProvider, prCustomDomain),
+ "_blank",
</file context>
| window.open( | |
| transformPrUrl(pr.url, prProvider, prCustomDomain), | |
| "_blank", | |
| ); | |
| window.open( | |
| transformPrUrl(pr.url, prProvider, prCustomDomain), | |
| "_blank", | |
| "noopener,noreferrer", | |
| ); |
Use transformPrUrl in WorkspaceStatusBadge and WorkspaceHoverCard so clicking the PR badge or "View on …" button respects the user's chosen PR link provider (BetterHub, Devin, custom, etc.).
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx`:
- Around line 46-49: The code is defaulting prProvider to
DEFAULT_PR_LINK_PROVIDER before the query settles; update the
electronTrpc.settings.getPrLinkProvider.useQuery() usage to account for
loading/error state (e.g., check isLoading/isFetching or data === undefined) and
avoid falling back to DEFAULT_PR_LINK_PROVIDER until prLinkSettings is defined
or you have a synchronous hydrate source; modify the logic around
prLinkSettings/prProvider in WorkspaceHoverCard (references:
electronTrpc.settings.getPrLinkProvider.useQuery(), prLinkSettings, prProvider,
DEFAULT_PR_LINK_PROVIDER) so the PR link is either not rendered (or disabled)
while settings are loading or is sourced from a synchronous settings
cache/hydration to prevent briefly rendering a GitHub link for users of
BetterHub/Devin/custom.
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceStatusBadge.tsx`:
- Around line 24-27: The badge's click handler assumes prLinkSettings
immediately, causing DEFAULT_PR_LINK_PROVIDER to be used while the query is
unresolved; update WorkspaceStatusBadge (and similar components: PRButton,
WorkspaceHoverCard, useCreateOrOpenPR) to check the query loading/state (e.g.,
useQuery's isLoading/isFetching or whether prLinkSettings is defined) and
disable or no-op the click until prLinkSettings is available (or show a loading
state), rather than using DEFAULT_PR_LINK_PROVIDER; alternatively, hydrate the
query at app load so prLinkSettings is defined before the badge is interactive.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5905c94f-c041-4b9f-bd5f-7fa31481462c
📒 Files selected for processing (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceStatusBadge.tsxapps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx
| const { data: prLinkSettings } = | ||
| electronTrpc.settings.getPrLinkProvider.useQuery(); | ||
| const prProvider = prLinkSettings?.provider ?? DEFAULT_PR_LINK_PROVIDER; | ||
| const prCustomDomain = prLinkSettings?.customDomain; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n "apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx"Repository: superset-sh/superset
Length of output: 7968
The PR link can temporarily default to GitHub while settings are loading, allowing users to click through to the wrong provider.
Lines 46-47 don't capture the loading state from getPrLinkProvider, so prProvider falls back to "github" before the query settles. When the PR data loads first (lines 179-189), the link renders immediately with this default, creating a window where users who configured BetterHub/Devin/custom can still be sent to GitHub. Either gate the PR link's rendering until prLinkSettings is available, or hydrate this value from a synchronously available settings source.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx`
around lines 46 - 49, The code is defaulting prProvider to
DEFAULT_PR_LINK_PROVIDER before the query settles; update the
electronTrpc.settings.getPrLinkProvider.useQuery() usage to account for
loading/error state (e.g., check isLoading/isFetching or data === undefined) and
avoid falling back to DEFAULT_PR_LINK_PROVIDER until prLinkSettings is defined
or you have a synchronous hydrate source; modify the logic around
prLinkSettings/prProvider in WorkspaceHoverCard (references:
electronTrpc.settings.getPrLinkProvider.useQuery(), prLinkSettings, prProvider,
DEFAULT_PR_LINK_PROVIDER) so the PR link is either not rendered (or disabled)
while settings are loading or is sourced from a synchronous settings
cache/hydration to prevent briefly rendering a GitHub link for users of
BetterHub/Devin/custom.
| const { data: prLinkSettings } = | ||
| electronTrpc.settings.getPrLinkProvider.useQuery(); | ||
| const prProvider = prLinkSettings?.provider ?? DEFAULT_PR_LINK_PROVIDER; | ||
| const prCustomDomain = prLinkSettings?.customDomain; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Router and persistence paths for PR link settings:"
rg -n -C3 '\bgetPrLinkProvider\b|\bsetPrLinkProvider\b|pr_link_provider|pr_link_custom_domain' apps/desktop/src
echo
echo "All query consumers:"
rg -n -C3 'getPrLinkProvider\.useQuery\(' apps/desktop/src
echo
echo "Any prefetch/initial-data/hydration for this query:"
rg -n -C3 'initialData|placeholderData|prefetch|prefetchQuery|ensureQueryData|hydrate' apps/desktop/srcRepository: superset-sh/superset
Length of output: 48122
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== tRPC client configuration and QueryClient setup ==="
fd -e ts -e tsx 'providers|ElectronTRPC|QueryClient' apps/desktop/src/renderer --type f | head -20 | xargs grep -l 'QueryClient\|prefetch\|defaultOptions' 2>/dev/null | head -5
echo
echo "=== ElectronTRPCProvider setup (checking for default behavior) ==="
cat apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx | head -100
echo
echo "=== Root layout or app initialization (checking for prefetch on startup) ==="
rg -n 'getPrLinkProvider' apps/desktop/src/renderer/routes --type ts --type tsx
echo
echo "=== Check if settings query is called during app initialization ==="
rg -n -B3 -A3 'useEffect|useLayoutEffect' apps/desktop/src/renderer/providers --type ts --type tsx | grep -A5 -B5 'getPrLink\|settings' || echo "No settings query in providers"Repository: superset-sh/superset
Length of output: 1328
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Root layout files ==="
find apps/desktop/src/renderer -name 'layout.*' -o -name 'root.*' -o -name '_app.*' -o -name 'page.tsx' | grep -E '(root|_app|layout)' | head -10
echo
echo "=== Check main app entry point ==="
cat apps/desktop/src/renderer/routes/_app.tsx 2>/dev/null | head -150 || cat apps/desktop/src/renderer/routes/layout.tsx 2>/dev/null | head -150 || echo "No main layout found at common paths"
echo
echo "=== Check if getPrLinkProvider query is prefetched anywhere in loaders ==="
rg 'getPrLinkProvider' apps/desktop/src/renderer/routes -A2 -B2
echo
echo "=== Check the specific component tree for WorkspaceStatusBadge ==="
cat apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceStatusBadge.tsx | head -100Repository: superset-sh/superset
Length of output: 5166
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Full WorkspaceStatusBadge component to verify click gating ==="
cat apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceStatusBadge.tsx
echo
echo "=== Check other components using getPrLinkProvider for comparison ==="
echo "--- PRButton.tsx ---"
cat apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ChangesHeader/components/PRButton/PRButton.tsx | grep -A20 'getPrLinkProvider' | head -30
echo
echo "--- WorkspaceHoverCard.tsx ---"
cat apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx | grep -A20 'getPrLinkProvider' | head -30Repository: superset-sh/superset
Length of output: 4134
Gate the click handler until the settings query is resolved.
On the initial render, prLinkSettings is undefined, so prProvider falls back to DEFAULT_PR_LINK_PROVIDER (GitHub). If the user clicks before the query resolves, this badge opens GitHub even when they've saved BetterHub, Devin, or a custom domain. Either gate the click until data is defined, or hydrate this query with initial data on app load instead of treating the loading state as "GitHub".
This pattern affects multiple components using getPrLinkProvider.useQuery() without checking the loading state (PRButton, WorkspaceHoverCard, useCreateOrOpenPR hook). Only GitSettings properly checks isPrLinkProviderLoading.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceStatusBadge.tsx`
around lines 24 - 27, The badge's click handler assumes prLinkSettings
immediately, causing DEFAULT_PR_LINK_PROVIDER to be used while the query is
unresolved; update WorkspaceStatusBadge (and similar components: PRButton,
WorkspaceHoverCard, useCreateOrOpenPR) to check the query loading/state (e.g.,
useQuery's isLoading/isFetching or whether prLinkSettings is defined) and
disable or no-op the click until prLinkSettings is available (or show a loading
state), rather than using DEFAULT_PR_LINK_PROVIDER; alternatively, hydrate the
query at app load so prLinkSettings is defined before the badge is interactive.
There was a problem hiding this comment.
Devin Review found 1 potential issue.
🐛 1 issue in files not directly in the diff
🐛 Missed transformPrUrl application in CommitInput's handleOpenPR (apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/CommitInput/CommitInput.tsx:147)
The PR's intent is to transform all PR URLs through the new transformPrUrl utility based on the user's PR link provider setting. However, CommitInput.tsx:147 still opens the raw GitHub PR URL directly via window.open(prUrl, "_blank") without applying the transform. Every other site that opens a PR URL in the diff was updated (page.tsx, WorkspaceStatusBadge, WorkspaceHoverCard, PRButton, useCreateOrOpenPR), but this one was missed. When a user has configured a non-GitHub PR link provider (e.g., BetterHub, Devin, or custom), clicking "Open PR" from the commit input area will still navigate to GitHub instead of the configured provider.
View 4 additional findings in Devin Review.
Summary
Test plan
https://www.better-hub.com/owner/repo/pull/123https://app.devin.ai/review/owner/repo/pull/123bun run typecheckandbun run lint:fixpassSummary by cubic
Adds a PR Link Provider setting so users can open pull requests in GitHub (default), BetterHub, Devin, or a custom domain. All PR links and shortcuts now honor this setting without changing stored GitHub URLs.
New Features
getPrLinkProvider,setPrLinkProvider) andtransformPrUrlutility.Migration
settings.pr_link_providerandsettings.pr_link_custom_domaincolumns.Written for commit e7367f8. Summary will update on new commits.
Summary by CodeRabbit