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
1,740 changes: 1,740 additions & 0 deletions apps/desktop/src/lib/trpc/routers/workspaces/github-extended.ts

Large diffs are not rendered by default.

1,762 changes: 38 additions & 1,724 deletions apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ export function readCachedGitHubCommitAuthor(
}

export function clearGitHubCachesForWorktree(worktreePath: string): void {
githubStatusResource.invalidate(worktreePath);
githubStatusResource.invalidatePrefix(worktreePath);
Comment thread
MocA-Love marked this conversation as resolved.
repoContextResource.invalidate(worktreePath);
recordGitHubCacheMetric({
kind: "status",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,9 @@ async function resolveAttachedPullRequest({

async function resolvePullRequestCommentsTarget(
worktreePath: string,
branchName?: string | null,
): Promise<PullRequestCommentsTarget | null> {
const githubStatus = await fetchGitHubPRStatus(worktreePath);
const githubStatus = await fetchGitHubPRStatus(worktreePath, branchName);
if (!githubStatus?.pr) {
return null;
}
Expand Down Expand Up @@ -265,26 +266,36 @@ async function refreshGitHubPRComments({
}

/**
* Fetches GitHub PR status for a worktree using the `gh` CLI.
* Fetches GitHub PR status for a worktree or branch workspace using the `gh` CLI.
* Returns null if `gh` is not installed, not authenticated, or on error.
*
* @param branchName - Optional branch name override. Used **only** to scope the
* cache key so multiple branch workspaces sharing a main-repo path do not
* cross-contaminate each other's PR status. The inner `refreshGitHubPRStatus`
* call still resolves SHA/upstream from the repo's currently checked-out
* branch — fully propagating the override inside the refresh path is out of
* scope for this PR because the fork's PR attachment / resolution helpers
* (`resolveGitHubStatusContext`, `resolveAttachedPullRequest`) differ from
* upstream and need a separate rework. Tracked as follow-up work.
*/
export async function fetchGitHubPRStatus(
worktreePath: string,
branchName?: string | null,
): Promise<GitHubStatus | null> {
const cacheKey = branchName ? `${worktreePath}::${branchName}` : worktreePath;
if (isRateLimited()) {
// When rate limited, return stale cache or null — never throw,
// and never overwrite stale cache with null
const cached = getCachedGitHubStatus(worktreePath);
const cached = getCachedGitHubStatus(cacheKey);
trackGitHubOperationEvent({
name: "status_refresh",
category: "sync",
worktreePath,
success:
cached !== null || getCachedGitHubStatusState(worktreePath) !== null,
success: cached !== null || getCachedGitHubStatusState(cacheKey) !== null,
durationMs: 0,
rateLimited: true,
error:
cached === null && getCachedGitHubStatusState(worktreePath) === null
cached === null && getCachedGitHubStatusState(cacheKey) === null
? "Rate limited without cached status"
: undefined,
});
Expand All @@ -295,7 +306,7 @@ export async function fetchGitHubPRStatus(
category: "sync",
worktreePath,
fn: () =>
readCachedGitHubStatus(worktreePath, () =>
readCachedGitHubStatus(cacheKey, () =>
rateLimitedRefresh(() => refreshGitHubPRStatus(worktreePath)),
Comment thread
MocA-Love marked this conversation as resolved.
),
});
Expand All @@ -317,9 +328,11 @@ async function rateLimitedRefresh<T>(fn: () => Promise<T>): Promise<T> {
export async function fetchGitHubPRComments({
worktreePath,
pullRequest,
branchName,
}: {
worktreePath: string;
pullRequest?: PullRequestCommentsTarget | null;
branchName?: string | null;
}): Promise<PullRequestComment[]> {
if (isRateLimited()) {
trackGitHubOperationEvent({
Expand All @@ -339,7 +352,8 @@ export async function fetchGitHubPRComments({
worktreePath,
fn: async () => {
const pullRequestTarget =
pullRequest ?? (await resolvePullRequestCommentsTarget(worktreePath));
pullRequest ??
(await resolvePullRequestCommentsTarget(worktreePath, branchName));
if (!pullRequestTarget) {
return [];
}
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { mergeRouters } from "../..";
import { mergeRouters, router } from "../..";
import { createGithubExtendedRouter } from "./github-extended";
import { createCreateProcedures } from "./procedures/create";
import { createDeleteProcedures } from "./procedures/delete";
import { createGenerateBranchNameProcedures } from "./procedures/generate-branch-name";
Expand All @@ -14,6 +15,7 @@ export const createWorkspacesRouter = () => {
createDeleteProcedures(),
createQueryProcedures(),
createGitStatusProcedures(),
router({ githubExtended: createGithubExtendedRouter() }),
createStatusProcedures(),
createInitProcedures(),
createSectionsProcedures(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { electronTrpc } from "renderer/lib/electron-trpc";
export function useCleanupMissingWorktrees() {
const utils = electronTrpc.useUtils();

return electronTrpc.workspaces.cleanupMissingWorktrees.useMutation({
onSuccess: async () => {
await utils.workspaces.invalidate();
await utils.projects.getRecents.invalidate();
return electronTrpc.workspaces.githubExtended.cleanupMissingWorktrees.useMutation(
{
onSuccess: async () => {
await utils.workspaces.invalidate();
await utils.projects.getRecents.invalidate();
},
},
});
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function KeepAliveWorkspaces() {
// Notify SyncService which workspace is active so only it gets polled.
// Passing workspaceId=null deactivates all polling (e.g., dashboard view).
const { mutate: setActiveSyncWorkspace } =
electronTrpc.workspaces.setActiveSyncWorkspace.useMutation();
electronTrpc.workspaces.githubExtended.setActiveSyncWorkspace.useMutation();
const prevActiveIdRef = useRef<string | null>(null);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function ProjectWorktreeAutoSync({ projectId }: { projectId: string }) {
// Only pay the cost of listing + existsSync-ing tracked worktrees when the
// project has opted in. Otherwise the query is skipped entirely.
const { data: missingWorktrees = [], isLoading } =
electronTrpc.workspaces.getMissingWorktrees.useQuery(
electronTrpc.workspaces.githubExtended.getMissingWorktrees.useQuery(
{ projectId },
{ enabled: autoRemoveEnabled },
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ function JobSteps({
}: JobStepsProps) {
const [expandedSteps, setExpandedSteps] = useState<Set<number>>(new Set());
const rerunMutation =
electronTrpc.workspaces.rerunPullRequestChecks.useMutation();
electronTrpc.workspaces.githubExtended.rerunPullRequestChecks.useMutation();
const trpcUtils = electronTrpc.useUtils();

const { data: jobResult, isLoading } =
electronTrpc.workspaces.getJobLogs.useQuery(
electronTrpc.workspaces.githubExtended.getJobLogs.useQuery(
{ workspaceId, detailsUrl },
{
staleTime: 3_000,
Expand Down Expand Up @@ -160,7 +160,7 @@ function JobSteps({
toast.success(
`Re-running ${mode === "failed" ? "failed" : "all"} jobs (${result.rerunCount})`,
);
void trpcUtils.workspaces.getJobLogs.invalidate();
void trpcUtils.workspaces.githubExtended.getJobLogs.invalidate();
void trpcUtils.workspaces.getGitHubStatus.invalidate();
} catch {
toast.error("Failed to re-run jobs");
Expand Down Expand Up @@ -442,7 +442,7 @@ export function ActionLogsPane({

// Poll workflow run jobs when runId is present (workflow dispatch case)
const { data: polledJobs } =
electronTrpc.workspaces.getWorkflowRunJobs.useQuery(
electronTrpc.workspaces.githubExtended.getWorkflowRunJobs.useQuery(
{ workspaceId, runId: runId ?? 0 },
{
enabled: !!runId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ function WorkflowRunCard({
onViewLogs: (runId: number) => void;
}) {
const { data: runs = [], isFetching } =
electronTrpc.workspaces.getGitHubWorkflowRuns.useQuery(
electronTrpc.workspaces.githubExtended.getGitHubWorkflowRuns.useQuery(
{
workspaceId,
workflowId: tracked.workflowId,
Expand Down Expand Up @@ -372,7 +372,7 @@ export function RepositoryPanel({ isActive = true }: RepositoryPanelProps) {
data: repositoryOverview,
isLoading,
error,
} = electronTrpc.workspaces.getGitHubRepositoryOverview.useQuery(
} = electronTrpc.workspaces.githubExtended.getGitHubRepositoryOverview.useQuery(
{ workspaceId: workspaceId ?? "" },
{
enabled: !!workspaceId && isActive,
Expand All @@ -381,11 +381,11 @@ export function RepositoryPanel({ isActive = true }: RepositoryPanelProps) {
},
);
const createIssueMutation =
electronTrpc.workspaces.createGitHubIssue.useMutation();
electronTrpc.workspaces.githubExtended.createGitHubIssue.useMutation();
const uploadIssueAssetMutation =
electronTrpc.workspaces.uploadGitHubIssueAsset.useMutation();
electronTrpc.workspaces.githubExtended.uploadGitHubIssueAsset.useMutation();
const dispatchWorkflowMutation =
electronTrpc.workspaces.dispatchGitHubWorkflow.useMutation();
electronTrpc.workspaces.githubExtended.dispatchGitHubWorkflow.useMutation();

const availableAssignees = repositoryOverview?.issueAssignees ?? [];
const availableAssigneesByLogin = useMemo(
Expand Down Expand Up @@ -471,9 +471,11 @@ export function RepositoryPanel({ isActive = true }: RepositoryPanelProps) {
return;
}

await trpcUtils.workspaces.getGitHubRepositoryOverview.invalidate({
workspaceId,
});
await trpcUtils.workspaces.githubExtended.getGitHubRepositoryOverview.invalidate(
{
workspaceId,
},
);
};

const handleCreateIssue = async () => {
Expand Down Expand Up @@ -664,10 +666,11 @@ export function RepositoryPanel({ isActive = true }: RepositoryPanelProps) {
return;
}
try {
const jobs = await trpcUtils.workspaces.getWorkflowRunJobs.fetch({
workspaceId,
runId,
});
const jobs =
await trpcUtils.workspaces.githubExtended.getWorkflowRunJobs.fetch({
workspaceId,
runId,
});
const failedIdx = jobs.findIndex((j) => j.status === "failure");
addActionLogsTab(
workspaceId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,17 +149,17 @@ export function ReviewPanel({
> | null>(null);
const copyToClipboardMutation = electronTrpc.external.copyText.useMutation();
const setPullRequestDraftStateMutation =
electronTrpc.workspaces.setPullRequestDraftState.useMutation();
electronTrpc.workspaces.githubExtended.setPullRequestDraftState.useMutation();
const setPullRequestThreadResolutionMutation =
electronTrpc.workspaces.setPullRequestThreadResolution.useMutation();
electronTrpc.workspaces.githubExtended.setPullRequestThreadResolution.useMutation();
const replyToPullRequestCommentMutation =
electronTrpc.workspaces.replyToPullRequestComment.useMutation();
electronTrpc.workspaces.githubExtended.replyToPullRequestComment.useMutation();
const updatePullRequestReviewersMutation =
electronTrpc.workspaces.updatePullRequestReviewers.useMutation();
electronTrpc.workspaces.githubExtended.updatePullRequestReviewers.useMutation();
const updatePullRequestAssigneesMutation =
electronTrpc.workspaces.updatePullRequestAssignees.useMutation();
electronTrpc.workspaces.githubExtended.updatePullRequestAssignees.useMutation();
const rerunPullRequestChecksMutation =
electronTrpc.workspaces.rerunPullRequestChecks.useMutation();
electronTrpc.workspaces.githubExtended.rerunPullRequestChecks.useMutation();
const candidateKind =
identityPopoverOpen === "assignees" ? "assignee" : "reviewer";
const canEditPullRequest = pr?.state === "open" || pr?.state === "draft";
Expand Down Expand Up @@ -190,19 +190,22 @@ export function ReviewPanel({
const {
data: identityCandidates = [],
isLoading: isIdentityCandidatesLoading,
} = electronTrpc.workspaces.getPullRequestIdentityCandidates.useQuery(
{
workspaceId: resolvedWorkspaceId ?? "",
kind: candidateKind,
pullRequestUrl: pr?.url,
},
{
enabled:
!!resolvedWorkspaceId && !!identityPopoverOpen && !!canEditPullRequest,
staleTime: 60_000,
refetchOnWindowFocus: false,
},
);
} =
electronTrpc.workspaces.githubExtended.getPullRequestIdentityCandidates.useQuery(
{
workspaceId: resolvedWorkspaceId ?? "",
kind: candidateKind,
pullRequestUrl: pr?.url,
},
{
enabled:
!!resolvedWorkspaceId &&
!!identityPopoverOpen &&
!!canEditPullRequest,
staleTime: 60_000,
refetchOnWindowFocus: false,
},
);

useEffect(() => {
return () => {
Expand Down Expand Up @@ -610,7 +613,7 @@ export function ReviewPanel({
: `Re-ran ${result.rerunCount} workflow run${result.rerunCount === 1 ? "" : "s"}`,
);
await refreshReview("status");
void trpcUtils.workspaces.getJobLogs.invalidate();
void trpcUtils.workspaces.githubExtended.getJobLogs.invalidate();
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
toast.error(`Failed to rerun jobs: ${message}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function CheckSteps({ detailsUrl }: CheckStepsProps) {
const workspaceId = useWorkspaceId();

const { data: steps, isLoading } =
electronTrpc.workspaces.getCheckJobSteps.useQuery(
electronTrpc.workspaces.githubExtended.getCheckJobSteps.useQuery(
{ workspaceId: workspaceId ?? "", detailsUrl },
{
enabled: !!workspaceId && !!detailsUrl,
Expand Down
Loading