@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.
Files changed (83) hide show
  1. package/package.json +2 -2
  2. package/src/api/client.ts +32 -0
  3. package/src/api/diagnose.ts +7 -1
  4. package/src/components/CloudConnectFlow.tsx +21 -7
  5. package/src/components/CloudFunnelButton.tsx +85 -8
  6. package/src/components/cloudConnectHandoff.test.ts +10 -1
  7. package/src/components/cloudConnectHandoff.ts +15 -1
  8. package/src/components/diagnose/AIMarkdown.tsx +87 -0
  9. package/src/components/diagnose/ActivityTurn.tsx +1244 -0
  10. package/src/components/diagnose/AgentControls.tsx +603 -0
  11. package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
  12. package/src/components/diagnose/AnalysisStory.tsx +13 -18
  13. package/src/components/diagnose/ApplyDialog.tsx +289 -0
  14. package/src/components/diagnose/AssessmentCard.tsx +1579 -0
  15. package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
  16. package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
  17. package/src/components/diagnose/EvidenceCard.tsx +611 -0
  18. package/src/components/diagnose/Home.test.tsx +1 -3
  19. package/src/components/diagnose/Home.tsx +2 -1
  20. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
  21. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
  22. package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
  23. package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
  24. package/src/components/diagnose/InvestigationView.tsx +39 -51
  25. package/src/components/diagnose/StoryExcerpt.tsx +118 -0
  26. package/src/components/diagnose/assessmentCopy.ts +169 -0
  27. package/src/components/diagnose/investigationCase.test.tsx +658 -2
  28. package/src/components/diagnose/investigationCase.ts +135 -26
  29. package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +682 -0
  30. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +8 -2
  31. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +1 -1
  32. package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
  34. package/src/components/diagnose/investigationEvidence/adapters/listings.ts +136 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
  41. package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
  42. package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
  43. package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
  44. package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
  45. package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
  46. package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
  47. package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
  48. package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
  49. package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
  50. package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
  51. package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
  52. package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
  53. package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
  54. package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
  55. package/src/components/diagnose/investigationEvidence/index.ts +1 -1
  56. package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
  57. package/src/components/diagnose/investigationEvidence/observations.ts +13 -0
  58. package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
  59. package/src/components/diagnose/investigationEvidence/types.ts +97 -2
  60. package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
  61. package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
  62. package/src/components/diagnose/investigationState.test.ts +2 -2
  63. package/src/components/diagnose/investigationState.ts +2 -2
  64. package/src/components/diagnose/investigationStory.ts +1 -1
  65. package/src/components/diagnose/parts.test.tsx +15 -1
  66. package/src/components/diagnose/parts.tsx +26 -3904
  67. package/src/components/diagnose/target.ts +9 -3
  68. package/src/components/diagnose/toolCallLabel.test.ts +47 -1
  69. package/src/components/diagnose/toolCallLabel.ts +41 -2
  70. package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
  71. package/src/components/home/MCPSetupDialog.tsx +1 -1
  72. package/src/components/home/mcpToolCatalog.ts +27 -0
  73. package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
  74. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  75. package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
  76. package/src/components/resources/PodFilePreview.tsx +1 -1
  77. package/src/components/rightsizing/RightsizingScanView.tsx +20 -1
  78. package/src/components/ui/CommandPalette.tsx +12 -8
  79. package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
  80. package/src/components/ui/Omnibar.tsx +96 -60
  81. package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
  82. package/src/components/workload/WorkloadView.tsx +7 -1
  83. 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
- canRerunInvestigation("investigation", run("running"), false),
53
- ).toBe(false);
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
- canRerunInvestigation("investigation", run("stopping"), false),
67
- ).toBe(false);
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
- canRerunInvestigation("investigation", run("stopped"), false),
94
- ).toBe(true);
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 { TRANSITION_BACKDROP, TRANSITION_DRAWER, overlayTransitionStyle } from "../../utils/animation";
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 = groupsByKind.get(readableKind)!.size > 1
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();