Skip to content
Merged
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
3 changes: 3 additions & 0 deletions apps/desktop/electron-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ const config: Configuration = {
// Required for macOS microphone permission prompt
NSMicrophoneUsageDescription:
"Superset needs microphone access so voice-enabled tools like Codex transcription can capture audio input.",
// Required for macOS camera permission prompt
NSCameraUsageDescription:
"Superset needs camera access so websites and tools running inside the app can capture video input.",
// Required for macOS local network permission prompt
NSLocalNetworkUsageDescription:
"Superset needs access to your local network to discover and connect to development servers running on your network.",
Expand Down
50 changes: 50 additions & 0 deletions apps/desktop/src/lib/electron/request-media-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { SitePermissionKind } from "@superset/local-db";
import { shell, systemPreferences } from "electron";

const MEDIA_ACCESS_SETTINGS_URLS: Record<SitePermissionKind, string> = {
microphone:
"x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone",
camera:
"x-apple.systempreferences:com.apple.preference.security?Privacy_Camera",
};

interface RequestMediaAccessResult {
granted: boolean;
openedSystemSettings: boolean;
}

export async function requestMediaAccess(
kind: SitePermissionKind,
): Promise<RequestMediaAccessResult> {
if (process.platform !== "darwin") {
return {
granted: true,
openedSystemSettings: false,
};
}

try {
if (systemPreferences.getMediaAccessStatus(kind) === "granted") {
return {
granted: true,
openedSystemSettings: false,
};
}

const granted = await systemPreferences.askForMediaAccess(kind);
if (granted) {
return {
granted: true,
openedSystemSettings: false,
};
}
} catch {
// Fall through to opening System Settings.
}

await shell.openExternal(MEDIA_ACCESS_SETTINGS_URLS[kind]);
return {
granted: false,
openedSystemSettings: true,
};
}
71 changes: 71 additions & 0 deletions apps/desktop/src/lib/trpc/routers/browser/browser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import {
SITE_PERMISSION_KINDS,
SITE_PERMISSION_VALUES,
} from "@superset/local-db";
import { observable } from "@trpc/server/observable";
import { session } from "electron";
import { requestMediaAccess } from "lib/electron/request-media-access";
import { browserManager } from "main/lib/browser/browser-manager";
import { browserSitePermissionManager } from "main/lib/browser/browser-site-permission-manager";
import { z } from "zod";
import { publicProcedure, router } from "../..";

Expand Down Expand Up @@ -190,6 +196,71 @@ export const createBrowserRouter = () => {
};
}),

getSitePermissions: publicProcedure
.input(z.object({ url: z.string() }))
.query(({ input }) => {
return browserSitePermissionManager.getPermissionsForUrl(input.url);
}),

setSitePermission: publicProcedure
.input(
z.object({
origin: z.string(),
kind: z.enum(SITE_PERMISSION_KINDS),
value: z.enum(SITE_PERMISSION_VALUES),
}),
)
.mutation(async ({ input }) => {
const sitePermissions = browserSitePermissionManager.setPermission(
input.origin,
input.kind,
input.value,
);

const mediaAccess =
input.value === "allow" ? await requestMediaAccess(input.kind) : null;

return {
...sitePermissions,
mediaAccess,
};
}),

resetSitePermissions: publicProcedure
.input(z.object({ origin: z.string() }))
.mutation(({ input }) => {
browserSitePermissionManager.resetPermissions(input.origin);
return { success: true };
}),

onSitePermissionRequested: publicProcedure
.input(z.object({ paneId: z.string() }))
.subscription(({ input }) => {
return observable<{
paneId: string;
origin: string;
permissions: ("microphone" | "camera")[];
}>((emit) => {
const handler = (event: {
paneId: string;
origin: string;
permissions: ("microphone" | "camera")[];
}) => {
emit.next(event);
};
browserSitePermissionManager.on(
`permission-requested:${input.paneId}`,
handler,
);
return () => {
browserSitePermissionManager.off(
`permission-requested:${input.paneId}`,
handler,
);
};
});
}),

