patrol: show actionable state badges on collapsed Patrol finding rows

Patrol findings hide raw investigation-status and investigation-outcome
badges per the patrol-intelligence contract, but the contract explicitly
allows plain-language actionable state labels (approval required,
investigating, verifying fix, fix failed). This adds a getPatrolFinding-
ActionableState helper and renders the resulting badge on the collapsed
Patrol row between the severity badge and the title, so the operator
sees each finding's state without expanding. The badge is absent when
no actionable state applies, leaving the default new-finding row clean.
This commit is contained in:
rcourtman 2026-06-25 23:27:34 +01:00
parent 370017b48d
commit 7d0b481e22
5 changed files with 96 additions and 5 deletions

View file

@ -7336,11 +7336,11 @@
{
"id": "opencode-coverage-gap-protection-posture-attention-queue",
"agent_id": "opencode",
"summary": "Patrol workspace work-type grouping: classify findings by actionable type and surface composition in the workspace header",
"summary": "Patrol row-level actionable state labels: translate hidden investigation state into plain-language badges on collapsed Patrol finding rows",
"target_id": "v6-product-lane-expansion",
"claimed_at": "2026-06-25T22:11:19Z",
"heartbeat_at": "2026-06-25T22:11:19Z",
"expires_at": "2026-06-26T00:11:19Z",
"claimed_at": "2026-06-25T22:24:34Z",
"heartbeat_at": "2026-06-25T22:24:34Z",
"expires_at": "2026-06-26T00:24:34Z",
"work_item": {
"kind": "coverage-gap",
"id": "protection-posture-attention-queue"

View file

@ -80,6 +80,7 @@ import {
getInvestigationStatusLabel,
getInvestigationOutcomeSortOrder,
getInvestigationStatusBadgeTone,
getPatrolFindingActionableState,
} from '@/utils/aiFindingPresentation';
import { copyToClipboard } from '@/utils/clipboard';
@ -884,6 +885,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
const rowPrimaryAction = getFindingPrimaryActionPresentation(finding);
const patrolWorkflow = () =>
getFindingPatrolWorkflowPresentation(finding, aiIntelligenceStore.patrolPendingApprovals);
const patrolActionableState = () => getPatrolFindingActionableState(finding);
const collapsedApprovalAction = () => {
const workflow = patrolWorkflow();
return workflow?.stage === 'approval' ? workflow : undefined;
@ -976,6 +978,19 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
>
{severityPresentation.label}
</MetadataBadge>
{/* Patrol actionable state plain-language translation of
investigation state that the contract allows on the
collapsed Patrol row (approval required, investigating,
verifying fix, fix failed). Raw investigation-status and
investigation-outcome badges stay hidden for Patrol
findings; this badge surfaces the operator-facing meaning. */}
<Show when={isPatrolFindingsSource() && patrolActionableState()}>
{(state) => (
<MetadataBadge {...FINDING_ROW_BADGE_PROPS} tone={state().tone}>
{state().label}
</MetadataBadge>
)}
</Show>
{/* Alert-triggered badge */}
<Show when={hasTriggeringAlert(finding)}>
<MetadataBadge

View file

@ -960,6 +960,12 @@ describe('aiFindingPresentation', () => {
expect(patrolWorkspaceSource).toContain('workTypeComposition');
});
it('shows contract-sanctioned actionable state badges on collapsed Patrol rows', () => {
expect(findingsPanelSource).toContain('getPatrolFindingActionableState');
expect(findingsPanelSource).toContain('patrolActionableState');
expect(findingsPanelSource).toContain('isPatrolFindingsSource() && patrolActionableState()');
});
it('does not stack a default detected loop-state badge on active findings', () => {
expect(findingsPanelSource).toContain('const shouldShowLoopStateBadge = () =>');
expect(findingsPanelSource).toContain('!isPatrolFindingsSource()');

View file

@ -4,8 +4,8 @@ import {
classifyPatrolFindingWorkType,
getPatrolWorkTypeComposition,
getPatrolWorkTypeCompositionClause,
getPatrolFindingActionableState,
} from '@/utils/aiFindingPresentation';
import type { UnifiedFinding } from '@/stores/aiIntelligence';
type ClassifyInput = Parameters<typeof classifyPatrolFindingWorkType>[0];
@ -178,3 +178,44 @@ describe('getPatrolWorkTypeCompositionClause', () => {
).toBe(' — 1 needs approval, 1 failed fix, 2 recurring');
});
});
describe('getPatrolFindingActionableState', () => {
it('returns undefined for a plain new finding', () => {
expect(getPatrolFindingActionableState(makeFinding())).toBeUndefined();
});
it('returns approval required for fix_queued', () => {
expect(
getPatrolFindingActionableState(makeFinding({ investigationOutcome: 'fix_queued' })),
).toEqual({ label: 'Approval required', tone: 'warning' });
});
it.each(['fix_failed', 'fix_verification_failed', 'cannot_fix', 'timed_out'] as const)(
'returns fix failed for %s',
(outcome) => {
expect(getPatrolFindingActionableState(makeFinding({ investigationOutcome: outcome }))).toEqual(
{ label: 'Fix failed', tone: 'danger' },
);
},
);
it('returns investigating for a running investigation', () => {
expect(
getPatrolFindingActionableState(makeFinding({ investigationStatus: 'running' })),
).toEqual({ label: 'Investigating', tone: 'info' });
});
it('returns verifying fix for fix_executed', () => {
expect(
getPatrolFindingActionableState(makeFinding({ investigationOutcome: 'fix_executed' })),
).toEqual({ label: 'Verifying fix', tone: 'info' });
});
it('returns undefined for non-active findings', () => {
expect(
getPatrolFindingActionableState(
makeFinding({ status: 'resolved', investigationOutcome: 'fix_queued' }),
),
).toBeUndefined();
});
});

View file

@ -658,6 +658,35 @@ export function getPatrolWorkTypeCompositionClause(
return `${parts.join(', ')}`;
}
export interface PatrolActionableStatePresentation {
label: string;
tone: MetadataBadgeTone;
}
export function getPatrolFindingActionableState(
finding: Pick<UnifiedFinding, 'status' | 'investigationStatus' | 'investigationOutcome'>,
): PatrolActionableStatePresentation | undefined {
if (finding.status !== 'active') return undefined;
if (finding.investigationOutcome === 'fix_queued') {
return { label: 'Approval required', tone: 'warning' };
}
if (isFailedFixOutcome(finding.investigationOutcome)) {
return { label: 'Fix failed', tone: 'danger' };
}
if (finding.investigationStatus === 'running') {
return { label: 'Investigating', tone: 'info' };
}
if (finding.investigationOutcome === 'fix_executed') {
return { label: 'Verifying fix', tone: 'info' };
}
return undefined;
}
export const getPatrolFindingIssueCountLabel = (count: number): string => {
const normalized = Number.isFinite(count) ? Math.max(0, Math.trunc(count)) : 0;
if (normalized === 1) {