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
54 changes: 54 additions & 0 deletions apps/desktop/src/lib/trpc/routers/projects/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
refreshDefaultBranch,
} from "../workspaces/utils/git";
import { assignRandomColor } from "./utils/colors";
import { fetchGitHubOwner, getGitHubAvatarUrl } from "./utils/github";

type Project = SelectProject;

Expand Down Expand Up @@ -710,6 +711,59 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {

return { success: true, terminalWarning };
}),

getGitHubAvatar: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
const project = localDb
.select()
.from(projects)
.where(eq(projects.id, input.id))
.get();

if (!project) {
console.log("[getGitHubAvatar] Project not found:", input.id);
return null;
}

// If we already have the github owner cached, return the avatar URL
if (project.githubOwner) {
console.log(
"[getGitHubAvatar] Using cached owner:",
project.githubOwner,
);
return {
owner: project.githubOwner,
avatarUrl: getGitHubAvatarUrl(project.githubOwner),
};
}

// Fetch the owner from GitHub
console.log(
"[getGitHubAvatar] Fetching owner for:",
project.mainRepoPath,
);
const owner = await fetchGitHubOwner(project.mainRepoPath);

if (!owner) {
console.log("[getGitHubAvatar] Failed to fetch owner");
return null;
}

console.log("[getGitHubAvatar] Fetched owner:", owner);

// Cache the owner
localDb
.update(projects)
.set({ githubOwner: owner })
.where(eq(projects.id, input.id))
.run();

return {
owner,
avatarUrl: getGitHubAvatarUrl(owner),
};
}),
});
};

Expand Down
48 changes: 48 additions & 0 deletions apps/desktop/src/lib/trpc/routers/projects/utils/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { z } from "zod";
import { execWithShellEnv } from "../../workspaces/utils/shell-env";

const GHRepoOwnerResponseSchema = z.object({
owner: z.object({
login: z.string(),
}),
});

/**
* Fetches the GitHub owner (user or org) for a repository using the `gh` CLI.
* Returns null if `gh` is not installed, not authenticated, or on error.
*/
export async function fetchGitHubOwner(
repoPath: string,
): Promise<string | null> {
try {
console.log("[fetchGitHubOwner] Running gh repo view in:", repoPath);
const { stdout, stderr } = await execWithShellEnv(
"gh",
["repo", "view", "--json", "owner"],
{ cwd: repoPath },
);
if (stderr) {
console.log("[fetchGitHubOwner] stderr:", stderr);
}
console.log("[fetchGitHubOwner] stdout:", stdout);
const raw = JSON.parse(stdout);
const result = GHRepoOwnerResponseSchema.safeParse(raw);
if (!result.success) {
console.error("[GitHub] Owner schema validation failed:", result.error);
return null;
}
console.log("[fetchGitHubOwner] Parsed owner:", result.data.owner.login);
return result.data.owner.login;
} catch (error) {
console.error("[fetchGitHubOwner] Error:", error);
return null;
}
}

