@skyhook-io/radar-app 1.14.2 → 1.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/api/client.ts +32 -0
- package/src/api/diagnose.ts +7 -1
- package/src/components/CloudConnectFlow.tsx +21 -7
- package/src/components/CloudFunnelButton.tsx +85 -8
- package/src/components/cloudConnectHandoff.test.ts +10 -1
- package/src/components/cloudConnectHandoff.ts +15 -1
- package/src/components/diagnose/AIMarkdown.tsx +87 -0
- package/src/components/diagnose/ActivityTurn.tsx +1244 -0
- package/src/components/diagnose/AgentControls.tsx +603 -0
- package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
- package/src/components/diagnose/AnalysisStory.tsx +13 -18
- package/src/components/diagnose/ApplyDialog.tsx +289 -0
- package/src/components/diagnose/AssessmentCard.tsx +1579 -0
- package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
- package/src/components/diagnose/EvidenceCard.tsx +611 -0
- package/src/components/diagnose/Home.test.tsx +1 -3
- package/src/components/diagnose/Home.tsx +2 -1
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
- package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
- package/src/components/diagnose/InvestigationView.tsx +39 -51
- package/src/components/diagnose/StoryExcerpt.tsx +118 -0
- package/src/components/diagnose/assessmentCopy.ts +169 -0
- package/src/components/diagnose/investigationCase.test.tsx +658 -2
- package/src/components/diagnose/investigationCase.ts +135 -26
- package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +682 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +8 -2
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +1 -1
- package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
- package/src/components/diagnose/investigationEvidence/adapters/listings.ts +136 -0
- package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
- package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
- package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
- package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
- package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
- package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
- package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
- package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
- package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
- package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
- package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
- package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
- package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
- package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
- package/src/components/diagnose/investigationEvidence/index.ts +1 -1
- package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +13 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
- package/src/components/diagnose/investigationEvidence/types.ts +97 -2
- package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
- package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
- package/src/components/diagnose/investigationState.test.ts +2 -2
- package/src/components/diagnose/investigationState.ts +2 -2
- package/src/components/diagnose/investigationStory.ts +1 -1
- package/src/components/diagnose/parts.test.tsx +15 -1
- package/src/components/diagnose/parts.tsx +26 -3904
- package/src/components/diagnose/target.ts +9 -3
- package/src/components/diagnose/toolCallLabel.test.ts +47 -1
- package/src/components/diagnose/toolCallLabel.ts +41 -2
- package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +27 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
- package/src/components/resources/PodFilePreview.tsx +1 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +20 -1
- package/src/components/ui/CommandPalette.tsx +12 -8
- package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
- package/src/components/ui/Omnibar.tsx +96 -60
- package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
- package/src/components/workload/WorkloadView.tsx +7 -1
- package/src/components/ui/Disclosure.tsx +0 -47
|
@@ -48,9 +48,9 @@ describe("canRerunInvestigation", () => {
|
|
|
48
48
|
|
|
49
49
|
it("stays hidden while a turn is in flight", () => {
|
|
50
50
|
// A start would be handed back the live run, so the button does nothing.
|
|
51
|
-
expect(
|
|
52
|
-
|
|
53
|
-
)
|
|
51
|
+
expect(canRerunInvestigation("investigation", run("running"), false)).toBe(
|
|
52
|
+
false,
|
|
53
|
+
);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it("stays hidden on a stale run", () => {
|
|
@@ -62,9 +62,9 @@ describe("canRerunInvestigation", () => {
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
it("blocks fresh starts while a human turn stops, but allows a separate human run from an automatic one", () => {
|
|
65
|
-
expect(
|
|
66
|
-
|
|
67
|
-
)
|
|
65
|
+
expect(canRerunInvestigation("investigation", run("stopping"), false)).toBe(
|
|
66
|
+
false,
|
|
67
|
+
);
|
|
68
68
|
expect(
|
|
69
69
|
canRerunInvestigation(
|
|
70
70
|
"investigation",
|
|
@@ -89,9 +89,9 @@ describe("canRerunInvestigation", () => {
|
|
|
89
89
|
expect(canRerunInvestigation("investigation", run("error"), false)).toBe(
|
|
90
90
|
true,
|
|
91
91
|
);
|
|
92
|
-
expect(
|
|
93
|
-
|
|
94
|
-
)
|
|
92
|
+
expect(canRerunInvestigation("investigation", run("stopped"), false)).toBe(
|
|
93
|
+
true,
|
|
94
|
+
);
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
|
|
@@ -25,7 +25,11 @@ import {
|
|
|
25
25
|
} from "lucide-react";
|
|
26
26
|
import { Tooltip } from "../ui/Tooltip";
|
|
27
27
|
import { useAnimatedUnmount } from "../../hooks/useAnimatedUnmount";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
TRANSITION_BACKDROP,
|
|
30
|
+
TRANSITION_DRAWER,
|
|
31
|
+
overlayTransitionStyle,
|
|
32
|
+
} from "../../utils/animation";
|
|
29
33
|
import {
|
|
30
34
|
useDiagnose,
|
|
31
35
|
useDiagnoseLayout,
|
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import { StoryExcerpt } from "./StoryExcerpt";
|
|
2
|
+
import { storyExcerpt } from "./investigationEvidence/excerpt";
|
|
3
|
+
import {
|
|
4
|
+
type InvestigationCaseItem,
|
|
5
|
+
investigationCaseObservationKey,
|
|
6
|
+
} from "./investigationCase";
|
|
7
|
+
import {
|
|
8
|
+
useCallback,
|
|
9
|
+
useContext,
|
|
10
|
+
useLayoutEffect,
|
|
11
|
+
useState,
|
|
12
|
+
useId,
|
|
13
|
+
} from "react";
|
|
14
|
+
import { clsx } from "clsx";
|
|
15
|
+
import { Collapse, CollapseChevron } from "@skyhook-io/k8s-ui";
|
|
16
|
+
import { parseLogLine } from "../../utils/log-format";
|
|
17
|
+
import { evidenceDisplaySnapshot } from "./investigationEvidencePresentation";
|
|
18
|
+
import {
|
|
19
|
+
investigationEvidenceSubjectRef,
|
|
20
|
+
investigationEvidenceSourceDomId,
|
|
21
|
+
type InvestigationEvidenceGroup,
|
|
22
|
+
type InvestigationEvidenceObservation,
|
|
23
|
+
} from "./investigationEvidence";
|
|
24
|
+
import type { InvestigationSourceExcerpt } from "./investigationSourceFocus";
|
|
25
|
+
import { evidenceSourceExcerpt } from "./investigationSourceFocus";
|
|
26
|
+
import { Tooltip } from "../ui/Tooltip";
|
|
27
|
+
import { AgentClaimNote, AgentRoleChip } from "./AgentCase";
|
|
28
|
+
import { useDisclosureReveal } from "./useDisclosureReveal";
|
|
29
|
+
import { EvidenceNavigationContext } from "./investigationEvidence/navigation";
|
|
30
|
+
import {
|
|
31
|
+
EvidenceCaveat,
|
|
32
|
+
EvidenceIcon,
|
|
33
|
+
OpenResourceButton,
|
|
34
|
+
OpenTimelineButton,
|
|
35
|
+
SourceButton,
|
|
36
|
+
toneBorder,
|
|
37
|
+
uniquePrimarySources,
|
|
38
|
+
phaseLabel,
|
|
39
|
+
} from "./investigationEvidence/cardParts";
|
|
40
|
+
import {
|
|
41
|
+
listingScopeNamespace,
|
|
42
|
+
namedInventoryRows,
|
|
43
|
+
} from "./investigationEvidence/bodies/inventory";
|
|
44
|
+
import {
|
|
45
|
+
EvidenceBody,
|
|
46
|
+
evidenceHasDetails,
|
|
47
|
+
} from "./investigationEvidence/bodies/index";
|
|
48
|
+
import {
|
|
49
|
+
evidenceTypePrefersFullRow,
|
|
50
|
+
investigationCaseItemKey,
|
|
51
|
+
investigationEvidenceShouldRevealHistory,
|
|
52
|
+
previousDifferentObservations,
|
|
53
|
+
} from "./investigationEvidencePartition";
|
|
54
|
+
|
|
55
|
+
export function EvidenceCard({
|
|
56
|
+
group,
|
|
57
|
+
observation: observationOverride,
|
|
58
|
+
domId = group.id,
|
|
59
|
+
animateArrival,
|
|
60
|
+
onViewSource,
|
|
61
|
+
spanFullRow = false,
|
|
62
|
+
prominence = "primary",
|
|
63
|
+
compact = false,
|
|
64
|
+
placedInStory = false,
|
|
65
|
+
gapRoles,
|
|
66
|
+
noteMode = "full",
|
|
67
|
+
}: {
|
|
68
|
+
group: InvestigationEvidenceGroup;
|
|
69
|
+
/**
|
|
70
|
+
* Render this exact observation instead of the group's current one — a
|
|
71
|
+
* story places the read the agent cited, which may since have been
|
|
72
|
+
* superseded, and says so rather than swapping in today's.
|
|
73
|
+
*/
|
|
74
|
+
observation?: InvestigationEvidenceObservation;
|
|
75
|
+
/** Stable layout/scroll identity when an existing card changes section. */
|
|
76
|
+
domId?: string;
|
|
77
|
+
animateArrival: boolean;
|
|
78
|
+
onViewSource: (
|
|
79
|
+
sourceId: string,
|
|
80
|
+
excerpt?: InvestigationSourceExcerpt,
|
|
81
|
+
) => void;
|
|
82
|
+
/** Fill both columns when this card has no compact row partner. */
|
|
83
|
+
spanFullRow?: boolean;
|
|
84
|
+
prominence?: "primary" | "supporting" | "secondary";
|
|
85
|
+
/**
|
|
86
|
+
* Header and summary only, so the collapsed story preview never slices a
|
|
87
|
+
* card. A story log card keeps its cited lines: they are the card.
|
|
88
|
+
*/
|
|
89
|
+
compact?: boolean;
|
|
90
|
+
/** This card also appears inside the story above. */
|
|
91
|
+
placedInStory?: boolean;
|
|
92
|
+
/** Roles whose "does not cover" line is shown; undefined shows every gap. */
|
|
93
|
+
gapRoles?: ReadonlySet<string>;
|
|
94
|
+
/**
|
|
95
|
+
* Inside the story the prose above the card is the agent's reading of it,
|
|
96
|
+
* so the note row keeps only the role chip (and an excluded hypothesis);
|
|
97
|
+
* repeating the sentence under the card doubled the text for nothing.
|
|
98
|
+
*/
|
|
99
|
+
noteMode?: "full" | "chip";
|
|
100
|
+
}) {
|
|
101
|
+
const {
|
|
102
|
+
onOpenResource,
|
|
103
|
+
onOpenTimeline,
|
|
104
|
+
revealSourceId,
|
|
105
|
+
revealRequestId,
|
|
106
|
+
citedOrderByGroup,
|
|
107
|
+
expandedGroupIds,
|
|
108
|
+
onGroupOpenChange,
|
|
109
|
+
metricsMarkersBySource,
|
|
110
|
+
caseByGroup,
|
|
111
|
+
excludedByItem,
|
|
112
|
+
} = useContext(EvidenceNavigationContext);
|
|
113
|
+
const open = expandedGroupIds?.has(group.id) ?? false;
|
|
114
|
+
const setOpen = useCallback(
|
|
115
|
+
(value: boolean) => onGroupOpenChange?.(group.id, value),
|
|
116
|
+
[onGroupOpenChange, group.id],
|
|
117
|
+
);
|
|
118
|
+
const { elementRef, revealAfterToggle } = useDisclosureReveal<HTMLElement>();
|
|
119
|
+
const observation = observationOverride ?? group.latest;
|
|
120
|
+
const supersededRead =
|
|
121
|
+
observationOverride !== undefined && observationOverride !== group.latest;
|
|
122
|
+
const displaySummary =
|
|
123
|
+
observation.data.type === "crash" && observation.summary
|
|
124
|
+
? parseLogLine(observation.summary).content
|
|
125
|
+
: observation.summary;
|
|
126
|
+
const citedOrder = citedOrderByGroup?.get(group.id);
|
|
127
|
+
const caseItems = caseByGroup?.get(group.id) ?? [];
|
|
128
|
+
// Annotations belong to the observation on screen: a card rendering an
|
|
129
|
+
// earlier read shows the note the agent attached to that read, not the
|
|
130
|
+
// latest one's.
|
|
131
|
+
const cardItems = caseItems.filter((item) =>
|
|
132
|
+
observationOverride
|
|
133
|
+
? item.observation !== undefined &&
|
|
134
|
+
item.observation.source.id === observation.source.id &&
|
|
135
|
+
item.observation.revision === observation.revision
|
|
136
|
+
: item.placement === "card",
|
|
137
|
+
);
|
|
138
|
+
const revisionItems = caseItems.filter(
|
|
139
|
+
(item) => item.placement === "revision",
|
|
140
|
+
);
|
|
141
|
+
const agentCause = cardItems.some((item) => item.role === "cause");
|
|
142
|
+
// A card placed in the story: the prose carries the claim, so the card
|
|
143
|
+
// shows its role beside the title, its scope line if any, and for a log
|
|
144
|
+
// stream the decisive lines themselves; the rest waits behind the chevron.
|
|
145
|
+
const storyCard = noteMode === "chip";
|
|
146
|
+
const storyGaps = storyCard
|
|
147
|
+
? cardItems
|
|
148
|
+
.map((item) =>
|
|
149
|
+
item.gap && (!gapRoles || gapRoles.has(item.role)) ? item.gap : "",
|
|
150
|
+
)
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
: [];
|
|
153
|
+
// A compact card keeps its single row; the agent's clause on what the
|
|
154
|
+
// result does not cover rides on it after the status, cut to the row, whole
|
|
155
|
+
// on hover. The clause is written to stand alone, so it carries no label.
|
|
156
|
+
const gapOnRow = compact && storyGaps.length > 0;
|
|
157
|
+
// What a rules_out card excludes is the point of placing it; it stays on
|
|
158
|
+
// the story card as its one line.
|
|
159
|
+
const storyExcludes = storyCard
|
|
160
|
+
? cardItems
|
|
161
|
+
.filter((item) => item.role === "rules_out")
|
|
162
|
+
.map((item) => excludedByItem?.get(investigationCaseItemKey(item)))
|
|
163
|
+
.filter((entry): entry is string => Boolean(entry))
|
|
164
|
+
: [];
|
|
165
|
+
const previousObservations = previousDifferentObservations(
|
|
166
|
+
group,
|
|
167
|
+
citedOrder,
|
|
168
|
+
new Set(revisionItems.map((item) => item.observation!.source.order)),
|
|
169
|
+
);
|
|
170
|
+
const meaningfulHistory = previousObservations.length > 0;
|
|
171
|
+
const citedObservation = group.observations.find(
|
|
172
|
+
(item) => item.source.order === citedOrder,
|
|
173
|
+
);
|
|
174
|
+
const differsFromAssessment =
|
|
175
|
+
citedObservation &&
|
|
176
|
+
evidenceDisplaySnapshot(citedObservation) !==
|
|
177
|
+
evidenceDisplaySnapshot(observation);
|
|
178
|
+
const bodyId = `${domId}-body`;
|
|
179
|
+
const hasEvidenceDetails = evidenceHasDetails(
|
|
180
|
+
observation.data,
|
|
181
|
+
observation.summary,
|
|
182
|
+
);
|
|
183
|
+
const excerpt = storyCard
|
|
184
|
+
? storyExcerpt(observation, cardItems, {
|
|
185
|
+
compact,
|
|
186
|
+
scopeNamespace: listingScopeNamespace(observation.source),
|
|
187
|
+
})
|
|
188
|
+
: undefined;
|
|
189
|
+
const citedRows =
|
|
190
|
+
observation.data.type === "inventory"
|
|
191
|
+
? namedInventoryRows(
|
|
192
|
+
observation.data.resources,
|
|
193
|
+
cardItems,
|
|
194
|
+
listingScopeNamespace(observation.source),
|
|
195
|
+
).flatMap((named) => (named.row ? [named.row] : []))
|
|
196
|
+
: undefined;
|
|
197
|
+
// A card whose content shows inline has nothing left behind the fold but
|
|
198
|
+
// the earlier lines or the history.
|
|
199
|
+
const inlineLogs = excerpt?.kind === "lines";
|
|
200
|
+
const inlineMetrics = excerpt?.kind === "chart";
|
|
201
|
+
const storyLogRest = excerpt?.kind === "lines" ? excerpt.rest : [];
|
|
202
|
+
const hasExpandableBody =
|
|
203
|
+
(hasEvidenceDetails && !inlineLogs && !inlineMetrics) || meaningfulHistory;
|
|
204
|
+
const canExpand = !compact && (hasExpandableBody || storyLogRest.length > 0);
|
|
205
|
+
const revealHistory = investigationEvidenceShouldRevealHistory(
|
|
206
|
+
group,
|
|
207
|
+
revealSourceId,
|
|
208
|
+
new Set(revisionItems.map((item) => item.observation!.source.id)),
|
|
209
|
+
);
|
|
210
|
+
useLayoutEffect(() => {
|
|
211
|
+
const destination = revealSourceId
|
|
212
|
+
? document.getElementById(
|
|
213
|
+
investigationEvidenceSourceDomId(revealSourceId),
|
|
214
|
+
)
|
|
215
|
+
: null;
|
|
216
|
+
if (
|
|
217
|
+
revealHistory ||
|
|
218
|
+
(destination && elementRef.current?.contains(destination))
|
|
219
|
+
)
|
|
220
|
+
setOpen(true);
|
|
221
|
+
}, [revealHistory, revealSourceId, revealRequestId, elementRef, setOpen]);
|
|
222
|
+
const wide = spanFullRow || evidenceTypePrefersFullRow(observation.data.type);
|
|
223
|
+
const resourceRef = investigationEvidenceSubjectRef(observation.data);
|
|
224
|
+
const resourceIdentity = resourceRef
|
|
225
|
+
? `${resourceRef.namespace ? `${resourceRef.namespace}/` : ""}${resourceRef.name}`
|
|
226
|
+
: undefined;
|
|
227
|
+
const primarySources = uniquePrimarySources(group);
|
|
228
|
+
const inlineSecretKeys =
|
|
229
|
+
observation.data.type === "resource" &&
|
|
230
|
+
observation.data.resource.kind.toLowerCase() === "secret" &&
|
|
231
|
+
!canExpand;
|
|
232
|
+
const headerContent = (
|
|
233
|
+
<>
|
|
234
|
+
<EvidenceIcon observation={observation} prominence={prominence} />
|
|
235
|
+
<span className="min-w-0 flex-1">
|
|
236
|
+
<span className="flex flex-wrap items-center gap-1.5">
|
|
237
|
+
<span className="text-sm font-semibold leading-snug text-theme-text-primary">
|
|
238
|
+
{observation.title}
|
|
239
|
+
</span>
|
|
240
|
+
{storyCard
|
|
241
|
+
? [...new Set(cardItems.map((item) => item.role))].map((role) =>
|
|
242
|
+
role ? <AgentRoleChip key={role} role={role} /> : null,
|
|
243
|
+
)
|
|
244
|
+
: null}
|
|
245
|
+
</span>
|
|
246
|
+
{observation.relevance === "broader" &&
|
|
247
|
+
resourceRef &&
|
|
248
|
+
resourceIdentity &&
|
|
249
|
+
!observation.title.includes(resourceIdentity) ? (
|
|
250
|
+
<span className="mt-0.5 block text-xs text-theme-text-secondary">
|
|
251
|
+
{resourceIdentity} · {resourceRef.kind}
|
|
252
|
+
</span>
|
|
253
|
+
) : null}
|
|
254
|
+
{gapOnRow ? (
|
|
255
|
+
<Tooltip
|
|
256
|
+
content={storyGaps.join(" · ")}
|
|
257
|
+
wrapperClassName="mt-0.5 w-full min-w-0"
|
|
258
|
+
>
|
|
259
|
+
<span className="min-w-0 flex-1 truncate text-xs leading-relaxed text-theme-text-secondary">
|
|
260
|
+
{displaySummary}
|
|
261
|
+
{displaySummary ? " · " : null}
|
|
262
|
+
<span data-agent-gap-hint className="text-theme-text-tertiary">
|
|
263
|
+
{storyGaps.join(" · ")}
|
|
264
|
+
</span>
|
|
265
|
+
</span>
|
|
266
|
+
</Tooltip>
|
|
267
|
+
) : displaySummary ? (
|
|
268
|
+
<span
|
|
269
|
+
className={clsx(
|
|
270
|
+
"mt-0.5 block text-xs leading-relaxed text-theme-text-secondary",
|
|
271
|
+
!open && !inlineSecretKeys && "line-clamp-2",
|
|
272
|
+
"[overflow-wrap:anywhere]",
|
|
273
|
+
)}
|
|
274
|
+
>
|
|
275
|
+
{displaySummary}
|
|
276
|
+
</span>
|
|
277
|
+
) : null}
|
|
278
|
+
{supersededRead ? (
|
|
279
|
+
<span className="mt-0.5 block text-xs text-theme-text-tertiary">
|
|
280
|
+
Captured in turn {observation.source.turnIndex + 1} · a newer read
|
|
281
|
+
of this exists in Captured results
|
|
282
|
+
</span>
|
|
283
|
+
) : placedInStory ? (
|
|
284
|
+
<span className="mt-0.5 block text-xs text-theme-text-tertiary">
|
|
285
|
+
In the analysis
|
|
286
|
+
</span>
|
|
287
|
+
) : group.historical ? (
|
|
288
|
+
<span className="mt-0.5 block text-xs text-theme-text-tertiary">
|
|
289
|
+
Previous observation · not confirmed by the latest check
|
|
290
|
+
</span>
|
|
291
|
+
) : differsFromAssessment &&
|
|
292
|
+
citedOrder != null &&
|
|
293
|
+
observation.source.order !== citedOrder ? (
|
|
294
|
+
<span className="mt-0.5 block text-xs text-theme-text-tertiary">
|
|
295
|
+
{observation.source.order > citedOrder
|
|
296
|
+
? "Observed after the assessment’s source"
|
|
297
|
+
: "Earlier observation retained from a more direct source"}
|
|
298
|
+
</span>
|
|
299
|
+
) : null}
|
|
300
|
+
</span>
|
|
301
|
+
{canExpand ? (
|
|
302
|
+
<CollapseChevron open={open} className="h-4 w-4 self-center" />
|
|
303
|
+
) : null}
|
|
304
|
+
</>
|
|
305
|
+
);
|
|
306
|
+
return (
|
|
307
|
+
<article
|
|
308
|
+
data-evidence-source={observation.source.id}
|
|
309
|
+
ref={elementRef}
|
|
310
|
+
id={domId}
|
|
311
|
+
data-evidence-card
|
|
312
|
+
tabIndex={-1}
|
|
313
|
+
aria-label={`${observation.title} evidence`}
|
|
314
|
+
className={clsx(
|
|
315
|
+
"@container/card scroll-mt-14 overflow-hidden outline-none focus:ring-2 focus:ring-accent/50 data-[source-related]:ring-2 data-[source-related]:ring-accent/35",
|
|
316
|
+
"rounded-lg border",
|
|
317
|
+
// The card the agent calls the cause sorts first and is often the
|
|
318
|
+
// calmest thing on screen: a cause is frequently a Secret or a
|
|
319
|
+
// ConfigMap that Radar has no reason to colour, while every card
|
|
320
|
+
// echoing the failure below it carries red. Lifting the surface marks
|
|
321
|
+
// it without borrowing a severity hue, and without touching the left
|
|
322
|
+
// rule (Radar's severity) or the ring (the source you are viewing).
|
|
323
|
+
agentCause ? "bg-theme-elevated shadow-theme-sm" : "bg-theme-surface",
|
|
324
|
+
toneBorder(observation.tone, observation.tier, prominence),
|
|
325
|
+
wide && "@min-[760px]/evidence:col-span-2",
|
|
326
|
+
animateArrival && "animate-transcript-enter",
|
|
327
|
+
)}
|
|
328
|
+
>
|
|
329
|
+
{primarySources.map((source) => (
|
|
330
|
+
<span
|
|
331
|
+
key={source.id}
|
|
332
|
+
id={investigationEvidenceSourceDomId(source.id)}
|
|
333
|
+
className="block scroll-mt-14"
|
|
334
|
+
aria-hidden
|
|
335
|
+
/>
|
|
336
|
+
))}
|
|
337
|
+
<div className="flex min-w-0 items-stretch">
|
|
338
|
+
{canExpand ? (
|
|
339
|
+
<button
|
|
340
|
+
type="button"
|
|
341
|
+
aria-expanded={open}
|
|
342
|
+
aria-controls={bodyId}
|
|
343
|
+
onClick={() => {
|
|
344
|
+
const opening = !open;
|
|
345
|
+
setOpen(opening);
|
|
346
|
+
revealAfterToggle(opening);
|
|
347
|
+
}}
|
|
348
|
+
className={clsx(
|
|
349
|
+
"flex min-w-0 flex-1 items-start text-left hover:bg-theme-hover/60",
|
|
350
|
+
prominence === "primary"
|
|
351
|
+
? "gap-2.5 px-3 py-2.5"
|
|
352
|
+
: "gap-2 px-2.5 py-2",
|
|
353
|
+
)}
|
|
354
|
+
>
|
|
355
|
+
{headerContent}
|
|
356
|
+
</button>
|
|
357
|
+
) : (
|
|
358
|
+
<div
|
|
359
|
+
className={clsx(
|
|
360
|
+
"flex min-w-0 flex-1 items-start text-left",
|
|
361
|
+
prominence === "primary"
|
|
362
|
+
? "gap-2.5 px-3 py-2.5"
|
|
363
|
+
: "gap-2 px-2.5 py-2",
|
|
364
|
+
)}
|
|
365
|
+
>
|
|
366
|
+
{headerContent}
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
<div className="flex shrink-0 items-center gap-0.5 px-2">
|
|
370
|
+
{observation.data.type === "changes" ? (
|
|
371
|
+
<EvidenceCaveat data={observation.data} />
|
|
372
|
+
) : null}
|
|
373
|
+
<SourceButton
|
|
374
|
+
ariaLabel={`View result for ${observation.title}`}
|
|
375
|
+
compact
|
|
376
|
+
onClick={() =>
|
|
377
|
+
onViewSource(
|
|
378
|
+
observation.source.id,
|
|
379
|
+
evidenceSourceExcerpt(observation.data),
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
/>
|
|
383
|
+
<OpenResourceButton
|
|
384
|
+
resourceRef={resourceRef}
|
|
385
|
+
onOpenResource={onOpenResource}
|
|
386
|
+
compact
|
|
387
|
+
/>
|
|
388
|
+
{observation.data.type === "changes" && observation.data.subject ? (
|
|
389
|
+
<OpenTimelineButton
|
|
390
|
+
scope={observation.data.subject}
|
|
391
|
+
onOpenTimeline={onOpenTimeline}
|
|
392
|
+
/>
|
|
393
|
+
) : null}
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
{storyCard ? (
|
|
397
|
+
<>
|
|
398
|
+
{(storyGaps.length > 0 || storyExcludes.length > 0) && !compact ? (
|
|
399
|
+
<div
|
|
400
|
+
className={clsx(
|
|
401
|
+
"space-y-0.5 text-[11px] text-theme-text-tertiary",
|
|
402
|
+
prominence === "primary" ? "px-3 pb-2.5" : "px-2.5 pb-2",
|
|
403
|
+
)}
|
|
404
|
+
>
|
|
405
|
+
{storyExcludes.map((hypothesis) => (
|
|
406
|
+
<p key={hypothesis} data-agent-excludes>
|
|
407
|
+
Rules out: {hypothesis}
|
|
408
|
+
</p>
|
|
409
|
+
))}
|
|
410
|
+
{storyGaps.map((gap) => (
|
|
411
|
+
<p key={gap} data-agent-gap>
|
|
412
|
+
{gap}
|
|
413
|
+
</p>
|
|
414
|
+
))}
|
|
415
|
+
</div>
|
|
416
|
+
) : null}
|
|
417
|
+
{excerpt ? (
|
|
418
|
+
<StoryExcerpt
|
|
419
|
+
excerpt={excerpt}
|
|
420
|
+
data={observation.data}
|
|
421
|
+
prominence={prominence}
|
|
422
|
+
open={open}
|
|
423
|
+
canExpand={canExpand}
|
|
424
|
+
changeCoverage={metricsMarkersBySource?.get(
|
|
425
|
+
observation.source.id,
|
|
426
|
+
)}
|
|
427
|
+
/>
|
|
428
|
+
) : null}
|
|
429
|
+
</>
|
|
430
|
+
) : cardItems.some((item) => item.claim || item.role) ? (
|
|
431
|
+
<div
|
|
432
|
+
className={clsx(
|
|
433
|
+
"space-y-1.5",
|
|
434
|
+
prominence === "primary" ? "px-3 pb-2.5" : "px-2.5 pb-2",
|
|
435
|
+
)}
|
|
436
|
+
>
|
|
437
|
+
{cardItems.map((item) => (
|
|
438
|
+
<AgentClaimNote
|
|
439
|
+
key={item.index}
|
|
440
|
+
claim={compact ? "" : item.claim}
|
|
441
|
+
role={item.role}
|
|
442
|
+
gap={
|
|
443
|
+
compact || (gapRoles && !gapRoles.has(item.role))
|
|
444
|
+
? undefined
|
|
445
|
+
: item.gap
|
|
446
|
+
}
|
|
447
|
+
excludes={excludedByItem?.get(investigationCaseItemKey(item))}
|
|
448
|
+
className="pt-1.5"
|
|
449
|
+
/>
|
|
450
|
+
))}
|
|
451
|
+
</div>
|
|
452
|
+
) : null}
|
|
453
|
+
{canExpand && hasExpandableBody ? (
|
|
454
|
+
<div id={bodyId}>
|
|
455
|
+
<Collapse open={open}>
|
|
456
|
+
<div
|
|
457
|
+
className={clsx(
|
|
458
|
+
"space-y-3 border-t border-theme-border/60",
|
|
459
|
+
prominence === "primary" ? "px-3 py-3" : "px-2.5 py-2.5",
|
|
460
|
+
)}
|
|
461
|
+
>
|
|
462
|
+
{hasEvidenceDetails && !inlineLogs && !inlineMetrics ? (
|
|
463
|
+
<EvidenceBody
|
|
464
|
+
data={observation.data}
|
|
465
|
+
condensed={storyCard}
|
|
466
|
+
cardSummary={observation.summary}
|
|
467
|
+
changeCoverage={metricsMarkersBySource?.get(
|
|
468
|
+
observation.source.id,
|
|
469
|
+
)}
|
|
470
|
+
citedRows={citedRows}
|
|
471
|
+
/>
|
|
472
|
+
) : null}
|
|
473
|
+
{meaningfulHistory ? (
|
|
474
|
+
<RevisionHistory
|
|
475
|
+
observations={previousObservations}
|
|
476
|
+
citedOrder={citedOrder}
|
|
477
|
+
caseItems={revisionItems}
|
|
478
|
+
reveal={revealHistory}
|
|
479
|
+
revealRequestId={revealRequestId}
|
|
480
|
+
onViewSource={onViewSource}
|
|
481
|
+
/>
|
|
482
|
+
) : null}
|
|
483
|
+
{observation.data.type !== "changes" ? (
|
|
484
|
+
<EvidenceCaveat data={observation.data} />
|
|
485
|
+
) : null}
|
|
486
|
+
</div>
|
|
487
|
+
</Collapse>
|
|
488
|
+
</div>
|
|
489
|
+
) : null}
|
|
490
|
+
</article>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function RevisionHistory({
|
|
495
|
+
observations,
|
|
496
|
+
citedOrder,
|
|
497
|
+
caseItems = [],
|
|
498
|
+
reveal,
|
|
499
|
+
revealRequestId,
|
|
500
|
+
onViewSource,
|
|
501
|
+
}: {
|
|
502
|
+
observations: InvestigationEvidenceObservation[];
|
|
503
|
+
citedOrder?: number;
|
|
504
|
+
/** Agent items bound to a superseded observation of this group. */
|
|
505
|
+
caseItems?: InvestigationCaseItem[];
|
|
506
|
+
reveal: boolean;
|
|
507
|
+
revealRequestId?: number;
|
|
508
|
+
onViewSource: (
|
|
509
|
+
sourceId: string,
|
|
510
|
+
excerpt?: InvestigationSourceExcerpt,
|
|
511
|
+
) => void;
|
|
512
|
+
}) {
|
|
513
|
+
const [open, setOpen] = useState(false);
|
|
514
|
+
const regionId = useId();
|
|
515
|
+
const { metricsMarkersBySource } = useContext(EvidenceNavigationContext);
|
|
516
|
+
const { elementRef, revealAfterToggle } =
|
|
517
|
+
useDisclosureReveal<HTMLDivElement>();
|
|
518
|
+
useLayoutEffect(() => {
|
|
519
|
+
if (reveal) setOpen(true);
|
|
520
|
+
}, [reveal, revealRequestId]);
|
|
521
|
+
return (
|
|
522
|
+
<div>
|
|
523
|
+
<button
|
|
524
|
+
type="button"
|
|
525
|
+
aria-expanded={open}
|
|
526
|
+
aria-controls={regionId}
|
|
527
|
+
onClick={() => {
|
|
528
|
+
setOpen(!open);
|
|
529
|
+
revealAfterToggle(!open);
|
|
530
|
+
}}
|
|
531
|
+
className="flex items-center gap-1.5 rounded-md px-1 py-1 text-xs text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-secondary"
|
|
532
|
+
>
|
|
533
|
+
<CollapseChevron open={open} className="h-3.5 w-3.5" />
|
|
534
|
+
Previous observations · {observations.length}
|
|
535
|
+
</button>
|
|
536
|
+
<div id={regionId} ref={elementRef}>
|
|
537
|
+
<Collapse open={open}>
|
|
538
|
+
<ol className="space-y-3 pt-2">
|
|
539
|
+
{observations.map((observation) => {
|
|
540
|
+
const key = investigationCaseObservationKey(observation);
|
|
541
|
+
const rowItems = caseItems.filter(
|
|
542
|
+
(item) =>
|
|
543
|
+
item.observation &&
|
|
544
|
+
investigationCaseObservationKey(item.observation) === key,
|
|
545
|
+
);
|
|
546
|
+
const rowRoles = [...new Set(rowItems.map((item) => item.role))];
|
|
547
|
+
return (
|
|
548
|
+
<li
|
|
549
|
+
key={`${observation.source.id}-${observation.revision}`}
|
|
550
|
+
data-case-observation={rowItems.length > 0 ? key : undefined}
|
|
551
|
+
className="flex min-w-0 items-start gap-2 text-xs"
|
|
552
|
+
>
|
|
553
|
+
<span className="mt-0.5 shrink-0 text-theme-text-tertiary">
|
|
554
|
+
<span className="font-medium text-theme-text-secondary">
|
|
555
|
+
{observation.source.order === citedOrder
|
|
556
|
+
? "Used for assessment"
|
|
557
|
+
: phaseLabel(observation.source.phase)}
|
|
558
|
+
</span>
|
|
559
|
+
</span>
|
|
560
|
+
<div className="min-w-0 flex-1 space-y-1 text-theme-text-secondary">
|
|
561
|
+
{rowRoles.length > 0 ? (
|
|
562
|
+
<p className="flex flex-wrap items-center gap-1.5">
|
|
563
|
+
<span>{observation.summary || observation.title}</span>
|
|
564
|
+
{rowRoles.map((role) => (
|
|
565
|
+
<AgentRoleChip key={role} role={role} />
|
|
566
|
+
))}
|
|
567
|
+
</p>
|
|
568
|
+
) : (
|
|
569
|
+
<p>{observation.summary || observation.title}</p>
|
|
570
|
+
)}
|
|
571
|
+
{rowItems
|
|
572
|
+
.filter((item) => item.claim)
|
|
573
|
+
.map((item) => (
|
|
574
|
+
<AgentClaimNote
|
|
575
|
+
key={item.index}
|
|
576
|
+
claim={item.claim}
|
|
577
|
+
role={rowRoles.length > 1 ? item.role : undefined}
|
|
578
|
+
className="pt-1"
|
|
579
|
+
/>
|
|
580
|
+
))}
|
|
581
|
+
{evidenceHasDetails(
|
|
582
|
+
observation.data,
|
|
583
|
+
observation.summary,
|
|
584
|
+
) && (
|
|
585
|
+
<EvidenceBody
|
|
586
|
+
data={observation.data}
|
|
587
|
+
cardSummary={observation.summary}
|
|
588
|
+
changeCoverage={metricsMarkersBySource?.get(
|
|
589
|
+
observation.source.id,
|
|
590
|
+
)}
|
|
591
|
+
/>
|
|
592
|
+
)}
|
|
593
|
+
</div>
|
|
594
|
+
<SourceButton
|
|
595
|
+
ariaLabel={`View result for ${phaseLabel(observation.source.phase).toLowerCase()} observation of ${observation.title}`}
|
|
596
|
+
onClick={() =>
|
|
597
|
+
onViewSource(
|
|
598
|
+
observation.source.id,
|
|
599
|
+
evidenceSourceExcerpt(observation.data),
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
/>
|
|
603
|
+
</li>
|
|
604
|
+
);
|
|
605
|
+
})}
|
|
606
|
+
</ol>
|
|
607
|
+
</Collapse>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
);
|
|
611
|
+
}
|
|
@@ -294,9 +294,7 @@ describe("RecentList", () => {
|
|
|
294
294
|
|
|
295
295
|
describe("InvestigationHome", () => {
|
|
296
296
|
it("points users toward history or a focused resource investigation", () => {
|
|
297
|
-
const html = renderToStaticMarkup(
|
|
298
|
-
<InvestigationHome agentLabel="Codex" />,
|
|
299
|
-
);
|
|
297
|
+
const html = renderToStaticMarkup(<InvestigationHome agentLabel="Codex" />);
|
|
300
298
|
const text = visible(html);
|
|
301
299
|
expect(text).toContain("Choose an investigation");
|
|
302
300
|
expect(text).toContain("start a focused investigation with Codex");
|
|
@@ -293,7 +293,8 @@ export function RecentList({
|
|
|
293
293
|
? `${parsed.account} · ${parsed.region}`
|
|
294
294
|
: r.context;
|
|
295
295
|
const readableKind = pluralToKind(r.kind);
|
|
296
|
-
const kind =
|
|
296
|
+
const kind =
|
|
297
|
+
groupsByKind.get(readableKind)!.size > 1
|
|
297
298
|
? `${readableKind} · ${r.group || "core"}`
|
|
298
299
|
: readableKind;
|
|
299
300
|
const initialIssue = r.health?.topReason?.trim();
|