feat: extend PR context to include check details
Add check details to PR context via includeCheckDetails flag Open a checks dialog showing check run summaries and steps Improve PR lookup for forked repos by matching head branch
This commit is contained in:
@@ -261,7 +261,7 @@ export function GitHubPullRequestPickerDialog({
|
||||
if (startingNumber) return;
|
||||
setStartingNumber(number);
|
||||
try {
|
||||
const prContext = await github.prContext(projectDirectory, number, { includeDiff });
|
||||
const prContext = await github.prContext(projectDirectory, number, { includeDiff, includeCheckDetails: false });
|
||||
if (prContext.connected === false) {
|
||||
toast.error('GitHub not connected');
|
||||
return;
|
||||
|
||||
@@ -9,6 +9,13 @@ import {
|
||||
} from '@remixicon/react';
|
||||
import { toast } from '@/components/ui';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
@@ -19,8 +26,13 @@ import {
|
||||
import { generatePullRequestDescription } from '@/lib/gitApi';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useMessageStore } from '@/stores/messageStore';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import type {
|
||||
GitHubPullRequest,
|
||||
GitHubCheckRun,
|
||||
GitHubPullRequestContextResult,
|
||||
GitHubPullRequestStatus,
|
||||
} from '@/lib/api/types';
|
||||
|
||||
@@ -76,6 +88,8 @@ export const PullRequestSection: React.FC<{
|
||||
const { github } = useRuntimeAPIs();
|
||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||
const setSidebarSection = useUIStore((state) => state.setSidebarSection);
|
||||
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
||||
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
||||
|
||||
const openGitHubSettings = React.useCallback(() => {
|
||||
setSidebarSection('settings');
|
||||
@@ -97,8 +111,224 @@ export const PullRequestSection: React.FC<{
|
||||
const [isMerging, setIsMerging] = React.useState(false);
|
||||
const [isMarkingReady, setIsMarkingReady] = React.useState(false);
|
||||
|
||||
const [checksDialogOpen, setChecksDialogOpen] = React.useState(false);
|
||||
const [checkDetails, setCheckDetails] = React.useState<GitHubPullRequestContextResult | null>(null);
|
||||
const [isLoadingCheckDetails, setIsLoadingCheckDetails] = React.useState(false);
|
||||
|
||||
const canShow = Boolean(directory && branch && baseBranch && branch !== baseBranch);
|
||||
|
||||
const pr = status?.pr ?? null;
|
||||
|
||||
const openChecksDialog = React.useCallback(async () => {
|
||||
if (!github?.prContext) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
return;
|
||||
}
|
||||
if (!pr) return;
|
||||
|
||||
setChecksDialogOpen(true);
|
||||
setIsLoadingCheckDetails(true);
|
||||
try {
|
||||
const ctx = await github.prContext(directory, pr.number, {
|
||||
includeDiff: false,
|
||||
includeCheckDetails: true,
|
||||
});
|
||||
setCheckDetails(ctx);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to load check details', { description: message });
|
||||
} finally {
|
||||
setIsLoadingCheckDetails(false);
|
||||
}
|
||||
}, [directory, github, pr]);
|
||||
|
||||
const renderCheckRunSummary = React.useCallback((run: GitHubCheckRun) => {
|
||||
const status = run.status || 'unknown';
|
||||
const conclusion = run.conclusion ?? undefined;
|
||||
const statusText = conclusion ? `${status} / ${conclusion}` : status;
|
||||
const appName = run.app?.name || run.app?.slug;
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="typography-ui-label text-foreground truncate">{run.name}</div>
|
||||
<div className="typography-micro text-muted-foreground truncate">
|
||||
{appName ? `${appName} · ${statusText}` : statusText}
|
||||
</div>
|
||||
{run.output?.summary ? (
|
||||
<div className="typography-micro text-muted-foreground whitespace-pre-wrap line-clamp-4 mt-2">
|
||||
{run.output.summary}
|
||||
</div>
|
||||
) : null}
|
||||
{run.job?.steps && run.job.steps.length > 0 ? (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="typography-micro text-muted-foreground">Steps</div>
|
||||
<div className="space-y-1">
|
||||
{run.job.steps.map((step, idx) => {
|
||||
const c = (step.conclusion || '').toLowerCase();
|
||||
const isFail = c && !['success', 'neutral', 'skipped'].includes(c);
|
||||
return (
|
||||
<div
|
||||
key={`${step.name}-${idx}`}
|
||||
className={
|
||||
'typography-micro flex items-center gap-2 rounded px-2 py-1 ' +
|
||||
(isFail ? 'bg-destructive/10 text-destructive' : 'text-muted-foreground')
|
||||
}
|
||||
>
|
||||
<span className="truncate">{step.name}</span>
|
||||
{step.conclusion ? <span className="ml-auto flex-shrink-0">{step.conclusion}</span> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{run.detailsUrl ? (
|
||||
<Button variant="outline" size="sm" asChild className="flex-shrink-0">
|
||||
<a href={run.detailsUrl} target="_blank" rel="noopener noreferrer">
|
||||
<RiExternalLinkLine className="size-4" />
|
||||
Open
|
||||
</a>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const sendFailedChecksToChat = React.useCallback(async () => {
|
||||
setActiveMainTab('chat');
|
||||
|
||||
if (!github?.prContext) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
return;
|
||||
}
|
||||
if (!directory || !pr) return;
|
||||
if (!currentSessionId) {
|
||||
toast.error('No active session', { description: 'Open a chat session first.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { currentProviderId, currentModelId, currentAgentName, currentVariant } = useConfigStore.getState();
|
||||
const lastUsedProvider = useMessageStore.getState().lastUsedProvider;
|
||||
const providerID = currentProviderId || lastUsedProvider?.providerID;
|
||||
const modelID = currentModelId || lastUsedProvider?.modelID;
|
||||
if (!providerID || !modelID) {
|
||||
toast.error('No model selected');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const context = await github.prContext(directory, pr.number, { includeDiff: false, includeCheckDetails: true });
|
||||
const runs = context.checkRuns ?? [];
|
||||
const failed = runs.filter((r) => {
|
||||
const conclusion = typeof r.conclusion === 'string' ? r.conclusion.toLowerCase() : '';
|
||||
if (!conclusion) return false;
|
||||
return !['success', 'neutral', 'skipped'].includes(conclusion);
|
||||
});
|
||||
|
||||
if (failed.length === 0) {
|
||||
toast.message('No failed checks');
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleText = 'Review these PR failed checks and propose likely fixes. Do not implement until I confirm.';
|
||||
const instructionsText = `Use the attached checks payload.
|
||||
- Summarize what is failing.
|
||||
- Identify likely root cause(s).
|
||||
- Propose a minimal fix plan and verification steps.
|
||||
- No speculation: ask for missing info if needed.`;
|
||||
const payloadText = `GitHub PR failed checks (JSON)\n${JSON.stringify({
|
||||
repo: context.repo ?? null,
|
||||
pr: context.pr ?? null,
|
||||
failedChecks: failed,
|
||||
}, null, 2)}`;
|
||||
|
||||
await useMessageStore.getState().sendMessage(
|
||||
visibleText,
|
||||
providerID,
|
||||
modelID,
|
||||
currentAgentName ?? undefined,
|
||||
currentSessionId,
|
||||
undefined,
|
||||
null,
|
||||
[
|
||||
{ text: instructionsText, synthetic: true },
|
||||
{ text: payloadText, synthetic: true },
|
||||
],
|
||||
currentVariant
|
||||
);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to load checks', { description: message });
|
||||
}
|
||||
}, [currentSessionId, directory, github, pr, setActiveMainTab]);
|
||||
|
||||
const sendCommentsToChat = React.useCallback(async () => {
|
||||
setActiveMainTab('chat');
|
||||
|
||||
if (!github?.prContext) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
return;
|
||||
}
|
||||
if (!directory || !pr) return;
|
||||
if (!currentSessionId) {
|
||||
toast.error('No active session', { description: 'Open a chat session first.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { currentProviderId, currentModelId, currentAgentName, currentVariant } = useConfigStore.getState();
|
||||
const lastUsedProvider = useMessageStore.getState().lastUsedProvider;
|
||||
const providerID = currentProviderId || lastUsedProvider?.providerID;
|
||||
const modelID = currentModelId || lastUsedProvider?.modelID;
|
||||
if (!providerID || !modelID) {
|
||||
toast.error('No model selected');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const context = await github.prContext(directory, pr.number, { includeDiff: false, includeCheckDetails: false });
|
||||
const issueComments = context.issueComments ?? [];
|
||||
const reviewComments = context.reviewComments ?? [];
|
||||
const total = issueComments.length + reviewComments.length;
|
||||
if (total === 0) {
|
||||
toast.message('No PR comments');
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleText = 'Review these PR comments and propose the required changes and next actions. Do not implement until I confirm.';
|
||||
const instructionsText = `Use the attached comments payload.
|
||||
- Identify required vs optional changes.
|
||||
- Call out intent/implementation mismatch if present.
|
||||
- Propose a minimal plan and verification steps.
|
||||
- No speculation: ask for missing info if needed.`;
|
||||
const payloadText = `GitHub PR comments (JSON)\n${JSON.stringify({
|
||||
repo: context.repo ?? null,
|
||||
pr: context.pr ?? null,
|
||||
issueComments,
|
||||
reviewComments,
|
||||
}, null, 2)}`;
|
||||
|
||||
await useMessageStore.getState().sendMessage(
|
||||
visibleText,
|
||||
providerID,
|
||||
modelID,
|
||||
currentAgentName ?? undefined,
|
||||
currentSessionId,
|
||||
undefined,
|
||||
null,
|
||||
[
|
||||
{ text: instructionsText, synthetic: true },
|
||||
{ text: payloadText, synthetic: true },
|
||||
],
|
||||
currentVariant
|
||||
);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to load PR comments', { description: message });
|
||||
}
|
||||
}, [currentSessionId, directory, github, pr, setActiveMainTab]);
|
||||
|
||||
const refresh = React.useCallback(async () => {
|
||||
if (!canShow) return;
|
||||
if (!github?.prStatus) {
|
||||
@@ -236,7 +466,6 @@ export const PullRequestSection: React.FC<{
|
||||
return null;
|
||||
}
|
||||
|
||||
const pr = status?.pr ?? null;
|
||||
const repoUrl = status?.repo?.url || null;
|
||||
const checks = status?.checks ?? null;
|
||||
const canMerge = Boolean(status?.canMerge);
|
||||
@@ -306,6 +535,27 @@ export const PullRequestSection: React.FC<{
|
||||
{pr.mergeable === false ? ' · not mergeable' : ''}
|
||||
{typeof pr.mergeableState === 'string' && pr.mergeableState ? ` · ${pr.mergeableState}` : ''}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
{checks ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={openChecksDialog}
|
||||
disabled={isLoadingCheckDetails}
|
||||
>
|
||||
{isLoadingCheckDetails ? <RiLoader4Line className="size-4 animate-spin" /> : null}
|
||||
Check details
|
||||
</Button>
|
||||
) : null}
|
||||
{checks?.failure ? (
|
||||
<Button variant="outline" size="sm" onClick={sendFailedChecksToChat}>
|
||||
Send failed checks to chat
|
||||
</Button>
|
||||
) : null}
|
||||
<Button variant="outline" size="sm" onClick={sendCommentsToChat}>
|
||||
Send PR comments to chat
|
||||
</Button>
|
||||
</div>
|
||||
{canMerge && pr.draft ? (
|
||||
<div className="typography-micro text-muted-foreground">
|
||||
Draft PRs must be marked ready before merge.
|
||||
@@ -450,6 +700,50 @@ export const PullRequestSection: React.FC<{
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
|
||||
<Dialog open={checksDialogOpen} onOpenChange={setChecksDialogOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[70vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<RiGitPullRequestLine className="h-5 w-5" />
|
||||
Check Details
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{pr ? `PR #${pr.number}` : 'Pull request'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto mt-2">
|
||||
{isLoadingCheckDetails ? (
|
||||
<div className="text-center text-muted-foreground py-8 flex items-center justify-center gap-2">
|
||||
<RiLoader4Line className="h-4 w-4 animate-spin" />
|
||||
Loading...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isLoadingCheckDetails ? (
|
||||
<div className="space-y-3">
|
||||
{Array.isArray(checkDetails?.checkRuns) && checkDetails?.checkRuns.length > 0 ? (
|
||||
checkDetails.checkRuns.map((run, idx) => (
|
||||
<div key={`${run.name}-${idx}`} className="rounded-md border border-border/60 p-3">
|
||||
{renderCheckRunSummary(run)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground py-8">No check details available.</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<div className="flex-1" />
|
||||
<Button variant="outline" onClick={() => setChecksDialogOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -509,6 +509,31 @@ export type GitHubChecksSummary = {
|
||||
pending: number;
|
||||
};
|
||||
|
||||
export type GitHubCheckRun = {
|
||||
id?: number;
|
||||
name: string;
|
||||
app?: {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
};
|
||||
status?: string;
|
||||
conclusion?: string | null;
|
||||
detailsUrl?: string;
|
||||
output?: {
|
||||
title?: string;
|
||||
summary?: string;
|
||||
text?: string;
|
||||
};
|
||||
job?: {
|
||||
runId?: number;
|
||||
jobId?: number;
|
||||
url?: string;
|
||||
name?: string;
|
||||
conclusion?: string | null;
|
||||
steps?: Array<{ name: string; status?: string; conclusion?: string | null; number?: number }>;
|
||||
};
|
||||
};
|
||||
|
||||
export type GitHubPullRequest = {
|
||||
number: number;
|
||||
title: string;
|
||||
@@ -576,6 +601,7 @@ export type GitHubPullRequestContextResult = {
|
||||
files?: GitHubPullRequestFile[];
|
||||
diff?: string;
|
||||
checks?: GitHubChecksSummary | null;
|
||||
checkRuns?: GitHubCheckRun[];
|
||||
};
|
||||
|
||||
export type GitHubPullRequestStatus = {
|
||||
@@ -699,7 +725,11 @@ export interface GitHubAPI {
|
||||
prReady(payload: GitHubPullRequestReadyInput): Promise<GitHubPullRequestReadyResult>;
|
||||
|
||||
prsList(directory: string, options?: { page?: number }): Promise<GitHubPullRequestsListResult>;
|
||||
prContext(directory: string, number: number, options?: { includeDiff?: boolean }): Promise<GitHubPullRequestContextResult>;
|
||||
prContext(
|
||||
directory: string,
|
||||
number: number,
|
||||
options?: { includeDiff?: boolean; includeCheckDetails?: boolean }
|
||||
): Promise<GitHubPullRequestContextResult>;
|
||||
|
||||
issuesList(directory: string, options?: { page?: number }): Promise<GitHubIssuesListResult>;
|
||||
issueGet(directory: string, number: number): Promise<GitHubIssueGetResult>;
|
||||
|
||||
@@ -340,11 +340,12 @@ interface MessageState {
|
||||
sessionAbortFlags: Map<string, SessionAbortRecord>;
|
||||
pendingAssistantHeaderSessions: Set<string>;
|
||||
pendingUserMessageMetaBySession: Map<string, { mode?: string; providerID?: string; modelID?: string; variant?: string }>;
|
||||
|
||||
}
|
||||
|
||||
interface MessageActions {
|
||||
loadMessages: (sessionId: string, limit?: number) => Promise<void>;
|
||||
sendMessage: (content: string, providerID: string, modelID: string, agent?: string, currentSessionId?: string, attachments?: AttachedFile[], agentMentionName?: string | null, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>, variant?: string) => Promise<void>;
|
||||
sendMessage: (content: string, providerID: string, modelID: string, agent?: string, currentSessionId?: string, attachments?: AttachedFile[], agentMentionName?: string | null, additionalParts?: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean }>, variant?: string) => Promise<void>;
|
||||
abortCurrentOperation: (currentSessionId?: string) => Promise<void>;
|
||||
_addStreamingPartImmediate: (sessionId: string, messageId: string, part: Part, role?: string, currentSessionId?: string) => void;
|
||||
addStreamingPart: (sessionId: string, messageId: string, part: Part, role?: string, currentSessionId?: string) => void;
|
||||
@@ -552,7 +553,7 @@ export const useMessageStore = create<MessageStore>()(
|
||||
});
|
||||
},
|
||||
|
||||
sendMessage: async (content: string, providerID: string, modelID: string, agent?: string, currentSessionId?: string, attachments?: AttachedFile[], agentMentionName?: string | null, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>, variant?: string) => {
|
||||
sendMessage: async (content: string, providerID: string, modelID: string, agent?: string, currentSessionId?: string, attachments?: AttachedFile[], agentMentionName?: string | null, additionalParts?: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean }>, variant?: string) => {
|
||||
if (!currentSessionId) {
|
||||
throw new Error("No session selected");
|
||||
}
|
||||
@@ -677,6 +678,7 @@ export const useMessageStore = create<MessageStore>()(
|
||||
// Convert additional parts to SDK format
|
||||
const additionalPartsPayload = additionalParts?.map((part) => ({
|
||||
text: part.text,
|
||||
synthetic: part.synthetic,
|
||||
files: part.attachments?.map((file) => ({
|
||||
type: "file" as const,
|
||||
mime: file.mimeType,
|
||||
@@ -693,7 +695,7 @@ export const useMessageStore = create<MessageStore>()(
|
||||
agent,
|
||||
variant,
|
||||
files: filePayloads.length > 0 ? filePayloads : undefined,
|
||||
additionalParts: additionalPartsPayload,
|
||||
additionalParts: additionalPartsPayload && additionalPartsPayload.length > 0 ? additionalPartsPayload : undefined,
|
||||
agentMentions: agentMentionName ? [{ name: agentMentionName }] : undefined,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user