clearBrowsingData: publicProcedure
.input(
z.object({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,26 @@
import type { GitHubStatus } from "@superset/local-db";
import { normalizeGitHubRepoUrl } from "./pull-request-url";
import {
type GitRemoteInfo,
type GitTrackingRefInfo,
getPullRequestHeadRepoUrl,
isOpenPullRequestState,
type PullRequestPushTargetInfo,
resolveRemoteNameForPullRequestHead,
} from "../../workspaces/utils/github/pr-attachment";

type ExistingPullRequest = NonNullable<GitHubStatus["pr"]>;

export interface GitRemoteInfo {
name: string;
fetchUrl?: string;
pushUrl?: string;
}

export interface GitTrackingRefInfo {
remoteName: string;
branchName: string;
}
export type { GitRemoteInfo };

export interface ExistingPullRequestPushTargetInfo {
remote: string;
targetBranch: string;
}

export function isOpenPullRequestState(
state: ExistingPullRequest["state"],
): boolean {
return state === "open" || state === "draft";
}
type ExistingPullRequest = NonNullable<GitHubStatus["pr"]>;
export type ExistingPullRequestPushTargetInfo = PullRequestPushTargetInfo;
export { isOpenPullRequestState };

export function getExistingPRHeadRepoUrl(
pr: Pick<
ExistingPullRequest,
"headRepositoryOwner" | "headRepositoryName" | "isCrossRepository"
>,
): string | null {
if (
!pr.isCrossRepository ||
!pr.headRepositoryOwner ||
!pr.headRepositoryName
) {
return null;
}

return `https://github.com/${pr.headRepositoryOwner}/${pr.headRepositoryName}`;
return getPullRequestHeadRepoUrl(pr);
}

export function resolveRemoteNameForExistingPRHead({
Expand All @@ -54,36 +35,11 @@ export function resolveRemoteNameForExistingPRHead({
>;
fallbackRemote: string;
}): string | null {
if (!pr.isCrossRepository) {
return fallbackRemote;
}

const headRepoUrl = getExistingPRHeadRepoUrl(pr);
if (!headRepoUrl) {
return null;
}

const normalizedHeadRepoUrl = normalizeGitHubRepoUrl(headRepoUrl);
if (!normalizedHeadRepoUrl) {
return null;
}

for (const remote of remotes) {
const fetchUrl = remote.fetchUrl
? normalizeGitHubRepoUrl(remote.fetchUrl)
: null;
const pushUrl = remote.pushUrl
? normalizeGitHubRepoUrl(remote.pushUrl)
: null;
if (
fetchUrl === normalizedHeadRepoUrl ||
pushUrl === normalizedHeadRepoUrl
) {
return remote.name;
}
}

return null;
return resolveRemoteNameForPullRequestHead({
remotes,
pr,
fallbackRemote,
});
}

export function shouldRetargetPushToExistingPRHead({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,126 +1,26 @@
import { TRPCError } from "@trpc/server";
import type { SimpleGit } from "simple-git";
import { z } from "zod";
import { execGitWithShellPath } from "../../workspaces/utils/git-client";
import { getRepoContext } from "../../workspaces/utils/github";
import { getPullRequestRepoNames } from "../../workspaces/utils/github/repo-context";
import { fetchGitHubPRStatus } from "../../workspaces/utils/github";
import { execWithShellEnv } from "../../workspaces/utils/shell-env";
import {
buildPullRequestCompareUrl,
normalizeGitHubRepoUrl,
parseUpstreamRef,
} from "./pull-request-url";

async function findOpenPRByHeadCommit(
worktreePath: string,
): Promise<string | null> {
try {
const { stdout: headOutput } = await execGitWithShellPath(
["rev-parse", "HEAD"],
{ cwd: worktreePath },
);
const headSha = headOutput.trim();
if (!headSha) {
return null;
}

const repoNames = getPullRequestRepoNames(
await getRepoContext(worktreePath),
);
const repoArgSets =
repoNames.length > 0
? repoNames.map((repoName) => ["--repo", repoName])
: [[]];

for (const repoArgs of repoArgSets) {
try {
const { stdout } = await execWithShellEnv(
"gh",
[
"pr",
"list",
...repoArgs,
"--state",
"open",
"--search",
`${headSha} is:pr`,
"--limit",
"20",
"--json",
"url,headRefOid",
],
{ cwd: worktreePath },
);

const parsed = JSON.parse(stdout) as Array<{
url?: string;
headRefOid?: string;
}>;
const match = parsed.find(
(candidate) => candidate.headRefOid === headSha,
);
if (match?.url?.trim()) {
return match.url.trim();
}
} catch (error) {
console.warn(
"[git/findExistingOpenPRUrl] Failed repo-scoped commit-based PR lookup:",
{
worktreePath,
repoArgs,
message: error instanceof Error ? error.message : String(error),
},
);
}
}

return null;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(
"[git/findExistingOpenPRUrl] Failed commit-based PR lookup:",
message,
);
return null;
}
}
import { clearWorktreeStatusCaches } from "./worktree-status-caches";

export async function findExistingOpenPRUrl(
worktreePath: string,
): Promise<string | null> {
// Prefer tracking-based lookup first for fork/branch-name mismatch scenarios.
try {
const { stdout } = await execWithShellEnv(
"gh",
[
"pr",
"view",
"--json",
"url,state",
"--jq",
'if .state == "OPEN" then .url else "" end',
],
{ cwd: worktreePath },
);
const url = stdout.trim();
if (url) {
return url;
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const isNoPROpenError = message
.toLowerCase()
.includes("no pull requests found");
if (!isNoPROpenError) {
console.warn(
"[git/findExistingOpenPRUrl] Failed tracking-branch PR lookup:",
message,
);
}
// Fallback to commit-SHA search below.
clearWorktreeStatusCaches(worktreePath);
const githubStatus = await fetchGitHubPRStatus(worktreePath);
const pullRequest = githubStatus?.pr;
if (pullRequest?.state !== "open" && pullRequest?.state !== "draft") {
return null;
}

return findOpenPRByHeadCommit(worktreePath);
return pullRequest.url.trim() || null;
}

const ghRepoMetadataSchema = z.object({
Expand Down
Loading
Loading