Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
EXTERNAL_APPS,
FILE_OPEN_MODES,
NON_EDITOR_APPS,
PR_LINK_PROVIDERS,
settings,
TERMINAL_LINK_BEHAVIORS,
type TerminalPreset,
Expand All @@ -22,6 +23,7 @@ import {
DEFAULT_CONFIRM_ON_QUIT,
DEFAULT_FILE_OPEN_MODE,
DEFAULT_OPEN_LINKS_IN_APP,
DEFAULT_PR_LINK_PROVIDER,
DEFAULT_SHOW_PRESETS_BAR,
DEFAULT_SHOW_RESOURCE_MONITOR,
DEFAULT_TERMINAL_LINK_BEHAVIOR,
Expand Down Expand Up @@ -502,6 +504,41 @@ export const createSettingsRouter = () => {
return { success: true };
}),

getPrLinkProvider: publicProcedure.query(() => {
const row = getSettings();
return {
provider: row.prLinkProvider ?? DEFAULT_PR_LINK_PROVIDER,
customDomain: row.prLinkCustomDomain ?? null,
};
}),

setPrLinkProvider: publicProcedure
.input(
z.object({
provider: z.enum(PR_LINK_PROVIDERS),
customDomain: z.string().nullable().optional(),
}),
)
.mutation(({ input }) => {
localDb
.insert(settings)
.values({
id: 1,
prLinkProvider: input.provider,
prLinkCustomDomain: input.customDomain ?? null,
})
.onConflictDoUpdate({
target: settings.id,
set: {
prLinkProvider: input.provider,
prLinkCustomDomain: input.customDomain ?? null,
},
})
.run();

return { success: true };
}),

