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
2 changes: 2 additions & 0 deletions .agents/skills/cdp-verification/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ For a non-local workspace, the normal desktop flow intentionally restores an enc

Do not use setup `--force` to fix a stale connection string, a missing CDP cookie, or a corrupt generated Next.js cache. First rerun the applicable setup script without force. If every API route returns Next.js's HTML 404, stop the dev stack, move `apps/api/.next` aside, and restart. `--force` is normally only appropriate when the user explicitly intends to replace the copied local/host databases and encrypted auth token. The stale-state signature in the next paragraph is the one explicit exception.

To run the local API against another database (a fresh Neon branch of production, say), passing `DATABASE_URL` in the environment is not enough: in development `apps/api/next.config.ts` reloads the root `.env` with `override: true` and puts the workspace's database back. The signs are "column ... does not exist" from a branch that has the column, and a sign-in that returns a user your edits on the branch no longer match. Change that database only through the root `.env`, or with a local, unpushed change to that guard, and check a query that only the target branch can answer. The Local Admin button signs in whichever user holds `DEV_EMAIL` in that database.

One failure signature where `./.superset/setup.sh --force` IS the fix (verified 2026-07-28): session restore hangs at "Restoring your session", the Local Admin sign-in button returns a bodyless 500, get-session returns 200, and a raw `select 1` against `DATABASE_URL` may still succeed; the worktree's seeded dev state (Neon branch credentials in `.env`, `auth-token.enc`, copied DBs) has gone stale as a set. Rerunning with `--force` recreates the Neon branch, rewrites `.env`, and reseeds `superset-dev-data/` together, which restores sign-in. Two side effects to expect: any manual `.env` edits (e.g. a port remap) are wiped and must be re-applied, and `superset-dev-data/` is reset.

