-
-
{message}
+
+
+
Where this was found
+
+ {uniqueLocations.length} code {uniqueLocations.length === 1 ? 'location' : 'locations'}
+
- {detail &&
{detail}
}
- {children &&
{children}
}
+
+ {visibleLocations.map(location => (
+ -
+
+ {location}
+
+ ))}
+
+ {uniqueLocations.length > 2 && (
+
+ )}
);
}
-function ErrorPanel({
- message,
- retryLabel,
- onRetry,
- disabled,
+type RemediationPresentationSummary = {
+ prUrl: string | null;
+ prNumber: number | null;
+ prDraft: boolean | null;
+ failureCode: string | null;
+ blockedReason: string | null;
+};
+
+function getRemediationPresentation({
+ status,
+ finding,
+ latestAttempt,
+ summary,
+ canStart,
+ unavailableReason,
+ unavailableCopy,
+ isAwaitingStart,
}: {
- message: string;
- retryLabel: string;
- onRetry: () => void;
- disabled: boolean;
-}) {
- return (
-
-
{message}
-
-
- );
-}
+ status: string | null;
+ finding: SecurityFinding;
+ latestAttempt: RemediationAttempt | null;
+ summary: RemediationPresentationSummary;
+ canStart: boolean;
+ unavailableReason: string | null | undefined;
+ unavailableCopy: string | null;
+ isAwaitingStart: boolean;
+}): RemediationPresentation {
+ const cancellationRequested = Boolean(latestAttempt?.cancellationRequestedAt);
+ const requester = latestAttempt?.origin === 'manual' ? 'User request' : 'Security Agent';
+ const prUrl = latestAttempt?.prUrl ?? summary.prUrl;
+ const prNumber = latestAttempt?.prNumber ?? summary.prNumber;
+ const prDraft = latestAttempt?.prDraft ?? summary.prDraft;
+ const failureCode = latestAttempt?.failureCode ?? summary.failureCode;
+ const blockedReason = latestAttempt?.blockedReason ?? summary.blockedReason;
-function EmptyPanel({ children, text }: { children?: React.ReactNode; text: string }) {
- return (
-
- );
+ if (isAwaitingStart) {
+ return {
+ hero: {
+ title: 'Queueing remediation',
+ description:
+ 'Security Agent is creating the remediation attempt and reserving its repository branch.',
+ icon: Loader2,
+ tone: 'warning',
+ spinning: true,
+ },
+ summary: [
+ {
+ label: 'Current state',
+ value: 'Request in progress',
+ detail: 'The attempt is not persisted yet',
+ icon: Clock3,
+ tone: 'warning',
+ },
+ {
+ label: 'How did it start?',
+ value: 'Manual request',
+ detail: 'Duplicate starts remain suppressed',
+ icon: UserRound,
+ tone: 'neutral',
+ },
+ {
+ label: 'What happens next?',
+ value: 'Cloud Agent queue',
+ detail: 'Status refreshes automatically',
+ icon: Sparkles,
+ tone: 'neutral',
+ },
+ ],
+ context:
+ 'Starting remediation creates an attempt. It does not mark the Security Finding fixed.',
+ action: {
+ label: 'Current status',
+ title: 'Creating the remediation attempt',
+ description: 'Wait for Security Agent to confirm the queued attempt.',
+ },
+ disclosureTitle: 'Remediation progress',
+ steps: [
+ {
+ title: 'Create remediation attempt',
+ detail: 'Security Agent is validating and saving the request.',
+ state: 'running',
+ },
+ {
+ title: 'Wait for Cloud Agent',
+ detail: 'Execution begins after the request is accepted.',
+ state: 'pending',
+ },
+ ],
+ };
+ }
+
+ if (cancellationRequested && isActiveRemediationStatus(status)) {
+ return {
+ hero: {
+ title: 'Cancellation has been requested',
+ description:
+ 'Security Agent asked Cloud Agent to stop. The attempt remains active until Cloud Agent confirms cancellation or returns another final result.',
+ icon: Loader2,
+ tone: 'warning',
+ spinning: true,
+ },
+ summary: [
+ {
+ label: 'Current state',
+ value: 'Waiting for confirmation',
+ detail: 'The attempt is not cancelled yet',
+ icon: Clock3,
+ tone: 'warning',
+ },
+ {
+ label: 'Cancellation requested',
+ value: latestAttempt?.cancellationRequestedAt
+ ? formatUtcDate(latestAttempt.cancellationRequestedAt)
+ : 'Recently',
+ detail: 'Security Agent accepted the request',
+ icon: UserRound,
+ tone: 'neutral',
+ },
+ {
+ label: 'Possible outcome',
+ value: 'Cancelled or PR opened',
+ detail: 'A pull request can still win the race',
+ icon: GitPullRequest,
+ tone: 'warning',
+ },
+ ],
+ context:
+ 'Cancellation is best effort. If Cloud Agent opens a verified pull request before stopping, Security Agent will show that result.',
+ action: {
+ label: 'Current status',
+ title: 'Waiting for Cloud Agent',
+ description:
+ 'No further action is available until Cloud Agent confirms cancellation or returns a pull request.',
+ },
+ disclosureTitle: 'Cancellation progress',
+ steps: [
+ {
+ title: 'Remediation started',
+ detail: 'Cloud Agent began repository work.',
+ state: 'done',
+ },
+ {
+ title: 'Cancellation requested',
+ detail: 'Security Agent accepted the request and asked Cloud Agent to interrupt.',
+ state: 'done',
+ },
+ {
+ title: 'Waiting for Cloud Agent',
+ detail: 'Cloud Agent has not confirmed interruption or returned a pull request.',
+ state: 'waiting',
+ },
+ ],
+ };
+ }
+
+ if (!status && canStart) {
+ return {
+ hero: {
+ title: 'Ready to prepare a fix',
+ description: `Security Agent can ask Cloud Agent to update ${finding.package_name}, review affected usage, and open a pull request for your team.`,
+ icon: GitPullRequest,
+ tone: 'neutral',
+ },
+ summary: [
+ {
+ label: 'Why available?',
+ value: 'Concrete fix path',
+ detail: 'Analysis supports a user-reviewed repository change',
+ icon: ShieldCheck,
+ tone: 'success',
+ },
+ {
+ label: 'How does it start?',
+ value: 'Manual request',
+ detail: 'This action is independent of Auto Remediation',
+ icon: UserRound,
+ tone: 'neutral',
+ },
+ {
+ label: 'What happens next?',
+ value: 'Review a pull request',
+ detail: 'The finding remains open until source sync',
+ icon: GitPullRequest,
+ tone: 'neutral',
+ },
+ ],
+ context:
+ 'Starting remediation creates a Security Remediation Attempt. It does not mark the finding fixed.',
+ action: {
+ label: 'Next step',
+ title: 'Prepare a fix',
+ description:
+ 'Cloud Agent will make the smallest safe change it can identify, run focused checks, and open a pull request for review.',
+ },
+ disclosureTitle: 'Why remediation is available',
+ steps: [
+ {
+ title: 'Finding is still open',
+ detail: 'GitHub continues to report the vulnerable package in this repository.',
+ state: 'done',
+ },
+ {
+ title: 'Analysis is current',
+ detail: 'The latest codebase analysis matches current finding data.',
+ state: 'done',
+ },
+ {
+ title: 'A concrete response is available',
+ detail: 'Analysis provides enough evidence for a user-reviewed remediation attempt.',
+ state: 'done',
+ },
+ {
+ title: 'No remediation is active',
+ detail: 'No other attempt or known remediation pull request blocks a new start.',
+ state: 'done',
+ },
+ ],
+ };
+ }
+
+ if (!status) {
+ const analysisRequired = isCodebaseAnalysisRequiredReason(unavailableReason);
+ return {
+ hero: {
+ title: analysisRequired ? 'Analyze the repository first' : 'Remediation is unavailable',
+ description:
+ unavailableCopy ||
+ 'Security Agent cannot start a remediation attempt for this finding in its current state.',
+ icon: analysisRequired ? Brain : Ban,
+ tone: 'warning',
+ },
+ summary: [
+ {
+ label: 'What is known?',
+ value: 'Published vulnerability',
+ detail: 'The source advisory remains available',
+ icon: ShieldAlert,
+ tone: 'warning',
+ },
+ {
+ label: 'What is missing?',
+ value: analysisRequired ? 'Repository analysis' : 'An eligible fix path',
+ detail: unavailableCopy || 'Current safety gates block a new attempt',
+ icon: analysisRequired ? Brain : Ban,
+ tone: 'warning',
+ },
+ {
+ label: 'What should I do?',
+ value: analysisRequired ? 'Analyze repository' : 'Review finding state',
+ detail: 'Resolve the recorded blocker before starting remediation',
+ icon: Search,
+ tone: 'neutral',
+ },
+ ],
+ context:
+ 'Security Agent derives remediation availability from server-provided safety and policy checks.',
+ action: {
+ label: 'Next step',
+ title: analysisRequired ? 'Check repository risk and fix options' : 'Resolve the blocker',
+ description:
+ unavailableCopy || 'Review analysis and finding status before trying remediation again.',
+ },
+ disclosureTitle: analysisRequired
+ ? 'What blocks remediation'
+ : 'Why remediation is unavailable',
+ steps: [
+ {
+ title: 'Source advisory is available',
+ detail: `${finding.package_name} matches the published affected range.`,
+ state: 'done',
+ },
+ {
+ title: analysisRequired
+ ? 'Repository analysis is required'
+ : 'A safety gate blocks remediation',
+ detail: unavailableCopy || 'Security Agent cannot safely admit a new attempt.',
+ state: 'attention',
+ },
+ {
+ title: 'Remediation decision is pending',
+ detail: 'A new attempt remains unavailable until the blocker is resolved.',
+ state: 'pending',
+ },
+ ],
+ };
+ }
+
+ if (status === 'queued') {
+ return {
+ hero: {
+ title: 'Remediation is queued',
+ description:
+ 'Security Agent accepted the request. Cloud Agent will begin when execution capacity is available.',
+ icon: Loader2,
+ tone: 'warning',
+ spinning: true,
+ },
+ summary: [
+ {
+ label: 'Current state',
+ value: 'Waiting for capacity',
+ detail: 'No repository changes have started',
+ icon: Clock3,
+ tone: 'warning',
+ },
+ {
+ label: 'Attempt started by',
+ value: requester,
+ detail: formatRemediationOrigin(latestAttempt?.origin ?? 'manual'),
+ icon: UserRound,
+ tone: 'neutral',
+ },
+ {
+ label: 'Next update',
+ value: 'Cloud Agent starts',
+ detail: 'Status refreshes automatically',
+ icon: Sparkles,
+ tone: 'neutral',
+ },
+ ],
+ context:
+ 'Only one active remediation attempt can exist for this finding. Another start is unavailable while this attempt is queued.',
+ action: {
+ label: 'Attempt controls',
+ title: 'Manage this attempt',
+ description:
+ 'You can cancel before Cloud Agent starts if this remediation is no longer needed.',
+ },
+ disclosureTitle: 'Remediation progress',
+ steps: [
+ {
+ title: 'Request accepted',
+ detail: `Security Agent created attempt #${latestAttempt?.attemptNumber ?? 1}.`,
+ state: 'done',
+ },
+ {
+ title: 'Waiting for Cloud Agent',
+ detail: 'The attempt will start when execution capacity is available.',
+ state: 'waiting',
+ },
+ {
+ title: 'Prepare repository change',
+ detail: 'Cloud Agent has not started repository work.',
+ state: 'pending',
+ },
+ {
+ title: 'Open pull request',
+ detail: 'No pull request exists yet.',
+ state: 'pending',
+ },
+ ],
+ };
+ }
+
+ if (status === 'launching') {
+ return {
+ hero: {
+ title: 'Cloud Agent is starting',
+ description:
+ 'Security Agent claimed the queued attempt and is opening the isolated session that will prepare the repository change.',
+ icon: Loader2,
+ tone: 'warning',
+ spinning: true,
+ },
+ summary: [
+ {
+ label: 'Current state',
+ value: 'Launching session',
+ detail: 'Repository work has not been confirmed yet',
+ icon: Sparkles,
+ tone: 'warning',
+ },
+ {
+ label: 'Attempt started by',
+ value: requester,
+ detail: formatRemediationOrigin(latestAttempt?.origin ?? 'manual'),
+ icon: UserRound,
+ tone: 'neutral',
+ },
+ {
+ label: 'Next update',
+ value: 'Repository work begins',
+ detail: 'Status refreshes automatically',
+ icon: GitBranch,
+ tone: 'neutral',
+ },
+ ],
+ context:
+ 'Launching is an active Security Remediation Attempt. A launch failure can return the attempt to queued or end it as failed.',
+ action: {
+ label: 'Attempt controls',
+ title: 'Manage this attempt',
+ description:
+ 'You can request cancellation while Security Agent finishes starting the session.',
+ },
+ disclosureTitle: 'Remediation progress',
+ steps: [
+ {
+ title: 'Request accepted',
+ detail: `Security Agent created attempt #${latestAttempt?.attemptNumber ?? 1}.`,
+ state: 'done',
+ },
+ {
+ title: 'Cloud Agent session starting',
+ detail: 'The attempt is creating an isolated execution session.',
+ state: 'running',
+ },
+ {
+ title: 'Prepare repository change',
+ detail: 'Repository work has not started yet.',
+ state: 'pending',
+ },
+ {
+ title: 'Open pull request',
+ detail: 'No pull request exists yet.',
+ state: 'pending',
+ },
+ ],
+ };
+ }
+
+ if (status === 'running') {
+ return {
+ hero: {
+ title: 'Cloud Agent is preparing a fix',
+ description:
+ 'The agent is updating the dependency, reviewing affected usage, and running focused validation before deciding whether to open a pull request.',
+ icon: Loader2,
+ tone: 'warning',
+ spinning: true,
+ },
+ summary: [
+ {
+ label: 'Current state',
+ value: 'Repository work',
+ detail: 'Changes and validation are in progress',
+ icon: Wrench,
+ tone: 'warning',
+ },
+ {
+ label: 'Attempt started by',
+ value: requester,
+ detail: formatRemediationOrigin(latestAttempt?.origin ?? 'manual'),
+ icon: UserRound,
+ tone: 'neutral',
+ },
+ {
+ label: 'Expected outcome',
+ value: 'Pull request or explanation',
+ detail: 'No-change and failure outcomes stay explicit',
+ icon: GitPullRequest,
+ tone: 'neutral',
+ },
+ ],
+ context:
+ 'A running remediation does not change finding status. The source record remains open until GitHub reports it fixed or it is dismissed.',
+ action: {
+ label: 'Attempt controls',
+ title: 'Manage this attempt',
+ description:
+ 'Status updates automatically. Cancellation asks Cloud Agent to stop but cannot guarantee it stops before opening a pull request.',
+ },
+ disclosureTitle: 'Remediation progress',
+ steps: [
+ {
+ title: 'Request accepted',
+ detail: `Security Agent created attempt #${latestAttempt?.attemptNumber ?? 1}.`,
+ state: 'done',
+ },
+ {
+ title: 'Cloud Agent started',
+ detail: 'The isolated session and remediation branch are ready.',
+ state: 'done',
+ },
+ {
+ title: 'Prepare and validate change',
+ detail: `Cloud Agent is working on ${finding.package_name} and affected usage.`,
+ state: 'running',
+ },
+ {
+ title: 'Open pull request',
+ detail: 'No verified pull request exists yet.',
+ state: 'pending',
+ },
+ ],
+ };
+ }
+
+ if (status === 'pr_opened') {
+ const draft = Boolean(prDraft);
+ return {
+ hero: {
+ title: draft
+ ? 'Draft remediation pull request is ready'
+ : 'Remediation pull request is ready',
+ description: draft
+ ? 'Cloud Agent prepared a concrete fix, but incomplete validation or recorded risk needs reviewer attention.'
+ : `Cloud Agent prepared a repository change and opened${prNumber ? ` pull request #${prNumber}` : ' a pull request'} for team review.`,
+ icon: GitPullRequest,
+ tone: draft ? 'warning' : 'success',
+ },
+ summary: [
+ {
+ label: 'Outcome',
+ value: `${draft ? 'Draft ' : ''}PR${prNumber ? ` #${prNumber}` : ' opened'}`,
+ detail: 'Expected repository and remediation branch recorded',
+ icon: GitPullRequest,
+ tone: 'success',
+ },
+ {
+ label: 'Validation',
+ value: latestAttempt?.validationEvidence?.length
+ ? 'Evidence recorded'
+ : 'Review required',
+ detail: draft
+ ? latestAttempt?.draftReason || 'Validation or risk requires reviewer attention'
+ : 'Review recorded checks before merging',
+ icon: FileCheck2,
+ tone: draft ? 'warning' : 'success',
+ },
+ {
+ label: 'Finding state',
+ value: 'Still open',
+ detail: 'GitHub confirms when the vulnerability is fixed',
+ icon: ShieldAlert,
+ tone: 'warning',
+ },
+ ],
+ context:
+ 'A pull request is not the same as a fixed finding. The finding stays open until GitHub reports the vulnerability resolved.',
+ action: {
+ label: 'Next step',
+ title: draft ? 'Review validation gaps and code changes' : 'Review the pull request',
+ description: draft
+ ? latestAttempt?.draftReason ||
+ 'Run missing validation and review recorded risks before marking the pull request ready.'
+ : 'Review the dependency update and related code changes before merging into the default branch.',
+ },
+ disclosureTitle: draft ? 'Why the pull request is a draft' : 'How remediation completed',
+ steps: [
+ {
+ title: 'Prepared a concrete fix',
+ detail: 'Cloud Agent recorded a repository change for this finding.',
+ state: 'done',
+ },
+ {
+ title: 'Ran available validation',
+ detail: latestAttempt?.validationEvidence?.length
+ ? `${latestAttempt.validationEvidence.length} validation ${latestAttempt.validationEvidence.length === 1 ? 'record' : 'records'} available in attempt history.`
+ : 'No structured validation evidence was recorded.',
+ state: draft ? 'attention' : 'done',
+ },
+ {
+ title: 'Verified pull request outcome',
+ detail: prNumber
+ ? `Pull request #${prNumber} belongs to the recorded remediation attempt.`
+ : 'A pull request URL is recorded for this remediation attempt.',
+ state: 'done',
+ },
+ ],
+ };
+ }
+
+ if (status === 'blocked') {
+ return {
+ hero: {
+ title: 'Remediation was blocked',
+ description:
+ blockedReason ||
+ 'Security Agent intentionally stopped before creating a competing or unsafe repository change.',
+ icon: Ban,
+ tone: 'warning',
+ },
+ summary: [
+ {
+ label: 'Block reason',
+ value: blockedReason || 'Safety gate stopped the attempt',
+ detail: 'Security Agent did not continue past the blocker',
+ icon: Ban,
+ tone: 'warning',
+ },
+ {
+ label: 'Repository outcome',
+ value: prUrl ? 'Related pull request recorded' : 'No remediation PR opened',
+ detail: 'Review attempt history for context',
+ icon: GitBranch,
+ tone: 'neutral',
+ },
+ {
+ label: 'What should I do?',
+ value: prUrl ? 'Review the existing PR' : 'Resolve the blocker',
+ detail: 'Retry only when current safety gates allow it',
+ icon: Search,
+ tone: 'neutral',
+ },
+ ],
+ context:
+ 'Blocked is distinct from failed. Security Agent intentionally stopped because proceeding could create unsafe or conflicting work.',
+ action: {
+ label: 'Next step',
+ title: prUrl ? 'Review the existing remediation' : 'Resolve the recorded blocker',
+ description:
+ blockedReason ||
+ 'Review the attempt evidence before deciding whether another remediation attempt is appropriate.',
+ },
+ disclosureTitle: 'Why remediation was blocked',
+ steps: [
+ {
+ title: 'Attempt admitted',
+ detail: 'Security Agent accepted remediation for this finding.',
+ state: 'done',
+ },
+ {
+ title: 'Checked safety gates',
+ detail: blockedReason || 'A blocking condition was detected.',
+ state: 'done',
+ },
+ {
+ title: 'Stopped remediation',
+ detail: 'Security Agent ended the attempt before unsafe or conflicting work continued.',
+ state: 'error',
+ },
+ ],
+ };
+ }
+
+ if (status === 'failed') {
+ return {
+ hero: {
+ title: 'Remediation did not complete',
+ description:
+ latestAttempt?.lastErrorRedacted ||
+ 'Cloud Agent could not complete the repository change or open a trustworthy pull request.',
+ icon: XCircle,
+ tone: 'destructive',
+ },
+ summary: [
+ {
+ label: 'What happened?',
+ value: failureCode?.replace(/_/g, ' ') || 'Attempt failed',
+ detail: latestAttempt?.lastErrorRedacted || 'Review attempt details before retrying',
+ icon: TriangleAlert,
+ tone: 'destructive',
+ },
+ {
+ label: 'Pull request',
+ value: 'Not opened',
+ detail: 'No trustworthy pull request outcome was recorded',
+ icon: GitBranch,
+ tone: 'neutral',
+ },
+ {
+ label: 'What should I do?',
+ value: 'Resolve the error and retry',
+ detail: 'A retry creates a new preserved attempt',
+ icon: RefreshCw,
+ tone: 'neutral',
+ },
+ ],
+ context:
+ 'Failure leaves the finding open. Retrying creates a new Security Remediation Attempt and preserves this attempt in history.',
+ action: {
+ label: 'Next step',
+ title: 'Retry after resolving the recorded error',
+ description:
+ latestAttempt?.lastErrorRedacted ||
+ 'Review repository access and attempt details before starting another attempt.',
+ },
+ disclosureTitle: 'What failed',
+ steps: [
+ {
+ title: 'Request accepted',
+ detail: `Security Agent created attempt #${latestAttempt?.attemptNumber ?? 1}.`,
+ state: 'done',
+ },
+ {
+ title: 'Cloud Agent could not complete remediation',
+ detail:
+ latestAttempt?.lastErrorRedacted ||
+ 'The attempt ended before a safe outcome was recorded.',
+ state: 'error',
+ },
+ {
+ title: 'No pull request opened',
+ detail: 'No verified code change or pull request exists.',
+ state: 'pending',
+ },
+ ],
+ };
+ }
+
+ if (status === 'no_changes_needed') {
+ return {
+ hero: {
+ title: 'Cloud Agent found no safe change to make',
+ description:
+ 'The attempt ended without a repository change. Security Agent correctly did not open a no-change pull request.',
+ icon: CheckCircle2,
+ tone: 'neutral',
+ },
+ summary: [
+ {
+ label: 'Outcome',
+ value: 'No repository changes',
+ detail: 'A no-change pull request was not opened',
+ icon: Check,
+ tone: 'neutral',
+ },
+ {
+ label: 'Finding state',
+ value: 'Still open',
+ detail: 'Source synchronization controls closure',
+ icon: ShieldAlert,
+ tone: 'warning',
+ },
+ {
+ label: 'What should I do?',
+ value: 'Review source state',
+ detail: 'Retry only after new evidence or re-analysis',
+ icon: Info,
+ tone: 'neutral',
+ },
+ ],
+ context:
+ 'No changes needed is not the same as fixed. Security Agent does not close the finding or invent a pull request.',
+ action: {
+ label: 'Next step',
+ title: 'No immediate remediation action',
+ description:
+ 'Review the finding. If source data or repository evidence changes, run fresh analysis before retrying.',
+ },
+ disclosureTitle: 'Why no changes were made',
+ steps: [
+ {
+ title: 'Inspected repository evidence',
+ detail: 'Cloud Agent reviewed the current dependency and source state.',
+ state: 'done',
+ },
+ {
+ title: 'Found no safe repository change',
+ detail: 'The attempt determined that no change should be published.',
+ state: 'done',
+ },
+ {
+ title: 'Skipped a no-change pull request',
+ detail: 'Cloud Agent correctly ended without creating repository noise.',
+ state: 'done',
+ },
+ ],
+ };
+ }
+
+ if (status === 'cancelled') {
+ return {
+ hero: {
+ title: 'Remediation was cancelled',
+ description:
+ 'Cloud Agent confirmed interruption before opening a pull request. Any partial session work was not presented as a repository outcome.',
+ icon: XCircle,
+ tone: 'neutral',
+ },
+ summary: [
+ {
+ label: 'Outcome',
+ value: 'Cancelled',
+ detail: 'Cloud Agent confirmed interruption',
+ icon: XCircle,
+ tone: 'neutral',
+ },
+ {
+ label: 'Attempt started by',
+ value: requester,
+ detail: formatRemediationOrigin(latestAttempt?.origin ?? 'manual'),
+ icon: UserRound,
+ tone: 'neutral',
+ },
+ {
+ label: 'Pull request',
+ value: 'Not opened',
+ detail: 'No repository outcome was published',
+ icon: GitPullRequest,
+ tone: 'neutral',
+ },
+ ],
+ context:
+ 'Cancelled is a terminal attempt outcome, not a finding status. The Security Finding remains open.',
+ action: {
+ label: 'Next step',
+ title: 'Start a new attempt when ready',
+ description:
+ 'Retry remains available only when current analysis and safety gates provide a concrete fix path.',
+ },
+ disclosureTitle: 'How cancellation completed',
+ steps: [
+ {
+ title: 'Remediation started',
+ detail: 'Cloud Agent began repository work.',
+ state: 'done',
+ },
+ {
+ title: 'Cancellation requested',
+ detail: 'Security Agent asked Cloud Agent to stop the attempt.',
+ state: 'done',
+ },
+ {
+ title: 'Interruption confirmed',
+ detail: 'Cloud Agent stopped before opening a pull request.',
+ state: 'done',
+ },
+ ],
+ };
+ }
+
+ return {
+ hero: {
+ title: 'Review remediation status',
+ description: `Security Agent recorded this remediation as ${formatRemediationStatus(status)}.`,
+ icon: GitPullRequest,
+ tone: 'neutral',
+ },
+ summary: [
+ {
+ label: 'Current state',
+ value: formatRemediationStatus(status),
+ detail: 'Review attempt history for more context',
+ icon: GitPullRequest,
+ tone: 'neutral',
+ },
+ {
+ label: 'Finding state',
+ value: finding.status,
+ detail: 'Remediation does not directly close findings',
+ icon: ShieldAlert,
+ tone: 'warning',
+ },
+ {
+ label: 'Next step',
+ value: 'Review recorded evidence',
+ detail: 'Use the latest source and attempt state',
+ icon: Search,
+ tone: 'neutral',
+ },
+ ],
+ context:
+ 'Security Finding status and Security Remediation status are separate recorded outcomes.',
+ action: {
+ label: 'Current status',
+ title: 'Review remediation history',
+ description: 'Use the recorded attempts to understand the current state.',
+ },
+ disclosureTitle: 'Remediation progress',
+ steps: [
+ {
+ title: 'Remediation status recorded',
+ detail: formatRemediationStatus(status),
+ state: 'done',
+ },
+ ],
+ };
}
type FindingRemediationProps = {
+ finding: SecurityFinding;
status: string | null;
- prDraft: boolean | null;
+ summary: RemediationPresentationSummary;
outcomeSummary: string | null;
- blockedReason: string | null;
- updatedAt: string | null;
- failureCopy: string | null;
+ unavailableReason: string | null | undefined;
unavailableCopy: string | null;
attempts: RemediationAttempt[];
- action: React.ReactNode;
+ canStart: boolean;
+ isAwaitingStart: boolean;
+ actionStatusMessage?: string;
+ action: ReactNode;
};
function FindingRemediation({
+ finding,
status,
- prDraft,
+ summary,
outcomeSummary,
- blockedReason,
- updatedAt,
- failureCopy,
+ unavailableReason,
unavailableCopy,
attempts,
+ canStart,
+ isAwaitingStart,
+ actionStatusMessage,
action,
}: FindingRemediationProps) {
- const isActive = isActiveRemediationStatus(status);
+ const latestAttempt = attempts[0] ?? null;
+ const presentation = getRemediationPresentation({
+ status,
+ finding,
+ latestAttempt,
+ summary,
+ canStart,
+ unavailableReason,
+ unavailableCopy,
+ isAwaitingStart,
+ });
return (
-
-
-
-
-
- {isActive ? (
-
- ) : (
-
- )}
-
{formatRemediationStatus(status)}
- {prDraft && (
-
- Draft
-
- )}
-
- {outcomeSummary &&
{outcomeSummary}
}
- {blockedReason &&
Blocked: {blockedReason}
}
- {failureCopy &&
{failureCopy}
}
- {updatedAt && (
-
- Updated {formatDistanceToNow(new Date(updatedAt), { addSuffix: true })}
-
- )}
+
+
+
+ {outcomeSummary && !isActiveRemediationStatus(status) && (
+
+
Recorded outcome
+
{outcomeSummary}
+ )}
+
+ {action}
+
+
+
{presentation.context}
+
+
+
+
+ {presentation.disclosureTitle}
+
+
+
+ {presentation.steps.map((step, index) => (
+
+ ))}
+
+
+
+
+ {attempts.length > 0 && (
+
+
+ Remediation attempt history ({attempts.length})
+
+
+
+ {attempts.map(attempt => (
+
+ ))}
+
+
+
+ )}
+
+
+
+ );
+}
+
+function getAttemptTone(attempt: RemediationAttempt): Tone {
+ if (attempt.status === 'pr_opened') return 'success';
+ if (attempt.status === 'failed') return 'destructive';
+ if (
+ attempt.status === 'queued' ||
+ attempt.status === 'launching' ||
+ attempt.status === 'running' ||
+ attempt.status === 'blocked'
+ )
+ return 'warning';
+ return 'neutral';
+}
+
+function AttemptRecord({ attempt }: { attempt: RemediationAttempt }) {
+ const tone = getAttemptTone(attempt);
+ const requestedBy = attempt.origin === 'manual' ? 'Kilo user' : 'Security Agent';
+ const outcome = attempt.blockedReason || attempt.lastErrorRedacted;
+ const validation = attempt.validationEvidence?.map(formatValidationEvidence) ?? [];
- {action &&
{action}
}
+ return (
+
+
+
+
+ #{attempt.attemptNumber}
+
+
+ {formatRemediationStatus(attempt.status, attempt.cancellationRequestedAt)}
+
+
+ {formatRemediationOrigin(attempt.origin)}
+
+
+ {formatUtcDate(attempt.updatedAt)}
+
+
- {unavailableCopy && (
-
- )}
+
+
+
+
+
- {attempts.length > 0 ? (
-
-
Attempts
-
- {attempts.map(attempt => (
-
-
-
- #{attempt.attemptNumber}
-
- {formatRemediationStatus(attempt.status)}
-
- {attempt.origin}
-
-
- {format(new Date(attempt.updatedAt), 'PPp')}
-
-
-
-
- Branch:{' '}
- {attempt.branchName}
-
-
- Model:{' '}
- {attempt.remediationModelSlug}
-
-
- {attempt.lastErrorRedacted && (
-
{attempt.lastErrorRedacted}
- )}
- {attempt.blockedReason && (
-
{attempt.blockedReason}
- )}
- {attempt.riskNotes && (
-
{attempt.riskNotes}
- )}
- {attempt.prUrl && (
-
- )}
-
- ))}
+ {outcome && (
+
+ )}
+
+ {validation.length > 0 && (
+
+
+
+
Validation:
+
+ {validation.map(item => (
+ - {item}
+ ))}
+
- ) : (
-
)}
-
- );
-}
-function FindingFooter({
- finding,
- canDismiss,
- onDismiss,
- onClose,
-}: {
- finding: SecurityFinding;
- canDismiss: boolean;
- onDismiss: () => void;
- onClose: () => void;
-}) {
- return (
-
+ )}
+
);
}
+type FindingDetailDialogProps = {
+ finding: SecurityFindingWithRemediation | null;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onDismiss: (analysis: FindingAnalysis) => void;
+ canDismiss: boolean;
+ onOpenFinding: (findingId: string) => void;
+ onStartAnalysis: StartFindingAnalysis;
+ analysisAtCapacity: boolean;
+ organizationId?: string;
+ showSla?: boolean;
+};
+
export function FindingDetailDialog({
- finding,
+ finding: initialFinding,
open,
onOpenChange,
onDismiss,
canDismiss,
+ onOpenFinding,
+ onStartAnalysis,
+ analysisAtCapacity,
organizationId,
showSla = true,
}: FindingDetailDialogProps) {
const trpc = useTRPC();
const isOrg = Boolean(organizationId);
const {
- handleStartAnalysis: triggerStartAnalysis,
+ handleStartAnalysis: startAnalysisCommand,
handleStartRemediation,
handleRetryRemediation,
handleCancelRemediation,
+ trackUiInteraction,
startingAnalysisIds,
startingRemediationIds,
cancellingRemediationAttemptIds,
} = useSecurityAgent();
- const isAwaitingAnalysisStart = finding ? startingAnalysisIds.has(finding.id) : false;
- const isAwaitingRemediationStart = finding ? startingRemediationIds.has(finding.id) : false;
+ const trackedOpenFindingIdRef = useRef
(null);
+ const settledAnalysisCompletionRef = useRef(null);
+ const openerRef = useRef(null);
+ const scrollContainerRef = useRef(null);
+ const [tabState, setTabState] = useState<{ findingId: string | null; tab: FindingTab }>({
+ findingId: null,
+ tab: 'details',
+ });
+ const findingId = initialFinding?.id;
+
+ useEffect(() => {
+ if (!open || !findingId) {
+ trackedOpenFindingIdRef.current = null;
+ return;
+ }
+ if (trackedOpenFindingIdRef.current === findingId) return;
+
+ trackedOpenFindingIdRef.current = findingId;
+ trackUiInteraction('finding_detail_opened');
+ }, [findingId, open, trackUiInteraction]);
+
+ const hasActiveAnalysisStartCommand = findingId ? startingAnalysisIds.has(findingId) : false;
+ const isAwaitingRemediationStart = findingId ? startingRemediationIds.has(findingId) : false;
const pollWhileActive = (query: {
state: {
@@ -790,7 +2955,7 @@ export function FindingDetailDialog({
isActiveRemediationStatus(attempt.status)
);
if (
- isAwaitingAnalysisStart ||
+ hasActiveAnalysisStartCommand ||
isAwaitingRemediationStart ||
status === 'pending' ||
status === 'running' ||
@@ -800,26 +2965,89 @@ export function FindingDetailDialog({
}
return false as const;
};
- const orgAnalysisQuery = useQuery({
+
+ const { data: orgAnalysisData, refetch: refetchOrgAnalysis } = useQuery({
...trpc.organizations.securityAgent.getAnalysis.queryOptions({
organizationId: organizationId ?? '',
- findingId: finding?.id ?? '',
+ findingId: findingId ?? '',
}),
- enabled: open && Boolean(finding) && isOrg,
+ enabled: open && Boolean(initialFinding) && isOrg,
refetchInterval: pollWhileActive,
});
- const personalAnalysisQuery = useQuery({
+ const { data: personalAnalysisData, refetch: refetchPersonalAnalysis } = useQuery({
...trpc.securityAgent.getAnalysis.queryOptions({
- findingId: finding?.id ?? '',
+ findingId: findingId ?? '',
}),
- enabled: open && Boolean(finding) && !isOrg,
+ enabled: open && Boolean(initialFinding) && !isOrg,
refetchInterval: pollWhileActive,
});
- const analysisData = isOrg ? orgAnalysisQuery.data : personalAnalysisQuery.data;
+ const analysisData = isOrg ? orgAnalysisData : personalAnalysisData;
+ const refetchAnalysis = isOrg ? refetchOrgAnalysis : refetchPersonalAnalysis;
+ const analysisSettlementKey =
+ findingId && analysisData?.completedAt ? `${findingId}:${analysisData.completedAt}` : null;
+
+ useEffect(() => {
+ if (
+ !open ||
+ !analysisSettlementKey ||
+ analysisData?.status !== 'completed' ||
+ analysisData.findingState.status !== 'open' ||
+ settledAnalysisCompletionRef.current === analysisSettlementKey
+ ) {
+ return;
+ }
+
+ const timeoutId = window.setTimeout(() => {
+ settledAnalysisCompletionRef.current = analysisSettlementKey;
+ void refetchAnalysis();
+ }, ANALYSIS_POLL_INTERVAL_MS);
+ return () => window.clearTimeout(timeoutId);
+ }, [
+ analysisData?.findingState.status,
+ analysisData?.status,
+ analysisSettlementKey,
+ open,
+ refetchAnalysis,
+ ]);
+
+ const finding =
+ initialFinding && analysisData
+ ? {
+ ...initialFinding,
+ status: analysisData.findingState.status,
+ ignored_reason: analysisData.findingState.ignoredReason,
+ ignored_by: analysisData.findingState.ignoredBy,
+ fixed_at: analysisData.findingState.fixedAt,
+ updated_at: analysisData.findingState.updatedAt,
+ analysis_status: analysisData.status,
+ analysis_started_at: analysisData.startedAt,
+ analysis_completed_at: analysisData.completedAt,
+ analysis_error: analysisData.error,
+ analysis: analysisData.analysis,
+ session_id: analysisData.sessionId,
+ cli_session_id: analysisData.cliSessionId,
+ remediationSummary: analysisData.remediationSummary,
+ remediationCapability: analysisData.remediationCapability,
+ }
+ : initialFinding;
if (!finding) return null;
+ const selectedTab = tabState.findingId === finding.id ? tabState.tab : 'details';
+ const handleTabChange = (value: string) => {
+ if (value !== 'details' && value !== 'analysis' && value !== 'remediation') return;
+ setTabState({ findingId: finding.id, tab: value });
+ scrollContainerRef.current?.scrollTo({ top: 0 });
+ if (value === 'analysis') trackUiInteraction('finding_analysis_viewed');
+ if (value === 'remediation') trackUiInteraction('finding_remediation_viewed');
+ };
+
const analysisStatus = analysisData?.status ?? finding.analysis_status;
+ const isAwaitingAnalysisAdmission = isAwaitingManualAnalysisAdmission(
+ hasActiveAnalysisStartCommand,
+ analysisStatus
+ );
+ const isRestartingAnalysis = hasActiveAnalysisStartCommand && analysisStatus === 'running';
const analysis = analysisData?.analysis ?? finding.analysis;
const analysisError = analysisData?.error ?? finding.analysis_error;
const cliSessionId = analysisData?.cliSessionId ?? finding.cli_session_id;
@@ -833,16 +3061,9 @@ export function FindingDetailDialog({
const isEffectiveRemediationActive = isActiveRemediationStatus(effectiveRemediationStatus);
const effectiveRemediationPrUrl =
remediationSummary?.prUrl ?? latestHistoryAttempt?.prUrl ?? null;
- const effectiveRemediationPrDraft =
- remediationSummary?.prDraft ?? latestHistoryAttempt?.prDraft ?? null;
const effectiveRemediationOutcomeSummary = isEffectiveRemediationActive
? null
: (remediationSummary?.outcomeSummary ?? null);
- const effectiveRemediationBlockedReason = isEffectiveRemediationActive
- ? null
- : (latestHistoryAttempt?.blockedReason ?? remediationSummary?.blockedReason ?? null);
- const effectiveRemediationUpdatedAt =
- latestHistoryAttempt?.updatedAt ?? remediationSummary?.updatedAt ?? null;
const hasRegisteredRemediationAttempt =
remediationAttempts.length > 0 ||
Boolean(remediationSummary?.latestAttemptId ?? remediationSummary?.latestAttempt?.id);
@@ -852,8 +3073,10 @@ export function FindingDetailDialog({
remediationSummary?.latestAttemptId ??
null)
: null;
+ const cancellationRequestedAt = latestHistoryAttempt?.cancellationRequestedAt ?? null;
const isCancellingRemediation =
- !!activeRemediationAttemptId && cancellingRemediationAttemptIds.has(activeRemediationAttemptId);
+ Boolean(activeRemediationAttemptId) &&
+ cancellingRemediationAttemptIds.has(activeRemediationAttemptId ?? '');
const remediationUnavailableCopy =
remediationCapability &&
!remediationCapability.canStart &&
@@ -878,33 +3101,41 @@ export function FindingDetailDialog({
Boolean(remediationCapability?.canStart) &&
!hasRegisteredRemediationAttempt &&
!isEffectiveRemediationActive;
- const canCancelRemediation = Boolean(activeRemediationAttemptId);
+ const canCancelRemediation = Boolean(activeRemediationAttemptId) && !cancellationRequestedAt;
const canRetryRemediation =
Boolean(remediationCapability?.canRetry) &&
!isEffectiveRemediationActive &&
effectiveRemediationStatus !== 'pr_opened';
- const remediationFailureCopy = isEffectiveRemediationActive
- ? null
- : getRemediationFailureCopy(
- latestHistoryAttempt?.failureCode ?? remediationSummary?.failureCode
- );
const isAnalyzing =
- isAwaitingAnalysisStart || analysisStatus === 'pending' || analysisStatus === 'running';
- const remediationAnalysisRefreshLabel =
- isAwaitingAnalysisStart || analysisStatus === 'pending'
- ? manualAnalysisAdmissionCopy.pendingLabel
+ isAwaitingAnalysisAdmission || analysisStatus === 'pending' || analysisStatus === 'running';
+ const analysisActionDisabled = isAnalyzing || analysisAtCapacity;
+ const canDismissFinding = canDismiss && finding.status === 'open';
+ const analysisActionTitle =
+ analysisAtCapacity && !isAnalyzing ? manualAnalysisCapacityFullCopy : undefined;
+ const remediationAnalysisRefreshLabel = isAwaitingAnalysisAdmission
+ ? manualAnalysisAdmissionCopy.pendingLabel
+ : analysisStatus === 'pending'
+ ? manualAnalysisAdmissionCopy.successTitle
: analysisStatus === 'running'
? 'Analysis running'
: 'Rerun analysis';
- const codebaseAnalysisActionLabel =
- isAwaitingAnalysisStart || analysisStatus === 'pending'
- ? manualAnalysisAdmissionCopy.pendingLabel
+ const codebaseAnalysisActionLabel = isAwaitingAnalysisAdmission
+ ? manualAnalysisAdmissionCopy.pendingLabel
+ : analysisStatus === 'pending'
+ ? manualAnalysisAdmissionCopy.successTitle
: analysisStatus === 'running'
? 'Analysis running'
- : 'Run codebase analysis';
+ : 'Analyze repository';
- const handleStartAnalysis: StartAnalysis = ({ forceSandbox, retrySandboxOnly } = {}) => {
- triggerStartAnalysis(finding.id, { forceSandbox, retrySandboxOnly });
+ const handleStartAnalysis: StartAnalysis = ({
+ forceSandbox,
+ retrySandboxOnly,
+ restartActive,
+ } = {}) => {
+ onStartAnalysis(finding.id, { forceSandbox, retrySandboxOnly, restartActive });
+ };
+ const handleRestartAnalysis = () => {
+ startAnalysisCommand(finding.id, { restartActive: true });
};
const handleStartCodebaseAnalysis = () => {
handleStartAnalysis({ forceSandbox: true });
@@ -912,154 +3143,235 @@ export function FindingDetailDialog({
const handleCancelRemediationClick = () => {
if (activeRemediationAttemptId) handleCancelRemediation(activeRemediationAttemptId, finding.id);
};
+ const handleDismiss = () => onDismiss(analysis);
+ const handleDialogOpenChange = (nextOpen: boolean) => {
+ if (!nextOpen) setTabState({ findingId: null, tab: 'details' });
+ onOpenChange(nextOpen);
+ };
+
const remediationAction = effectiveRemediationPrUrl ? (
-