Skip to content
29 changes: 28 additions & 1 deletion apps/desktop/src/lib/trpc/routers/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,42 @@ async function detectGhCli(): Promise<GhDetectResult> {
);
authenticated = true;
} catch {
// `gh auth status` exits non-zero when not logged in.
// `--active` requires gh >= 2.40; retry without it for older installs.
// Both variants exit non-zero when not logged in.
try {
await execWithShellEnv(
"gh",
["auth", "status", "--hostname", "github.com"],
{ timeout: 5000 },
);
authenticated = true;
} catch {}
}

return { installed: true, authenticated, version, path: "gh" };
}

interface BrewDetectResult {
installed: boolean;
version: string | null;
}

async function detectBrew(): Promise<BrewDetectResult> {
try {
const { stdout } = await execWithShellEnv("brew", ["--version"], {
timeout: 5000,
});
const version = stdout.match(/Homebrew (\S+)/)?.[1] ?? null;
return { installed: true, version };
} catch {
return { installed: false, version: null };
}
}

export const createSystemRouter = () => {
return router({
detectGhCli: publicProcedure.query(detectGhCli),
detectBrew: publicProcedure.query(detectBrew),
});
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,264 @@
import { Button } from "@superset/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@superset/ui/dialog";
import { Spinner } from "@superset/ui/spinner";
import { cn } from "@superset/ui/utils";
import { useEffect, useRef, useState } from "react";
import {
LuArrowUpRight,
LuCheck,
LuCopy,
LuTriangleAlert,
} from "react-icons/lu";
import { electronTrpcClient } from "renderer/lib/trpc-client";
import stripAnsi from "strip-ansi";
import { GhAuthTerminal } from "./GhAuthTerminal";

const GH_AUTH_COMMAND =
"gh auth login --hostname github.com --git-protocol https --web";
const GH_INSTALL_COMMAND = `brew install gh && ${GH_AUTH_COMMAND}`;

const ONE_TIME_CODE_PATTERN = /one-time code: ([A-Z0-9]{4}-[A-Z0-9]{4})/;

export type GhAuthDialogMode = "auth" | "install";

type Phase = "running" | "checking" | "success" | "failed";

interface GhAuthDialogProps {
open: boolean;
mode: GhAuthDialogMode;
onOpenChange: (open: boolean) => void;
/** Fired when the gh process exits so the caller can re-check auth status. */
onExit: () => void;
}

export function GhAuthDialog({
open,
mode,
onOpenChange,
onExit,
}: GhAuthDialogProps) {
const [phase, setPhase] = useState<Phase>("running");
const [attempt, setAttempt] = useState(0);
const [oneTimeCode, setOneTimeCode] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const outputBufferRef = useRef("");
const closeTimerRef = useRef<number | null>(null);
const terminalBoxRef = useRef<HTMLDivElement>(null);
const ptyWriteRef = useRef<((data: string) => void) | null>(null);
const onExitRef = useRef(onExit);
onExitRef.current = onExit;

useEffect(() => {
if (!open) return;
setPhase("running");
setAttempt(0);
setOneTimeCode(null);
setCopied(false);
outputBufferRef.current = "";
return () => {
if (closeTimerRef.current !== null) {
window.clearTimeout(closeTimerRef.current);
closeTimerRef.current = null;
}
};
}, [open]);

// Auto-copy so the happy path needs no clicks; the Copy button remains as
// a fallback for when the clipboard write is rejected (e.g. window unfocused).
useEffect(() => {
if (!oneTimeCode) return;
navigator.clipboard.writeText(oneTimeCode).then(
() => setCopied(true),
() => setCopied(false),
);
}, [oneTimeCode]);

const handleOutput = (data: string) => {
if (oneTimeCode) return;
outputBufferRef.current = (outputBufferRef.current + data).slice(-4096);
const match = stripAnsi(outputBufferRef.current).match(
ONE_TIME_CODE_PATTERN,
);
if (match?.[1]) {
setOneTimeCode(match[1]);
// gh only waits for Enter so the user can copy the code first —
// we've captured it, so advance straight to opening the browser.
ptyWriteRef.current?.("\r");
}
};

const handleTerminalExit = () => {
setPhase("checking");
onExitRef.current();
void electronTrpcClient.system.detectGhCli
.query()
.then((status) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Reopening during a previous status check can have the old check mark the new auth attempt successful and auto-close it. Guard this completion with a per-open/per-attempt token (or cancel it) before updating phase or scheduling the close.

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/onboarding/components/GhAuthDialog/GhAuthDialog.tsx, line 78:

<comment>Reopening during a previous status check can have the old check mark the new auth attempt successful and auto-close it. Guard this completion with a per-open/per-attempt token (or cancel it) before updating phase or scheduling the close.</comment>

<file context>
@@ -1,38 +1,203 @@
+		onExitRef.current();
+		void electronTrpcClient.system.detectGhCli
+			.query()
+			.then((status) => {
+				if (status.installed && status.authenticated) {
+					setPhase("success");
</file context>

if (status.installed && status.authenticated) {
setPhase("success");
closeTimerRef.current = window.setTimeout(() => {
onOpenChange(false);
}, 1500);
} else {
setPhase("failed");
}
})
.catch(() => setPhase("failed"));
};

const handleRetry = () => {
setPhase("running");
setOneTimeCode(null);
setCopied(false);
outputBufferRef.current = "";
setAttempt((prev) => prev + 1);
};

const focusTerminal = () => {
terminalBoxRef.current
?.querySelector<HTMLTextAreaElement>(".xterm-helper-textarea")
?.focus();
};

const handleCopyCode = () => {
if (!oneTimeCode) return;
navigator.clipboard.writeText(oneTimeCode).then(
() => setCopied(true),
() => {},
);
// Return focus to the terminal so Enter goes to gh, not the button.
focusTerminal();
};

const processActive = phase === "running" || phase === "checking";
const isInstall = mode === "install";

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[752px] gap-4">
<Dialog open={open} modal onOpenChange={onOpenChange}>
<DialogContent
className="gap-3 sm:max-w-[752px]"
aria-describedby={undefined}
onInteractOutside={(event) => {
if (processActive) event.preventDefault();
}}
onFocusOutside={(event) => {
if (processActive) event.preventDefault();
}}
Comment on lines +142 to +150

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

path="apps/desktop/src/renderer/routes/_authenticated/onboarding/components/GhAuthDialog/GhAuthDialog.tsx"
if [ ! -f "$path" ]; then
  echo "Missing file: $path"
  git ls-files | rg 'GhAuthDialog\.tsx|GhAuthDialog' || true
  exit 0
fi

echo "== file outline =="
ast-grep outline "$path" --view expanded || true

echo "== relevant source =="
wc -l "$path"
sed -n '1,180p' "$path" | cat -n

echo "== all dialog dismissal handler references in file =="
rg -n "onEscapeKeyDown|onInteractOutside|onFocusOutside|processActive|setPhase|detectGhCli|GhAuthTerminal|handleTerminalExit" "$path"

Repository: superset-sh/superset

Length of output: 8389


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect Dialog implementation =="
git ls-files | rg '`@superset/ui/dialog`|dialog' | sed -n '1,80p' || true

if [ -f "apps/desktop/src/lib/ui/index.ts" ]; then
  echo "== ui index excerpt =="
  sed -n '1,220p' apps/desktop/src/lib/ui/index.ts | cat -n
fi

if [ -d "apps/desktop/src/lib/ui" ]; then
  echo "== ui dialog files =="
  git ls-files "apps/desktop/src/lib/ui" | rg -i 'dialog|ui|components' || true
fi

echo "== package imports maybe linked via source =="
rg -n 'DialogContent|useDialog|AlertDialog|Radix.*Dialog|`@radix-ui/react-dialog`' apps/desktop/src || true

echo "== inspect local package metadata if present =="
for f in package.json bun.lockb bun.lock pnpm-lock.yaml yarn.lock; do
  [ -f "$f" ] && echo "-- $f --" && (rg -n '"`@radix-ui/react-dialog`"|`@radix-ui/react-dialog`|DialogContent' "$f" || true)
done

Repository: superset-sh/superset

Length of output: 50377


🌐 Web query:

Radix UI Dialog Content onEscapeKeyDown default behavior Escape key props example

💡 Result:

The default behavior of the Radix UI Dialog Content component is to close the dialog and return focus to the trigger element when the Escape key is pressed [1][2][3]. To customize or prevent this behavior, you can use the onEscapeKeyDown prop on the Dialog.Content component [1][2]. By calling event.preventDefault within the handler, you can stop the dialog from closing [4][5][6]. Example of preventing the Escape key from closing the dialog: <Dialog.Content onEscapeKeyDown={(event) => { event.preventDefault; }} > {/* Dialog content */} </Dialog.Content> You can also use this prop to execute custom logic before or instead of the default close action, such as showing a confirmation prompt or checking conditions before allowing the dialog to close [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== local dialog component =="
sed -n '1,220p' packages/ui/src/components/ui/dialog.tsx | cat -n

echo "== local DialogContent exports/usages =="
rg -n "export const DialogContent|DialogContent|DialogContentContent|onEscapeKeyDown|onInteractOutside|onFocusOutside" packages/ui/src/components/ui/dialog.tsx packages/ui/src/components/ui/alert-dialog.tsx || true

echo "== GhAuthTerminal source =="
file="apps/desktop/src/renderer/routes/_authenticated/onboarding/components/GhAuthDialog/GhAuthTerminal.tsx"
if [ -f "$file" ]; then
  wc -l "$file"
  ast-grep outline "$file" --view expanded || true
  sed -n '1,240p' "$file" | cat -n
fi

Repository: superset-sh/superset

Length of output: 10050


Guard Escape dismissal while a shell process is active.

GhAuthTerminal mounts a terminal and calls terminal.kill.mutate on unmount, and handleTerminalExit starts detectGhCli.query(). Since DialogContent accepts dialog dismissal props, add onEscapeKeyDown that calls event.preventDefault() when processActive is true, matching the outside-escape handlers.

🔒 Proposed fix to guard the Escape key
 				onFocusOutside={(event) => {
 					if (processActive) event.preventDefault();
 				}}
+				onEscapeKeyDown={(event) => {
+					if (processActive) event.preventDefault();
+				}}
📝 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
<DialogContent
className="gap-3 sm:max-w-[752px]"
aria-describedby={undefined}
onInteractOutside={(event) => {
if (processActive) event.preventDefault();
}}
onFocusOutside={(event) => {
if (processActive) event.preventDefault();
}}
<DialogContent
className="gap-3 sm:max-w-[752px]"
aria-describedby={undefined}
onInteractOutside={(event) => {
if (processActive) event.preventDefault();
}}
onFocusOutside={(event) => {
if (processActive) event.preventDefault();
}}
onEscapeKeyDown={(event) => {
if (processActive) event.preventDefault();
}}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/onboarding/components/GhAuthDialog/GhAuthDialog.tsx`
around lines 131 - 139, Update the DialogContent configuration in GhAuthDialog
to add an onEscapeKeyDown handler that prevents the default dismissal when
processActive is true, matching the existing onInteractOutside and
onFocusOutside guards.

>
<DialogHeader>
<DialogTitle>Sign in to GitHub CLI</DialogTitle>
<DialogDescription>
Follow the prompts below. Press Enter to open your browser,
authorize the device code, and this window will update once you're
signed in.
</DialogDescription>
<DialogTitle>
{isInstall ? "Install GitHub CLI" : "Sign in to GitHub CLI"}
</DialogTitle>
</DialogHeader>
<div className="h-[240px] w-full overflow-hidden rounded-md bg-[#151110] p-2">
{open && <GhAuthTerminal onExit={onExit} />}
</div>
{phase === "success" ? (
<div className="flex h-[296px] w-full flex-col items-center justify-center gap-2.5 rounded-lg border bg-[#151110]">
<LuCheck className="size-5 text-emerald-500" strokeWidth={2} />
<p className="text-sm font-medium text-foreground">
{isInstall
? "GitHub CLI installed and signed in"
: "Signed in to GitHub"}
</p>
</div>
) : (
<>
{phase === "failed" ? (
<div className="flex min-h-11 items-center gap-2.5 rounded-md border bg-muted/40 px-3.5 py-1.5 text-sm text-foreground">
<LuTriangleAlert className="size-3.5 shrink-0 text-destructive" />
<span className="select-text cursor-text">
{isInstall
? "Installation didn't complete"
: "Sign-in didn't complete"}
</span>
<span className="ml-auto flex shrink-0 items-center gap-2">
{isInstall && (
<Button
type="button"
size="sm"
variant="ghost"
onClick={() =>
window.open(
"https://cli.github.com/",
"_blank",
"noopener,noreferrer",
)
}
>
Install manually
<LuArrowUpRight className="size-3" />
</Button>
)}
<Button
type="button"
size="sm"
variant="outline"
onClick={handleRetry}
>
Retry
</Button>
</span>
</div>
) : phase === "checking" ? (
<div className="flex min-h-11 items-center gap-2.5 rounded-md bg-muted/40 px-3.5 text-sm text-muted-foreground">
<Spinner className="size-3.5 shrink-0" />
Checking sign-in status…
</div>
) : oneTimeCode ? (
<div className="flex min-h-11 items-center gap-3 rounded-md border bg-muted/40 px-3.5 py-1.5">
<span className="select-text cursor-text font-mono text-lg font-semibold tracking-[0.18em] text-foreground">
{oneTimeCode}
</span>
<span className="text-xs text-muted-foreground">
Paste this code on GitHub — your browser is opening
</span>
{copied ? (
<span className="ml-auto flex shrink-0 items-center gap-1.5 text-xs font-medium text-emerald-500">
<LuCheck className="size-3.5" strokeWidth={2.5} />
Copied
</span>
) : (
<Button
type="button"
size="sm"
variant="outline"
className="ml-auto"
onClick={handleCopyCode}
>
<LuCopy className="size-3.5" />
Copy
</Button>
)}
</div>
) : (
<div className="flex min-h-11 items-center gap-2.5 rounded-md bg-muted/40 px-3.5 text-sm text-muted-foreground">
<Spinner className="size-3.5 shrink-0" />
Follow the prompts below
</div>
)}
<div
ref={terminalBoxRef}
className={cn(
"h-[240px] w-full overflow-hidden rounded-lg border bg-[#151110] p-3 transition-opacity duration-300",
phase === "failed" && "opacity-60",
)}
>
{open && (
<GhAuthTerminal
key={attempt}
command={isInstall ? GH_INSTALL_COMMAND : GH_AUTH_COMMAND}
onExit={handleTerminalExit}
onOutput={handleOutput}
onWriterReady={(write) => {
ptyWriteRef.current = write;
}}
/>
)}
</div>
</>
)}
</DialogContent>
</Dialog>
);
Expand Down
Loading
Loading