/**
* Constructs the GitHub avatar URL for a user or organization.
* GitHub serves avatars at https://github.com/{owner}.png
*/
export function getGitHubAvatarUrl(owner: string): string {
return `https://github.com/${owner}.png`;
}
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,7 @@ export const createWorkspacesRouter = () => {
name: string;
color: string;
tabOrder: number;
githubOwner: string | null;
mainRepoPath: string;
};
workspaces: Array<{
Expand Down Expand Up @@ -853,6 +854,7 @@ export const createWorkspacesRouter = () => {
color: project.color,
// biome-ignore lint/style/noNonNullAssertion: filter guarantees tabOrder is not null
tabOrder: project.tabOrder!,
githubOwner: project.githubOwner ?? null,
mainRepoPath: project.mainRepoPath,
},
workspaces: [],
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
- script-src 'self' 'wasm-unsafe-eval' https://*.posthog.com: Allow scripts from same origin + WebAssembly (for xterm ImageAddon) + PostHog
- style-src 'self' 'unsafe-inline': Allow styles from same origin + inline (needed for CSS-in-JS)
- connect-src 'self' ws: wss: %NEXT_PUBLIC_API_URL% https://*.posthog.com https://*.sentry.io sentry-ipc:: Allow WebSocket + API (includes Electric proxy) + PostHog + Sentry
- img-src 'self' data: https://*.public.blob.vercel-storage.com: Allow images from same origin + data URIs + Vercel blob storage (avatars)
- img-src 'self' data: https://*.public.blob.vercel-storage.com https://github.com https://avatars.githubusercontent.com: Allow images from same origin + data URIs + Vercel blob storage + GitHub avatars
- font-src 'self': Allow fonts from same origin
-->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://*.posthog.com; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss: %NEXT_PUBLIC_API_URL% https://*.posthog.com https://*.sentry.io sentry-ipc:; img-src 'self' data: https://*.public.blob.vercel-storage.com; font-src 'self';" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://*.posthog.com; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss: %NEXT_PUBLIC_API_URL% https://*.posthog.com https://*.sentry.io sentry-ipc:; img-src 'self' data: https://*.public.blob.vercel-storage.com https://github.com https://avatars.githubusercontent.com; font-src 'self';" />
</head>

<body>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button } from "@superset/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { cn } from "@superset/ui/utils";
import { LuGitBranch } from "react-icons/lu";
import { LuGitCompareArrows } from "react-icons/lu";
import { HotkeyTooltipContent } from "renderer/components/HotkeyTooltipContent";
import { useSidebarStore } from "renderer/stores";

Expand All @@ -22,11 +22,11 @@ export function SidebarControl() {
className={cn(
"no-drag gap-1.5",
isSidebarOpen
? "font-semibold text-foreground"
? "font-semibold text-foreground bg-accent"
: "text-muted-foreground hover:text-foreground",
)}
>
<LuGitBranch className="size-4" />
<LuGitCompareArrows className="size-4" />
<span className="text-xs">Changes</span>
</Button>
</TooltipTrigger>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,32 @@ import {
} from "shared/hotkeys";

export function WorkspaceSidebarControl() {
const { isOpen, toggleOpen } = useWorkspaceSidebarStore();
const { isOpen, isCollapsed, toggleCollapsed, setOpen } =
useWorkspaceSidebarStore();

const handleToggle = () => {
if (!isOpen) {
// If sidebar is closed, open it to collapsed state
setOpen(true);
} else {
// If sidebar is open, toggle between collapsed and expanded
toggleCollapsed();
}
};

const sidebarCollapsed = isCollapsed();

return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={toggleOpen}
onClick={handleToggle}
aria-label="Toggle workspace sidebar"
className="no-drag"
>
{isOpen ? (
{isOpen && !sidebarCollapsed ? (
<LuPanelLeftClose className="size-4" />
) : (
<LuPanelLeft className="size-4" />
Expand All @@ -31,7 +44,7 @@ export function WorkspaceSidebarControl() {
</TooltipTrigger>
<TooltipContent side="bottom" showArrow={false}>
<span className="flex items-center gap-2">
Toggle Workspaces
{sidebarCollapsed ? "Expand" : "Collapse"} Workspaces
<KbdGroup>
{formatHotkeyDisplay(
getHotkey("TOGGLE_WORKSPACE_SIDEBAR"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { trpc } from "renderer/lib/trpc";
import { AvatarDropdown } from "../AvatarDropdown";
import { OpenInMenuButton } from "./OpenInMenuButton";
import { WindowControls } from "./WindowControls";
import { WorkspaceSidebarControl } from "./WorkspaceSidebarControl";

export function TopBar() {
const { data: platform } = trpc.window.getPlatform.useQuery();
Expand All @@ -17,9 +16,7 @@ export function TopBar() {
style={{
paddingLeft: isMac ? "88px" : "16px",
}}
>
<WorkspaceSidebarControl />
</div>
/>

<div className="flex-1" />

Expand Down
Loading
Loading