**Use `Runtime.evaluate` (`awaitPromise`, `returnByValue`), not `Network.*` interception**; sniffing misses React-Query-cached responses, and `refetchInterval` is paused while the window is backgrounded. After verifying the session through the applicable cookie or bearer path above, run requests inside the renderer. `API` below is the dev backend origin (`NEXT_PUBLIC_API_URL`, e.g. `http://localhost:5881`):
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/app/api/github/jobs/initial-sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
githubPullRequests,
githubRepositories,
} from "@superset/db/schema";
import { nudge } from "@superset/trpc/realtime";
import { subDays } from "date-fns";
import { eq } from "drizzle-orm";
import { z } from "zod";
Expand Down Expand Up @@ -228,6 +229,7 @@ export async function POST(request: Request) {
.set({ lastSyncedAt: new Date() })
.where(eq(githubInstallations.id, installationDbId));

nudge(organizationId, "pull_requests");
console.log("[github/initial-sync] Sync completed successfully");
return Response.json({ success: true });
} catch (error) {
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/app/api/github/sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
githubPullRequests,
githubRepositories,
} from "@superset/db/schema";
import { nudge } from "@superset/trpc/realtime";
import { subDays } from "date-fns";
import { eq } from "drizzle-orm";
import { z } from "zod";
Expand Down Expand Up @@ -220,6 +221,7 @@ export async function POST(request: Request) {
.set({ lastSyncedAt: new Date() })
.where(eq(githubInstallations.id, installation.id));

nudge(organizationId, "pull_requests");
console.log("[github/sync] Sync completed successfully");
return Response.json({
success: true,
Expand Down
55 changes: 53 additions & 2 deletions apps/api/src/app/api/github/webhook/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import type { EmitterWebhookEvent } from "@octokit/webhooks";
import { Webhooks } from "@octokit/webhooks";
import { db } from "@superset/db/client";
import {
cloudWorkspacePullRequests,
cloudWorkspaceRepositories,
cloudWorkspaces,
githubInstallations,
githubPullRequests,
githubRepositories,
} from "@superset/db/schema";
import { nudge } from "@superset/trpc/realtime";
import { and, eq } from "drizzle-orm";

import { env } from "@/env";
Expand Down Expand Up @@ -126,7 +130,42 @@ webhooks.on(
},
);

function upsertPullRequest(
async function nudgeCloudWorkspacesShowing(
repo: { id: string; organizationId: string },
pullRequest: { number: number; headBranch: string },
) {
const [linked, onBranch] = await Promise.all([
db
.select({ id: cloudWorkspacePullRequests.cloudWorkspaceId })
.from(cloudWorkspacePullRequests)
.where(
and(
eq(cloudWorkspacePullRequests.repositoryId, repo.id),
eq(cloudWorkspacePullRequests.prNumber, pullRequest.number),
),
)
.limit(1),
db
.select({ id: cloudWorkspaces.id })
.from(cloudWorkspaceRepositories)
.innerJoin(
cloudWorkspaces,
eq(cloudWorkspaceRepositories.cloudWorkspaceId, cloudWorkspaces.id),
)
.where(
and(
eq(cloudWorkspaceRepositories.repositoryId, repo.id),
eq(cloudWorkspaces.branch, pullRequest.headBranch),
),
)
.limit(1),
]);
if (linked.length > 0 || onBranch.length > 0) {
nudge(repo.organizationId, "pull_requests");
}
}

async function upsertPullRequest(
repo: { id: string; organizationId: string },
pr: {
number: number;
Expand All @@ -147,7 +186,7 @@ function upsertPullRequest(
},
) {
const upstreamUpdatedAt = new Date(pr.updated_at);
return db
await db
.insert(githubPullRequests)
.values({
repositoryId: repo.id,
Expand Down Expand Up @@ -190,6 +229,10 @@ function upsertPullRequest(
updatedAt: upstreamUpdatedAt,
},
});
await nudgeCloudWorkspacesShowing(repo, {
number: pr.number,
headBranch: pr.head.ref,
});
}

webhooks.on(
Expand Down Expand Up @@ -319,6 +362,10 @@ webhooks.on(
),
);
}
await nudgeCloudWorkspacesShowing(repo, {
number: pr.number,
headBranch: pr.head.ref,
});
},
);

Expand Down Expand Up @@ -410,6 +457,10 @@ webhooks.on(
lastSyncedAt: new Date(),
})
.where(eq(githubPullRequests.id, currentPr.id));
await nudgeCloudWorkspacesShowing(repo, {
number: currentPr.prNumber,
headBranch: currentPr.headBranch,
});
}
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { errorMessage } from "@superset/i18n/errors";
import { toast } from "@superset/ui/sonner";
import { useNavigate } from "@tanstack/react-router";
import { TRPCClientError } from "@trpc/client";
import { useMemo, useState } from "react";
import { useState } from "react";
import { useActiveOrganizationId } from "renderer/hooks/useActiveOrganizationId";
import { useCloudWorkspaces } from "renderer/hooks/useCloudWorkspaces";
import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard";
Expand All @@ -14,9 +14,8 @@ import { electronTrpc } from "renderer/lib/electron-trpc";
import { NewProjectDialog } from "renderer/routes/_authenticated/_dashboard/components/NewProjectDialog";
import { StateScreenShell } from "renderer/routes/_authenticated/_dashboard/components/StateScreenShell";
import { WorkspaceNotFoundState } from "renderer/routes/_authenticated/_dashboard/components/WorkspaceNotFoundState";
import type { CloudPullRequest } from "renderer/routes/_authenticated/_dashboard/hooks/useCloudPullRequests";
import {
cloudPullRequestRefKey,
type CloudPullRequest,
useCloudPullRequests,
} from "renderer/routes/_authenticated/_dashboard/hooks/useCloudPullRequests";
import { useCloudWorkspacePresence } from "renderer/routes/_authenticated/_dashboard/hooks/useCloudWorkspacePresence";
Expand Down Expand Up @@ -118,19 +117,12 @@ export function CloudWorkspaceRecordScreen({

const data = record.data;
const listed = workspaces?.find((row) => row.id === workspaceId);
const refs = useMemo(
() =>
(data?.repositories ?? []).map((repository) => ({
repoFullName: repository.fullName,
headBranch: repository.branch,
})),
[data?.repositories],
);
const { byRef } = useCloudPullRequests(refs);
const pullRequests = refs.flatMap((ref): CloudPullRequest[] => {
const pullRequest = byRef.get(cloudPullRequestRefKey(ref));
return pullRequest ? [pullRequest] : [];
const { byWorkspace: pullRequestsById } = useCloudPullRequests({
active: data !== undefined && data.status !== "deleted",
archived: data?.status === "deleted",
});
const pullRequests: CloudPullRequest[] =
pullRequestsById.get(workspaceId) ?? [];

const isGone =
record.error instanceof TRPCClientError &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function CloudWorkspacePullRequestsBadge({
className="w-80 p-1 pt-2"
onClick={(event) => event.stopPropagation()}
>
<CloudSection title={<Trans>Pull requests</Trans>}>
<CloudSection title={<Trans>Pull requests</Trans>} scrollable>
{pullRequests.map((pullRequest) => (
<CloudPullRequestRow
key={pullRequest.url}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@ import {
import { buildCloudSidebar } from "renderer/routes/_authenticated/_dashboard/utils/buildCloudSidebar";
import { useHostWorkspaces } from "renderer/routes/_authenticated/providers/HostWorkspacesProvider";
import { useSidebarSectionsCollapseStore } from "renderer/stores/sidebar-sections-collapse";
import {
type CloudPullRequestRef,
cloudPullRequestRefKey,
useCloudPullRequests,
} from "../../../../hooks/useCloudPullRequests";
import { useCloudPullRequests } from "../../../../hooks/useCloudPullRequests";
import { DashboardSidebarSectionHeader } from "../DashboardSidebarSectionHeader";
import { DashboardSidebarCloudGroup } from "./components/DashboardSidebarCloudGroup";
import { DashboardSidebarCloudHoverCard } from "./components/DashboardSidebarCloudHoverCard";
Expand Down Expand Up @@ -187,33 +183,12 @@ export function DashboardSidebarCloudSection({
const branchOf = (workspace: { id: string; branch: string }) =>
branchById.get(workspace.id) ?? workspace.branch;

const pullRequestRefs = useMemo<CloudPullRequestRef[]>(
() =>
shown.flatMap((workspace) => {
const repoFullName = repoFullNameById.get(workspace.id);
return repoFullName
? [
{
repoFullName,
headBranch: branchById.get(workspace.id) ?? workspace.branch,
},
]
: [];
}),
[shown, repoFullNameById, branchById],
);
const cloudPullRequests = useCloudPullRequests(pullRequestRefs);
const pullRequestOf = (workspace: { id: string; branch: string }) => {
const repoFullName = repoFullNameById.get(workspace.id);
return repoFullName
? (cloudPullRequests.byRef.get(
cloudPullRequestRefKey({
repoFullName,
headBranch: branchOf(workspace),
}),
) ?? null)
: null;
};
const { byWorkspace: pullRequestsById } = useCloudPullRequests({
active: shown.length > 0,
archived: false,
});
const pullRequestOf = (workspace: { id: string }) =>
pullRequestsById.get(workspace.id)?.[0] ?? null;
Comment on lines +186 to +191

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 Older boxes lose their badge

The sidebar no longer uses the open box's live branch to find its PR. For an older box that has not reported links, moving off its creation branch now removes an existing badge: the new fallback only searches cloudWorkspaces.branch. Keep the live-branch lookup as a fallback until the box has reported its PRs. The inline sidebar rows have the same issue.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarCloudSection/DashboardSidebarCloudSection.tsx
Line: 186-190

Comment:
**Older boxes lose their badge**

The sidebar no longer uses the open box's live branch to find its PR. For an older box that has not reported links, moving off its creation branch now removes an existing badge: the new fallback only searches `cloudWorkspaces.branch`. Keep the live-branch lookup as a fallback until the box has reported its PRs. The inline sidebar rows have the same issue.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.


const hoverCard = useCloudHoverCard();
const openUrl = electronTrpc.external.openUrl.useMutation();
Expand All @@ -223,9 +198,6 @@ export function DashboardSidebarCloudSection({
(workspace) => workspace.id === hoverCard.hoveredWorkspaceId,
)
: undefined;
const hoveredPullRequest = hoveredWorkspace
? pullRequestOf(hoveredWorkspace)
: null;
useEffect(() => {
if (hoverCard.hoveredWorkspaceId) {
void onWorkspaceHover?.(hoverCard.hoveredWorkspaceId);
Expand Down Expand Up @@ -342,7 +314,7 @@ export function DashboardSidebarCloudSection({
workspace={hoveredWorkspace}
repositories={repositoriesById.get(hoveredWorkspace.id) ?? []}
tasks={tasksById.get(hoveredWorkspace.id) ?? []}
pullRequests={hoveredPullRequest ? [hoveredPullRequest] : []}
pullRequests={pullRequestsById.get(hoveredWorkspace.id) ?? []}
now={now}
onOpenDetails={() =>
navigate({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { describe, expect, it } from "bun:test";
import { cloudPullRequestRefKey } from "renderer/routes/_authenticated/_dashboard/hooks/useCloudPullRequests";
import {
type BuildCloudSidebarWorkspacesParams,
buildCloudSidebarWorkspaces,
Expand Down Expand Up @@ -48,7 +47,7 @@ function build(overrides: Partial<BuildCloudSidebarWorkspacesParams> = {}) {
projects: [],
repoFullNameById: new Map(),
branchById: new Map(),
pullRequestsByRef: new Map(),
pullRequestsById: new Map(),
unknownRepoProjectName: "Cloud",
...overrides,
});
Expand Down Expand Up @@ -188,20 +187,14 @@ describe("buildCloudSidebarWorkspaces", () => {
expect(rows[0]?.lastActivityAt).toBe(DATE.getTime());
});

it("looks the pull request up by repo and the live branch", () => {
it("badges the workspace's first pull request, on the live branch", () => {
const pullRequest = makePullRequest({ isDraft: true });
const layout = build({
cloudWorkspaces: [makeCloudWorkspace({ id: "ws-1", branch: "stale" })],
repoFullNameById: new Map([["ws-1", "acme/app"]]),
branchById: new Map([["ws-1", "live"]]),
pullRequestsByRef: new Map([
[
cloudPullRequestRefKey({
repoFullName: "acme/app",
headBranch: "live",
}),
pullRequest,
],
pullRequestsById: new Map([
["ws-1", [pullRequest, makePullRequest({ number: 8 })]],
]),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
} from "@superset/shared/pull-request";
import { resolveProjectIconUrl } from "renderer/hooks/host-projects/resolveProjectIconUrl";
import { cloudRepoProjectId } from "renderer/lib/github/cloudRepoProjectId";
import { cloudPullRequestRefKey } from "renderer/routes/_authenticated/_dashboard/hooks/useCloudPullRequests";
import { getV2WorkspaceDisplayName } from "renderer/utils/getV2WorkspaceDisplayName";
import type {
DashboardSidebarWorkspace,
Expand Down Expand Up @@ -85,8 +84,11 @@ export interface BuildCloudSidebarWorkspacesParams {
repoFullNameById: ReadonlyMap<string, string>;
/** Live branch per cloud workspace id, for the ones whose sandbox is a host. */
branchById: ReadonlyMap<string, string>;
/** Keyed by {@link cloudPullRequestRefKey}. */
pullRequestsByRef: ReadonlyMap<string, CloudSidebarPullRequestInput>;
/** Per cloud workspace id, the one to show first. */
pullRequestsById: ReadonlyMap<
string,
readonly CloudSidebarPullRequestInput[]
>;
/** Name of the stand-in project for rows whose repo isn't known yet. */
unknownRepoProjectName: string;
}
Expand All @@ -111,7 +113,7 @@ export function buildCloudSidebarWorkspaces({
projects,
repoFullNameById,
branchById,
pullRequestsByRef,
pullRequestsById,
unknownRepoProjectName,
}: BuildCloudSidebarWorkspacesParams): CloudSidebarWorkspacesLayout {
const projectIdByRepo = new Map<string, string>();
Expand All @@ -134,11 +136,7 @@ export function buildCloudSidebarWorkspaces({
? projectIdByRepo.get(repoFullName.toLowerCase())
: undefined;
const projectId = existingProjectId ?? cloudRepoProjectId(repoFullName);
const pullRequest = repoFullName
? pullRequestsByRef.get(
cloudPullRequestRefKey({ repoFullName, headBranch: branch }),
)
: undefined;
const pullRequest = pullRequestsById.get(workspace.id)?.[0];

const row: DashboardSidebarWorkspace = {
id: workspace.id,
Expand Down
Loading
Loading