getBranchPrefix: publicProcedure.query(() => {
const row = getSettings();
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
useHasWorkspaceFailed,
useIsWorkspaceInitializing,
} from "renderer/stores/workspace-init";
import { transformPrUrl } from "renderer/utils/pr-url";
import { DEFAULT_PR_LINK_PROVIDER } from "shared/constants";

const EMPTY_HISTORY_STACK: string[] = [];

Expand Down Expand Up @@ -376,17 +378,24 @@ function WorkspacePage() {
const { createOrOpenPR } = useCreateOrOpenPR({
worktreePath: workspace?.worktreePath,
});
const { data: prLinkSettings } =
electronTrpc.settings.getPrLinkProvider.useQuery();
const prProvider = prLinkSettings?.provider ?? DEFAULT_PR_LINK_PROVIDER;
const prCustomDomain = prLinkSettings?.customDomain;
useAppHotkey(
"OPEN_PR",
() => {
if (pr?.url) {
window.open(pr.url, "_blank");
window.open(
transformPrUrl(pr.url, prProvider, prCustomDomain),
"_blank",
);
Comment on lines +389 to +392
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 10, 2026

Choose a reason for hiding this comment

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

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>
Suggested change
window.open(
transformPrUrl(pr.url, prProvider, prCustomDomain),
"_blank",
);
window.open(
transformPrUrl(pr.url, prProvider, prCustomDomain),
"_blank",
"noopener,noreferrer",
);
Fix with Cubic

} else {
createOrOpenPR();
}
},
undefined,
[pr?.url, createOrOpenPR],
[pr?.url, createOrOpenPR, prProvider, prCustomDomain],
);

const commandPalette = useCommandPalette({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BranchPrefixMode } from "@superset/local-db";
import type { BranchPrefixMode, PrLinkProvider } from "@superset/local-db";
import { Input } from "@superset/ui/input";
import { Label } from "@superset/ui/label";
import {
Expand All @@ -17,6 +17,7 @@ import {
WorktreeLocationPicker,
} from "../../../components/WorktreeLocationPicker";
import { BRANCH_PREFIX_MODE_LABELS } from "../../../utils/branch-prefix";
import { PR_LINK_PROVIDER_LABELS } from "../../../utils/pr-link-provider";
import {
isItemVisible,
SETTING_ITEM_ID,
Expand All @@ -36,6 +37,10 @@ export function GitSettings({ visibleItems }: GitSettingsProps) {
SETTING_ITEM_ID.GIT_BRANCH_PREFIX,
visibleItems,
);
const showPrLinkProvider = isItemVisible(
SETTING_ITEM_ID.GIT_PR_LINK_PROVIDER,
visibleItems,
);
const showWorktreeLocation = isItemVisible(
SETTING_ITEM_ID.GIT_WORKTREE_LOCATION,
visibleItems,
Expand Down Expand Up @@ -107,6 +112,44 @@ export function GitSettings({ visibleItems }: GitSettingsProps) {
});
};

const { data: prLinkProvider, isLoading: isPrLinkProviderLoading } =
electronTrpc.settings.getPrLinkProvider.useQuery();

const [customDomainInput, setCustomDomainInput] = useState(
prLinkProvider?.customDomain ?? "",
);

useEffect(() => {
setCustomDomainInput(prLinkProvider?.customDomain ?? "");
}, [prLinkProvider?.customDomain]);

const setPrLinkProvider = electronTrpc.settings.setPrLinkProvider.useMutation(
{
onError: (err) => {
console.error("[settings/pr-link-provider] Failed to update:", err);
},
onSettled: () => {
utils.settings.getPrLinkProvider.invalidate();
},
},
);

const handlePrLinkProviderChange = (provider: PrLinkProvider) => {
setPrLinkProvider.mutate({
provider,
customDomain: customDomainInput || null,
});
};

const handleCustomDomainBlur = () => {
const trimmed = customDomainInput.trim().replace(/\/+$/, "");
setCustomDomainInput(trimmed);
setPrLinkProvider.mutate({
provider: "custom",
customDomain: trimmed || null,
});
};

const { data: worktreeBaseDir, isLoading: isWorktreeBaseDirLoading } =
electronTrpc.settings.getWorktreeBaseDir.useQuery();
const setWorktreeBaseDir =
Expand Down Expand Up @@ -228,6 +271,56 @@ export function GitSettings({ visibleItems }: GitSettingsProps) {
</div>
)}

{showPrLinkProvider && (
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-sm font-medium">PR Link Provider</Label>
<p className="text-xs text-muted-foreground">
Choose where pull request links open
</p>
</div>
<div className="flex items-center gap-2">
<Select
value={prLinkProvider?.provider ?? "github"}
onValueChange={(value) =>
handlePrLinkProviderChange(value as PrLinkProvider)
}
disabled={
isPrLinkProviderLoading || setPrLinkProvider.isPending
}
>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{(
Object.entries(PR_LINK_PROVIDER_LABELS) as [
PrLinkProvider,
string,
][]
).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
{prLinkProvider?.provider === "custom" && (
<Input
placeholder="https://example.com"
value={customDomainInput}
onChange={(e) => setCustomDomainInput(e.target.value)}
onBlur={handleCustomDomainBlur}
className="w-[200px]"
disabled={
isPrLinkProviderLoading || setPrLinkProvider.isPending
}
/>
)}
</div>
</div>
)}

{showWorktreeLocation && (
<div className="space-y-0.5">
<Label className="text-sm font-medium">Worktree location</Label>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PR_LINK_PROVIDER_LABELS } from "./pr-link-provider";
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { PrLinkProvider } from "@superset/local-db";

export const PR_LINK_PROVIDER_LABELS: Record<PrLinkProvider, string> = {
github: "GitHub",
betterhub: "BetterHub",
devin: "Devin",
custom: "Custom",
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const SETTING_ITEM_ID = {
BEHAVIOR_OPEN_LINKS_IN_APP: "behavior-open-links-in-app",

GIT_BRANCH_PREFIX: "git-branch-prefix",
GIT_PR_LINK_PROVIDER: "git-pr-link-provider",
GIT_DELETE_LOCAL_BRANCH: "git-delete-local-branch",
GIT_WORKTREE_LOCATION: "git-worktree-location",

Expand Down Expand Up @@ -355,6 +356,26 @@ export const SETTINGS_ITEMS: SettingsItem[] = [
"unsaved",
],
},
{
id: SETTING_ITEM_ID.GIT_PR_LINK_PROVIDER,
section: "git",
title: "PR Link Provider",
description: "Choose where pull request links open",
keywords: [
"git",
"pr",
"pull request",
"link",
"provider",
"github",
"betterhub",
"devin",
"custom",
"review",
"code review",
"url",
],
},
{
id: SETTING_ITEM_ID.GIT_DELETE_LOCAL_BRANCH,
section: "git",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { cn } from "@superset/ui/utils";
import { LuCircleDot, LuGitMerge, LuGitPullRequest } from "react-icons/lu";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { transformPrUrl } from "renderer/utils/pr-url";
import { DEFAULT_PR_LINK_PROVIDER } from "shared/constants";
import { STROKE_WIDTH } from "../constants";

type PRState = "open" | "merged" | "closed" | "draft";
Expand All @@ -19,6 +21,10 @@ export function WorkspaceStatusBadge({
className,
}: WorkspaceStatusBadgeProps) {
const openUrl = electronTrpc.external.openUrl.useMutation();
const { data: prLinkSettings } =
electronTrpc.settings.getPrLinkProvider.useQuery();
const prProvider = prLinkSettings?.provider ?? DEFAULT_PR_LINK_PROVIDER;
const prCustomDomain = prLinkSettings?.customDomain;
Comment on lines +24 to +27
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
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/src

Repository: 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 -100

Repository: 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 -30

Repository: 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.

const iconClass = "w-3 h-3";

const config = {
Expand Down Expand Up @@ -69,7 +75,7 @@ export function WorkspaceStatusBadge({
const handleClick = (e: React.MouseEvent) => {
if (prUrl) {
e.stopPropagation();
openUrl.mutate(prUrl);
openUrl.mutate(transformPrUrl(prUrl, prProvider, prCustomDomain));
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import {
LuTriangleAlert,
} from "react-icons/lu";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { PR_LINK_PROVIDER_LABELS } from "renderer/routes/_authenticated/settings/utils/pr-link-provider";
import { usePRStatus } from "renderer/screens/main/hooks";
import { useHotkeyDisplay } from "renderer/stores/hotkeys";
import { transformPrUrl } from "renderer/utils/pr-url";
import { DEFAULT_PR_LINK_PROVIDER } from "shared/constants";
import { STROKE_WIDTH } from "../../../constants";
import { ChecksList } from "./components/ChecksList";
import { ChecksSummary } from "./components/ChecksSummary";
Expand Down Expand Up @@ -40,6 +43,11 @@ export function WorkspaceHoverCardContent({
isLoading: isLoadingGithub,
} = usePRStatus({ workspaceId });

const { data: prLinkSettings } =
electronTrpc.settings.getPrLinkProvider.useQuery();
const prProvider = prLinkSettings?.provider ?? DEFAULT_PR_LINK_PROVIDER;
const prCustomDomain = prLinkSettings?.customDomain;
Comment on lines +46 to +49
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:

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 openPRDisplay = useHotkeyDisplay("OPEN_PR");
const hasOpenPRShortcut = !(
openPRDisplay.length === 1 && openPRDisplay[0] === "Unassigned"
Expand Down Expand Up @@ -168,9 +176,17 @@ export function WorkspaceHoverCardContent({
className="w-full mt-1 h-7 text-xs gap-1.5"
asChild
>
<a href={pr.url} target="_blank" rel="noopener noreferrer">
<FaGithub className="size-3" />
View on GitHub
<a
href={transformPrUrl(pr.url, prProvider, prCustomDomain)}
target="_blank"
rel="noopener noreferrer"
>
{prProvider === "github" ? (
<FaGithub className="size-3" />
) : (
<LuExternalLink className="size-3" strokeWidth={STROKE_WIDTH} />
)}
View on {PR_LINK_PROVIDER_LABELS[prProvider]}
{hasOpenPRShortcut && (
<KbdGroup className="ml-auto">
{openPRDisplay.map((key) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
import { electronTrpc } from "renderer/lib/electron-trpc";
import { PRIcon } from "renderer/screens/main/components/PRIcon";
import { useCreateOrOpenPR } from "renderer/screens/main/hooks";
import { transformPrUrl } from "renderer/utils/pr-url";
import { DEFAULT_PR_LINK_PROVIDER } from "shared/constants";

interface PRButtonProps {
pr: GitHubStatus["pr"] | null;
Expand All @@ -35,6 +37,11 @@ export function PRButton({
worktreePath,
onRefresh,
}: PRButtonProps) {
const { data: prLinkSettings } =
electronTrpc.settings.getPrLinkProvider.useQuery();
const prProvider = prLinkSettings?.provider ?? DEFAULT_PR_LINK_PROVIDER;
const prCustomDomain = prLinkSettings?.customDomain;

const mergePRMutation = electronTrpc.changes.mergePR.useMutation({
onSuccess: () => {
toast.success("PR merged successfully");
Expand Down Expand Up @@ -100,11 +107,12 @@ export function PRButton({
}

const canMerge = pr.state === "open";
const transformedPrUrl = transformPrUrl(pr.url, prProvider, prCustomDomain);

if (!canMerge) {
return (
<a
href={pr.url}
href={transformedPrUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 ml-auto hover:opacity-80 transition-opacity"
Expand All @@ -120,7 +128,7 @@ export function PRButton({
return (
<div className="flex items-center ml-auto rounded border border-border overflow-hidden">
<a
href={pr.url}
href={transformedPrUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 px-1.5 py-0.5 hover:bg-accent transition-colors"
Expand Down
Loading