@skyhook-io/radar-app 1.13.4 → 1.14.0

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 (230) hide show
  1. package/package.json +8 -8
  2. package/src/App.tsx +60 -24
  3. package/src/RadarApp.tsx +5 -0
  4. package/src/api/client.authRedirect.test.ts +12 -1
  5. package/src/api/client.prometheusStatus.test.ts +24 -0
  6. package/src/api/client.ts +325 -16
  7. package/src/api/diagnose.ts +62 -15
  8. package/src/api/drain-plan.test.ts +86 -0
  9. package/src/api/quotas.test.ts +26 -0
  10. package/src/api/quotas.ts +20 -0
  11. package/src/api/workloadMetrics.test.ts +40 -0
  12. package/src/api/workloadMetrics.ts +68 -0
  13. package/src/components/CloudConnectFlow.tsx +179 -42
  14. package/src/components/CloudFunnelButton.tsx +107 -36
  15. package/src/components/ConnectionErrorView.test.tsx +30 -0
  16. package/src/components/ConnectionErrorView.tsx +31 -19
  17. package/src/components/SyncProgressPanel.tsx +73 -0
  18. package/src/components/audit/AuditView.tsx +2 -0
  19. package/src/components/cloudConnectHandoff.test.ts +140 -0
  20. package/src/components/cloudConnectHandoff.ts +124 -0
  21. package/src/components/diagnose/AgentCase.tsx +131 -0
  22. package/src/components/diagnose/DiagnoseContext.tsx +2 -1
  23. package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
  24. package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
  25. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
  26. package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
  27. package/src/components/diagnose/InvestigationView.tsx +222 -5
  28. package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
  29. package/src/components/diagnose/investigationCase.test.tsx +1568 -0
  30. package/src/components/diagnose/investigationCase.ts +439 -0
  31. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
  48. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  49. package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
  50. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  51. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  52. package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
  53. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  54. package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
  55. package/src/components/diagnose/investigationEvidence/types.ts +512 -0
  56. package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
  57. package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
  58. package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
  59. package/src/components/diagnose/investigationMetrics.test.ts +712 -0
  60. package/src/components/diagnose/investigationMetrics.ts +393 -0
  61. package/src/components/diagnose/investigationSourceFocus.ts +2 -0
  62. package/src/components/diagnose/investigationState.test.ts +322 -0
  63. package/src/components/diagnose/investigationState.ts +147 -25
  64. package/src/components/diagnose/parts.test.tsx +482 -0
  65. package/src/components/diagnose/parts.tsx +418 -41
  66. package/src/components/diagnose/target.test.ts +19 -0
  67. package/src/components/diagnose/target.ts +3 -2
  68. package/src/components/gitops/GitOpsView.tsx +7 -0
  69. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  70. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  71. package/src/components/resource/PrometheusCharts.tsx +4 -3
  72. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  73. package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
  74. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  75. package/src/components/resource/RightsizingStrip.tsx +15 -3
  76. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  77. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  78. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  79. package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
  80. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  81. package/src/components/resource/workloadMetricValues.ts +30 -0
  82. package/src/components/resources/PodFilePreview.test.tsx +131 -0
  83. package/src/components/resources/PodFilePreview.tsx +394 -0
  84. package/src/components/resources/PodFilesystemModal.tsx +157 -67
  85. package/src/components/resources/ResourcesView.tsx +44 -6
  86. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  87. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  88. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  89. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  90. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  91. package/src/components/settings/SettingsDialog.tsx +94 -14
  92. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  93. package/src/components/traffic/TrafficGraph.tsx +6 -6
  94. package/src/components/traffic/TrafficView.tsx +10 -9
  95. package/src/components/traffic/trafficFilters.test.ts +28 -1
  96. package/src/components/traffic/trafficFilters.ts +31 -0
  97. package/src/components/workload/WorkloadView.tsx +19 -8
  98. package/src/context/ConnectionContext.tsx +48 -3
  99. package/src/context/DiagnoseCustomization.test.tsx +26 -0
  100. package/src/context/DiagnoseCustomization.tsx +15 -2
  101. package/src/index.css +17 -0
  102. package/src/index.ts +1 -0
  103. package/src/utils/navigation.ts +1 -1
  104. package/src/utils/shell-safe.test.ts +25 -1
  105. package/src/utils/shell-safe.ts +16 -0
  106. package/src/utils/topology-namespace.test.ts +64 -0
  107. package/src/utils/topology-namespace.ts +18 -0
  108. package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
  109. package/src/components/diagnose/investigationEvidence.ts +0 -4784
  110. package/src/components/gitops/GitOpsActions.tsx +0 -1
  111. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  112. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  113. package/src/components/gitops/SyncCountdown.tsx +0 -1
  114. package/src/components/gitops/index.ts +0 -4
  115. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  116. package/src/components/logs/JsonLogLine.tsx +0 -1
  117. package/src/components/logs/LogCore.tsx +0 -2
  118. package/src/components/logs/useLogBuffer.ts +0 -2
  119. package/src/components/logs/useLogSearch.ts +0 -1
  120. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  121. package/src/components/resources/drawer-components.tsx +0 -1
  122. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  124. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  126. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  127. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  128. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  130. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  131. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  132. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  133. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  134. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  191. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  198. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  199. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  200. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  201. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  202. package/src/components/resources/renderers/index.ts +0 -94
  203. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  204. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  205. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  206. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  207. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  208. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  209. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  210. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  211. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  212. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  213. package/src/components/resources/resource-utils-argo.ts +0 -2
  214. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  215. package/src/components/resources/resource-utils-eso.ts +0 -2
  216. package/src/components/resources/resource-utils-flux.ts +0 -2
  217. package/src/components/resources/resource-utils-istio.ts +0 -2
  218. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  219. package/src/components/resources/resource-utils-knative.ts +0 -2
  220. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  221. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  222. package/src/components/resources/resource-utils-traefik.ts +0 -1
  223. package/src/components/resources/resource-utils-trivy.ts +0 -2
  224. package/src/components/resources/resource-utils-velero.ts +0 -2
  225. package/src/components/shared/EditableYamlView.tsx +0 -24
  226. package/src/components/timeline/DiffViewer.tsx +0 -1
  227. package/src/components/traffic/index.ts +0 -3
  228. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  229. package/src/components/ui/MetricsChart.tsx +0 -1
  230. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -37,6 +37,8 @@ import {
37
37
  type AgentInfo,
38
38
  type ApplyMutationOutcome,
39
39
  type ExecutionProfile,
40
+ type DiagnoseStreamEvent,
41
+ type MCPServerStatus,
40
42
  } from "../../api/diagnose";
41
43
  import { Collapse, CollapseChevron } from "@skyhook-io/k8s-ui";
42
44
  import { Markdown } from "../ui/Markdown";
@@ -52,6 +54,11 @@ import {
52
54
  type InvestigationRootCauseEvidenceResolution,
53
55
  type InvestigationEvidenceSource,
54
56
  } from "./investigationEvidence";
57
+ import type {
58
+ InvestigationCaseItem,
59
+ InvestigationCaseResolution,
60
+ } from "./investigationCase";
61
+ import { AgentClaimNote } from "./AgentCase";
55
62
 
56
63
  import { useDisclosureReveal } from "./useDisclosureReveal";
57
64
 
@@ -448,8 +455,69 @@ export type Turn = {
448
455
  // Set from the replay/live boundary when the terminal event arrives. Historical
449
456
  // conclusions render immediately; conclusions observed live enter smoothly.
450
457
  animateResult?: boolean;
458
+ // The latest startup phase the server reported before the agent's first
459
+ // message. It drives the pending status line only; it is never a transcript item.
460
+ startup?: StartupSignal;
451
461
  };
452
462
 
463
+ export type StartupSignal = {
464
+ phase: "investigating" | "connected" | "ready";
465
+ model?: string;
466
+ toolCount?: number;
467
+ mcpServers?: MCPServerStatus[];
468
+ };
469
+
470
+ const STARTUP_PHASE_RANK: Record<StartupSignal["phase"], number> = {
471
+ investigating: 0,
472
+ connected: 1,
473
+ ready: 2,
474
+ };
475
+
476
+ // Folds a phase event into the turn's startup signal. The handshake and the
477
+ // CLI's init line are reported by different goroutines, so a later event can
478
+ // name an earlier phase; the furthest phase wins and the init facts are kept.
479
+ export function mergeStartupSignal(
480
+ prev: StartupSignal | undefined,
481
+ event: Pick<
482
+ DiagnoseStreamEvent,
483
+ "phase" | "model" | "toolCount" | "mcpServers"
484
+ >,
485
+ ): StartupSignal | undefined {
486
+ const phase = event.phase;
487
+ if (phase !== "investigating" && phase !== "connected" && phase !== "ready")
488
+ return prev;
489
+ const facts =
490
+ phase === "ready"
491
+ ? {
492
+ model: event.model,
493
+ toolCount: event.toolCount,
494
+ mcpServers: event.mcpServers,
495
+ }
496
+ : {};
497
+ if (prev && STARTUP_PHASE_RANK[prev.phase] >= STARTUP_PHASE_RANK[phase]) {
498
+ return phase === "ready" ? { ...prev, ...facts } : prev;
499
+ }
500
+ return { ...prev, ...facts, phase };
501
+ }
502
+
503
+ function startupLabel(startup: StartupSignal, agentLabel: string): string {
504
+ switch (startup.phase) {
505
+ case "investigating":
506
+ return `${agentLabel} starting…`;
507
+ case "connected":
508
+ return "Connected to Radar's tools";
509
+ case "ready": {
510
+ const parts = [`${agentLabel} ready`];
511
+ if (startup.model) parts.push(startup.model);
512
+ if (startup.toolCount !== undefined)
513
+ parts.push(
514
+ `${startup.toolCount} Radar ${startup.toolCount === 1 ? "tool" : "tools"}`,
515
+ );
516
+ return parts.join(" · ");
517
+ }
518
+ }
519
+ }
520
+
453
521
  // TimelineItem is one ordered transcript entry: agent reasoning, or a tool call.
454
522
  export type TimelineItem =
455
523
  | { kind: "thinking"; text: string; animate?: boolean }
@@ -542,6 +610,7 @@ export function upsertTool(
542
610
 
543
611
  export function TurnView({
544
612
  turn,
613
+ agentLabel,
545
614
  onApply,
546
615
  onViewExplanation,
547
616
  onCheckStatus,
@@ -551,12 +620,16 @@ export function TurnView({
551
620
  evidenceStepIds,
552
621
  onViewEvidence,
553
622
  sourceRevealRequest,
623
+ assessmentSources,
554
624
  }: {
555
625
  turn: Turn;
626
+ agentLabel?: string;
556
627
  onApply?: (fix: string) => void;
557
628
  onViewExplanation?: () => void;
558
629
  onCheckStatus?: () => void;
559
630
  onRetryDiagnosis?: () => void;
631
+ /** Sources and agent items an answer turn cited; answers otherwise show none. */
632
+ assessmentSources?: ReactNode;
560
633
  // In the maximized workspace the pinned turn's conclusion renders in the side rail,
561
634
  // so the transcript suppresses its own copy (reasoning + tool calls still show).
562
635
  hideConclusion?: boolean;
@@ -637,6 +710,8 @@ export function TurnView({
637
710
  running={turn.status === "running"}
638
711
  applyMode={turn.apply}
639
712
  followup={followup}
713
+ startup={turn.startup}
714
+ agentLabel={agentLabel}
640
715
  turnIndex={turnIndex}
641
716
  evidenceStepIds={evidenceStepIds}
642
717
  onViewEvidence={onViewEvidence}
@@ -658,6 +733,7 @@ export function TurnView({
658
733
  followup={followup}
659
734
  onCheckStatus={onCheckStatus}
660
735
  animate={turn.animateResult !== false}
736
+ assessmentSources={assessmentSources}
661
737
  />
662
738
  ) : (
663
739
  <EmptyResult animate={turn.animateResult !== false} />
@@ -1069,6 +1145,8 @@ export function Timeline({
1069
1145
  running,
1070
1146
  applyMode,
1071
1147
  followup,
1148
+ startup,
1149
+ agentLabel = "Agent",
1072
1150
  turnIndex,
1073
1151
  evidenceStepIds,
1074
1152
  onViewEvidence,
@@ -1078,6 +1156,8 @@ export function Timeline({
1078
1156
  running: boolean;
1079
1157
  applyMode?: boolean;
1080
1158
  followup?: boolean;
1159
+ startup?: StartupSignal;
1160
+ agentLabel?: string;
1081
1161
  turnIndex?: number;
1082
1162
  evidenceStepIds?: ReadonlySet<string>;
1083
1163
  onViewEvidence?: (sourceId: string) => void;
@@ -1093,7 +1173,8 @@ export function Timeline({
1093
1173
  ? "Working"
1094
1174
  : "Investigation";
1095
1175
  // The live status verb tracks the running tool ("Reading logs…") so the wait is
1096
- // informative, not a generic spinner; falls back to a phase-appropriate label.
1176
+ // informative, not a generic spinner; before the first item it reports the
1177
+ // startup phases the server actually observed, never a timer-based guess.
1097
1178
  const activeTool = [...items]
1098
1179
  .reverse()
1099
1180
  .find((it) => it.kind === "tool" && it.status !== "done") as
@@ -1104,9 +1185,18 @@ export function Timeline({
1104
1185
  ? toolActivity(activeTool.tool)
1105
1186
  : items.length > 0
1106
1187
  ? "Working…"
1107
- : followup
1108
- ? "Thinking…"
1109
- : "Starting investigation…";
1188
+ : startup
1189
+ ? startupLabel(startup, agentLabel)
1190
+ : followup
1191
+ ? "Thinking…"
1192
+ : "Starting investigation…";
1193
+ const failedServers = (startup?.mcpServers ?? []).filter(
1194
+ (server) => server.status !== "connected",
1195
+ );
1196
+ const radarServer = failedServers.find((server) => server.name === "radar");
1197
+ const allDefiniteFailures = failedServers.every((server) =>
1198
+ mcpStatusIsFailure(server.status),
1199
+ );
1110
1200
  return (
1111
1201
  <div className="space-y-1.5">
1112
1202
  {items.length > 0 && (
@@ -1114,6 +1204,33 @@ export function Timeline({
1114
1204
  {heading}
1115
1205
  </div>
1116
1206
  )}
1207
+ {failedServers.length > 0 && (
1208
+ <div
1209
+ role="status"
1210
+ className="flex items-start gap-1.5 rounded border border-semantic-warning/40 bg-semantic-warning/10 p-2 text-[11px] leading-snug text-theme-text-secondary"
1211
+ >
1212
+ <AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-semantic-warning" />
1213
+ <span>
1214
+ {failedServers.map((server, i) => (
1215
+ <span key={server.name}>
1216
+ {i > 0 ? "; " : ""}
1217
+ MCP server{" "}
1218
+ <span className="font-medium text-theme-text-primary">
1219
+ {server.name}
1220
+ </span>{" "}
1221
+ {mcpStatusPhrase(server.status)}
1222
+ </span>
1223
+ ))}
1224
+ {radarServer
1225
+ ? mcpStatusIsFailure(radarServer.status)
1226
+ ? ` at startup. ${agentLabel} had no Radar tools this turn, so it could not use Radar's cluster evidence.`
1227
+ : ` at startup. Radar's tools may not have been available to ${agentLabel} this turn.`
1228
+ : allDefiniteFailures
1229
+ ? ` at startup. ${agentLabel} ran this turn without those tools.`
1230
+ : ` at startup. Those tools may not have been available to ${agentLabel} this turn.`}
1231
+ </span>
1232
+ </div>
1233
+ )}
1117
1234
  {items.map((it, i) => {
1118
1235
  if (it.kind === "thinking") {
1119
1236
  return (
@@ -1254,22 +1371,45 @@ function RunningStatus({ label }: { label: string }) {
1254
1371
  }, []);
1255
1372
  const sinceChange = elapsed - lastChangeRef.current;
1256
1373
  const stalled = elapsed >= 30 && sinceChange >= 30;
1374
+ const counter = runningElapsedLabel(elapsed);
1257
1375
  return (
1258
1376
  <div className="flex items-center gap-2 pt-1 text-xs">
1259
1377
  <Loader2 className="h-3 w-3 shrink-0 animate-spin text-accent" />
1260
- <span className="ai-shimmer">{label}</span>
1261
- {elapsed >= 3 && (
1262
- <span className="shrink-0 text-theme-text-tertiary">· {elapsed}s</span>
1263
- )}
1378
+ <span className="ai-shimmer min-w-0 truncate">{label}</span>
1264
1379
  {stalled && (
1265
1380
  <span className="shrink-0 text-theme-text-tertiary">
1266
1381
  · still working — no update for {sinceChange}s
1267
1382
  </span>
1268
1383
  )}
1384
+ {counter && (
1385
+ <span className="ml-auto shrink-0 tabular-nums text-theme-text-tertiary">
1386
+ {counter}
1387
+ </span>
1388
+ )}
1269
1389
  </div>
1270
1390
  );
1271
1391
  }
1272
1392
 
1393
+ // The wait the operator feels is the whole turn's, so the counter is a
1394
+ // row-level figure set apart from the label: "Connected to Radar's tools"
1395
+ // followed by "10s" read as if the handshake took that long.
1396
+ export function runningElapsedLabel(elapsed: number): string | undefined {
1397
+ return elapsed >= 3 ? `${elapsed}s elapsed` : undefined;
1398
+ }
1399
+
1400
+ // Claude Code reports each MCP server as connected, failed, needs-auth, or
1401
+ // pending. Only the first two are definite outcomes; anything else is left as
1402
+ // the CLI's own word so the warning never claims more than it knows.
1403
+ function mcpStatusIsFailure(status: string): boolean {
1404
+ return status === "failed" || status === "needs-auth";
1405
+ }
1406
+
1407
+ function mcpStatusPhrase(status: string): string {
1408
+ if (status === "failed") return "failed to connect";
1409
+ if (status === "needs-auth") return "needs authentication";
1410
+ return `is ${status}`;
1411
+ }
1412
+
1273
1413
  // Maps a running tool to a human verb so the status line reads as activity, not
1274
1414
  // machinery. Falls back to the prettified tool name for anything unmapped.
1275
1415
  function toolActivity(tool: string): string {
@@ -1321,6 +1461,9 @@ function ToolRow({
1321
1461
  const richResult =
1322
1462
  !!step.result && (isJsonPayload(step.result) || step.result.length > 200);
1323
1463
  const done = step.status === "done";
1464
+ const durationLabel = toolDurationLabel(step.ms);
1465
+ const errorReason =
1466
+ step.isError === true ? toolErrorReason(step.result) : undefined;
1324
1467
  const outcomeLabel = !done
1325
1468
  ? "Running"
1326
1469
  : step.isError === true
@@ -1359,9 +1502,16 @@ function ToolRow({
1359
1502
  {argumentsPreview}
1360
1503
  </span>
1361
1504
  )}
1362
- {step.ms != null && (
1505
+ {errorReason && !open && (
1506
+ // The arguments give way first: they are still readable expanded,
1507
+ // while the reason is the one thing this row exists to say.
1508
+ <span className="investigation-tool-reason max-w-full shrink-0 truncate text-[11px] text-semantic-error">
1509
+ {middleTruncate(errorReason)}
1510
+ </span>
1511
+ )}
1512
+ {durationLabel && (
1363
1513
  <span className="ml-auto shrink-0 text-[11px] text-theme-text-tertiary">
1364
- {step.ms}ms
1514
+ {durationLabel}
1365
1515
  </span>
1366
1516
  )}
1367
1517
  {hasDetail && <CollapseChevron open={open} className="h-3.5 w-3.5" />}
@@ -1454,6 +1604,7 @@ function ToolRow({
1454
1604
  {step.result && (
1455
1605
  <PayloadBlock
1456
1606
  label="Original result"
1607
+ detail={step.ms != null ? `${step.ms}ms` : undefined}
1457
1608
  text={step.result}
1458
1609
  sourceExcerpt={sourceExcerpt}
1459
1610
  revealRequestId={revealRequestId}
@@ -1488,6 +1639,59 @@ function ToolRow({
1488
1639
  );
1489
1640
  }
1490
1641
 
1642
+ // Only outliers carry information on the collapsed row: a slow log fetch or a
1643
+ // probe that hung. Sub-second calls stay silent; the exact figure lives in the
1644
+ // expanded result header.
1645
+ export function toolDurationLabel(ms: number | undefined): string | undefined {
1646
+ if (ms == null || ms < 2000) return undefined;
1647
+ return `${Math.round(ms / 1000)}s`;
1648
+ }
1649
+
1650
+ // The producer's own words for a failed call, reduced to one line. Radar's MCP
1651
+ // errors are plain text; a JSON envelope with an `error` field is unwrapped.
1652
+ // Radar's not-found errors append retry hints for the agent after an em dash;
1653
+ // only the clause before it says what failed, so the hints are dropped.
1654
+ export function toolErrorReason(
1655
+ result: string | undefined,
1656
+ ): string | undefined {
1657
+ if (!result) return undefined;
1658
+ let text = result;
1659
+ try {
1660
+ const parsed: unknown = JSON.parse(result);
1661
+ if (
1662
+ parsed &&
1663
+ typeof parsed === "object" &&
1664
+ typeof (parsed as { error?: unknown }).error === "string"
1665
+ ) {
1666
+ text = (parsed as { error: string }).error;
1667
+ }
1668
+ } catch {
1669
+ // plain text
1670
+ }
1671
+ const line = text
1672
+ .split("\n")
1673
+ .map((part) => part.trim())
1674
+ .find((part) => part.length > 0);
1675
+ if (!line) return undefined;
1676
+ const clause = line.split(" — ")[0].trim();
1677
+ return clause || line;
1678
+ }
1679
+
1680
+ const COLLAPSED_REASON_MAX = 100;
1681
+ const COLLAPSED_REASON_TAIL = 36;
1682
+
1683
+ // Keeps both ends of a long reason: the leading words say what failed and the
1684
+ // tail usually carries the identifier, which a plain end-truncation would lose.
1685
+ export function middleTruncate(
1686
+ text: string,
1687
+ max = COLLAPSED_REASON_MAX,
1688
+ tail = COLLAPSED_REASON_TAIL,
1689
+ ): string {
1690
+ if (text.length <= max) return text;
1691
+ const head = Math.max(1, max - tail - 1);
1692
+ return `${text.slice(0, head).trimEnd()}…${text.slice(-tail).trimStart()}`;
1693
+ }
1694
+
1491
1695
  // isJsonPayload / formatJson — a tool result is "structured" if it parses as JSON.
1492
1696
  function isJsonPayload(text: string): boolean {
1493
1697
  try {
@@ -1509,6 +1713,7 @@ function formatJson(text: string): string | null {
1509
1713
  // to keep indentation) or wrapped text (logs/prose), with copy + optional action.
1510
1714
  function PayloadBlock({
1511
1715
  label,
1716
+ detail,
1512
1717
  text,
1513
1718
  truncated,
1514
1719
  action,
@@ -1516,6 +1721,7 @@ function PayloadBlock({
1516
1721
  revealRequestId,
1517
1722
  }: {
1518
1723
  label: string;
1724
+ detail?: string;
1519
1725
  text: string;
1520
1726
  truncated?: boolean;
1521
1727
  action?: ReactNode;
@@ -1547,6 +1753,11 @@ function PayloadBlock({
1547
1753
  <div className="mb-0.5 flex items-center justify-between gap-2">
1548
1754
  <span className="text-[10px] uppercase tracking-wide text-theme-text-tertiary">
1549
1755
  {label}
1756
+ {detail && (
1757
+ <span className="ml-1.5 normal-case tracking-normal">
1758
+ · {detail}
1759
+ </span>
1760
+ )}
1550
1761
  </span>
1551
1762
  <div className="flex items-center gap-2">
1552
1763
  {action}
@@ -1711,6 +1922,7 @@ export function ResultCard({
1711
1922
  showDisclaimer = true,
1712
1923
  coverageLimited = false,
1713
1924
  evidenceConflict = false,
1925
+ evidenceConflictExplainedBy,
1714
1926
  compactActions = false,
1715
1927
  assessmentAction,
1716
1928
  assessmentSources,
@@ -1734,6 +1946,12 @@ export function ResultCard({
1734
1946
  coverageLimited?: boolean;
1735
1947
  /** Marks a healthy agent assessment that conflicts with same-turn Key evidence. */
1736
1948
  evidenceConflict?: boolean;
1949
+ /**
1950
+ * Titles of the conflicting cards when the agent placed a "not a problem"
1951
+ * note on every one of them; the banner then points at the agent's reasons
1952
+ * rather than accusing evidence it already addressed.
1953
+ */
1954
+ evidenceConflictExplainedBy?: string[];
1737
1955
  /** Show only the recommended (or first) action until the user asks for more. */
1738
1956
  compactActions?: boolean;
1739
1957
  actionNotice?: string;
@@ -1753,7 +1971,14 @@ export function ResultCard({
1753
1971
  // flag in its structured envelope. Never promote an ordinary answer into an
1754
1972
  // authoritative investigation conclusion.
1755
1973
  if (followup)
1756
- return <FollowupAnswer diagnosis={diagnosis} animate={animate} />;
1974
+ return (
1975
+ <>
1976
+ <FollowupAnswer diagnosis={diagnosis} animate={animate} />
1977
+ {assessmentSources ? (
1978
+ <AssessmentSourceDetails>{assessmentSources}</AssessmentSourceDetails>
1979
+ ) : null}
1980
+ </>
1981
+ );
1757
1982
  if (diagnosis.healthy && !diagnosis.rootCause)
1758
1983
  return section === "actions" ? null : (
1759
1984
  <AllClearCard
@@ -1762,6 +1987,7 @@ export function ResultCard({
1762
1987
  showDisclaimer={showDisclaimer}
1763
1988
  coverageLimited={coverageLimited}
1764
1989
  evidenceConflict={evidenceConflict}
1990
+ evidenceConflictExplainedBy={evidenceConflictExplainedBy}
1765
1991
  assessmentAction={assessmentAction}
1766
1992
  assessmentSources={assessmentSources}
1767
1993
  />
@@ -1814,40 +2040,173 @@ export function ResultCard({
1814
2040
  );
1815
2041
  }
1816
2042
 
2043
+ /**
2044
+ * Sources the assessment cites: root-cause links first, then every source an
2045
+ * agent item cites. Each row shows the roles the agent gave that source; a
2046
+ * claim the pane could not pin to one observation is shown here in full.
2047
+ */
2048
+ export function assessmentSourceRows(
2049
+ resolution: InvestigationRootCauseEvidenceResolution | undefined,
2050
+ investigationCase: InvestigationCaseResolution | undefined,
2051
+ ): Array<{
2052
+ source: InvestigationEvidenceSource;
2053
+ items: InvestigationCaseItem[];
2054
+ }> {
2055
+ const rows = new Map<
2056
+ string,
2057
+ { source: InvestigationEvidenceSource; items: InvestigationCaseItem[] }
2058
+ >();
2059
+ if (resolution?.status === "linked") {
2060
+ for (const link of resolution.links) {
2061
+ rows.set(link.source.id, { source: link.source, items: [] });
2062
+ }
2063
+ }
2064
+ for (const item of investigationCase?.items ?? []) {
2065
+ const row = rows.get(item.source.id) ?? { source: item.source, items: [] };
2066
+ row.items.push(item);
2067
+ rows.set(item.source.id, row);
2068
+ }
2069
+ return [...rows.values()];
2070
+ }
2071
+
2072
+ function joinTitles(titles: string[]): string {
2073
+ if (titles.length <= 1) return titles[0] ?? "";
2074
+ if (titles.length === 2) return `${titles[0]} and ${titles[1]}`;
2075
+ return `${titles.slice(0, -1).join(", ")}, and ${titles[titles.length - 1]}`;
2076
+ }
2077
+
2078
+ /**
2079
+ * Provenance for one assessment: the exact tool results it cited, each with
2080
+ * its source, and under a source only the agent notes that are not already
2081
+ * shown on a card. Notes that live on cards are counted, not repeated; an
2082
+ * earlier assessment no longer annotates the Evidence pane, so all of its
2083
+ * notes are listed here instead of being lost.
2084
+ */
1817
2085
  export function AssessmentSources({
1818
2086
  resolution,
2087
+ investigationCase,
2088
+ unlinkedEvidence = 0,
2089
+ evidenceMalformed = false,
2090
+ readOnly = false,
2091
+ renderedGroupIds,
1819
2092
  onViewSource,
1820
2093
  }: {
1821
2094
  resolution?: InvestigationRootCauseEvidenceResolution;
2095
+ investigationCase?: InvestigationCaseResolution;
2096
+ /**
2097
+ * Notes the agent wrote that could not be tied to a Radar result: a
2098
+ * reference that named nothing, a role Radar does not know, a sentence over
2099
+ * the length limit. Radar does not repair them, because repairing one means
2100
+ * deciding what the agent meant, so it says how many were lost instead.
2101
+ */
2102
+ unlinkedEvidence?: number;
2103
+ /**
2104
+ * The agent's notes were not a list at all, so none of them could be read
2105
+ * and no count describes how many were lost.
2106
+ */
2107
+ evidenceMalformed?: boolean;
2108
+ readOnly?: boolean;
2109
+ /**
2110
+ * Groups the Evidence pane actually rendered. A card-placed note whose card
2111
+ * was withheld is shown here instead of being counted as visible elsewhere;
2112
+ * without this the note renders in neither place. Omitted by hosts that do
2113
+ * not know, which keeps the original counting.
2114
+ */
2115
+ renderedGroupIds?: ReadonlySet<string>;
1822
2116
  onViewSource: (sourceId: string) => void;
1823
2117
  }) {
1824
- if (resolution?.status !== "linked" || !resolution.links.length) return null;
2118
+ const rows = assessmentSourceRows(resolution, investigationCase);
2119
+ if (rows.length === 0 && unlinkedEvidence === 0 && !evidenceMalformed)
2120
+ return null;
1825
2121
  return (
1826
2122
  <div className="mt-3 border-t border-theme-border/60 pt-2">
1827
- <h4 className="text-xs font-medium text-theme-text-secondary">
2123
+ <h4 className="text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
1828
2124
  Sources used for this assessment
2125
+ <span className="ml-1.5 font-medium normal-case tracking-normal text-theme-text-tertiary">
2126
+ {rows.length}
2127
+ </span>
1829
2128
  </h4>
1830
- <ul className="mt-1 space-y-1">
1831
- {resolution.links.map((link) => (
1832
- <li key={link.source.id}>
1833
- <button
1834
- type="button"
1835
- aria-label={`View ${prettyTool(link.source.tool)} source used for this assessment`}
1836
- onClick={() => onViewSource(link.source.id)}
1837
- className="flex w-full min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs text-accent-text hover:bg-theme-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
1838
- >
1839
- <FileSearch className="h-3.5 w-3.5 shrink-0" aria-hidden />
1840
- <span className="min-w-0 flex-1">
1841
- <span className="font-medium">
1842
- {prettyTool(link.source.tool)}
1843
- </span>
1844
- <CitedSourceScope source={link.source} />
1845
- </span>
1846
- <span className="shrink-0">View source</span>
1847
- </button>
1848
- </li>
1849
- ))}
2129
+ <ul className="mt-1 divide-y divide-theme-border/50">
2130
+ {rows.map(({ source, items }) => {
2131
+ const shownOnCard = (item: InvestigationCaseItem) =>
2132
+ !!item.claim &&
2133
+ item.placement !== "source" &&
2134
+ (!renderedGroupIds ||
2135
+ (!!item.groupId && renderedGroupIds.has(item.groupId)));
2136
+ const onCards = items.filter(shownOnCard).length;
2137
+ const notes = items.filter(
2138
+ (item) => item.claim && (readOnly || !shownOnCard(item)),
2139
+ );
2140
+ return (
2141
+ <li key={source.id} className="py-1.5">
2142
+ <div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-x-2 px-2">
2143
+ <FileSearch
2144
+ className="mt-0.5 h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
2145
+ aria-hidden
2146
+ />
2147
+ <div className="min-w-0 text-xs">
2148
+ <div className="font-medium text-theme-text-primary">
2149
+ {prettyTool(source.tool)}
2150
+ </div>
2151
+ <CitedSourceScope source={source} />
2152
+ {!readOnly && onCards > 0 ? (
2153
+ <div className="mt-0.5 text-[11px] text-theme-text-tertiary">
2154
+ {onCards === 1
2155
+ ? "1 agent note on an evidence card"
2156
+ : `${onCards} agent notes on evidence cards`}
2157
+ </div>
2158
+ ) : null}
2159
+ </div>
2160
+ <button
2161
+ type="button"
2162
+ aria-label={`View ${prettyTool(source.tool)} source used for this assessment`}
2163
+ onClick={() => onViewSource(source.id)}
2164
+ className="inline-flex shrink-0 items-center gap-1 rounded-md px-1.5 py-0.5 text-xs text-accent-text hover:bg-theme-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
2165
+ >
2166
+ View source
2167
+ </button>
2168
+ </div>
2169
+ {notes.length > 0 ? (
2170
+ <div
2171
+ className="ml-[1.9rem] mr-2 mt-1.5 space-y-1.5"
2172
+ data-source-placed-claims
2173
+ >
2174
+ <div className="text-[10px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
2175
+ {readOnly
2176
+ ? "Notes from this assessment"
2177
+ : "Notes not shown on a card"}
2178
+ </div>
2179
+ {notes.map((item) => (
2180
+ <AgentClaimNote
2181
+ key={item.index}
2182
+ claim={item.claim}
2183
+ role={item.role}
2184
+ subject={
2185
+ item.placement === "source"
2186
+ ? undefined
2187
+ : item.observation?.title
2188
+ }
2189
+ className="border-t-0"
2190
+ />
2191
+ ))}
2192
+ </div>
2193
+ ) : null}
2194
+ </li>
2195
+ );
2196
+ })}
1850
2197
  </ul>
2198
+ {evidenceMalformed ? (
2199
+ <p className="mt-2 text-[11px] text-theme-text-tertiary">
2200
+ The agent&apos;s notes could not be read, so none are shown.
2201
+ </p>
2202
+ ) : null}
2203
+ {unlinkedEvidence > 0 ? (
2204
+ <p className="mt-2 text-[11px] text-theme-text-tertiary">
2205
+ {unlinkedEvidence === 1
2206
+ ? "1 agent note could not be linked to a Radar result and is not shown."
2207
+ : `${unlinkedEvidence} agent notes could not be linked to Radar results and are not shown.`}
2208
+ </p>
2209
+ ) : null}
1851
2210
  </div>
1852
2211
  );
1853
2212
  }
@@ -2316,6 +2675,7 @@ function AllClearCard({
2316
2675
  showDisclaimer,
2317
2676
  coverageLimited,
2318
2677
  evidenceConflict,
2678
+ evidenceConflictExplainedBy,
2319
2679
  assessmentAction,
2320
2680
  assessmentSources,
2321
2681
  }: {
@@ -2324,6 +2684,7 @@ function AllClearCard({
2324
2684
  showDisclaimer: boolean;
2325
2685
  coverageLimited: boolean;
2326
2686
  evidenceConflict: boolean;
2687
+ evidenceConflictExplainedBy?: string[];
2327
2688
  assessmentAction?: ReactNode;
2328
2689
  assessmentSources?: ReactNode;
2329
2690
  }) {
@@ -2337,11 +2698,16 @@ function AllClearCard({
2337
2698
  const summary = detailed
2338
2699
  ? "The agent found no active problem in the evidence it reviewed."
2339
2700
  : report;
2701
+ const explained =
2702
+ evidenceConflict &&
2703
+ !!evidenceConflictExplainedBy &&
2704
+ evidenceConflictExplainedBy.length > 0;
2705
+ const unexplainedConflict = evidenceConflict && !explained;
2340
2706
  return (
2341
2707
  <div className={`mt-3 space-y-2 ${animate ? "animate-result-in" : ""}`}>
2342
2708
  <div
2343
2709
  className={`rounded-lg border p-3 ${
2344
- evidenceConflict
2710
+ unexplainedConflict || explained
2345
2711
  ? "border-amber-500/40 bg-amber-500/5"
2346
2712
  : coverageLimited
2347
2713
  ? "border-amber-500/30 bg-amber-500/5"
@@ -2351,33 +2717,44 @@ function AllClearCard({
2351
2717
  <div className="mb-1 flex items-center justify-between gap-2">
2352
2718
  <div
2353
2719
  className={`flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide ${
2354
- evidenceConflict || coverageLimited
2720
+ unexplainedConflict || explained || coverageLimited
2355
2721
  ? "text-amber-500"
2356
2722
  : "text-emerald-500"
2357
2723
  }`}
2358
2724
  >
2359
- {evidenceConflict || coverageLimited ? (
2725
+ {unexplainedConflict || explained || coverageLimited ? (
2360
2726
  <AlertTriangle className="h-3.5 w-3.5" />
2361
2727
  ) : (
2362
2728
  <CheckCircle2 className="h-3.5 w-3.5" />
2363
2729
  )}
2364
- {evidenceConflict
2730
+ {unexplainedConflict
2365
2731
  ? "Assessment conflicts with captured evidence"
2366
- : coverageLimited
2367
- ? "No problem identified in available evidence"
2368
- : "No problem found in checked evidence"}
2732
+ : explained
2733
+ ? "Agent reports no active problem; adverse evidence remains"
2734
+ : coverageLimited
2735
+ ? "No problem identified in available evidence"
2736
+ : "No problem found in checked evidence"}
2369
2737
  </div>
2370
2738
  <CopyButton text={report} label="Copy assessment" />
2371
2739
  </div>
2372
2740
  <AIMarkdown className="text-sm text-theme-text-primary [overflow-wrap:anywhere] [&_code]:font-normal [&_li]:text-theme-text-primary [&_p]:my-1 [&_p]:text-theme-text-primary [&_p:first-child]:mt-0 [&_p:last-child]:mb-0">
2373
2741
  {summary}
2374
2742
  </AIMarkdown>
2375
- {evidenceConflict ? (
2743
+ {unexplainedConflict ? (
2376
2744
  <p className="mt-2 text-xs text-theme-text-secondary">
2377
2745
  Radar also captured evidence of an active problem. Review that
2378
2746
  evidence before treating the agent&apos;s conclusion as an
2379
2747
  all-clear.
2380
2748
  </p>
2749
+ ) : explained ? (
2750
+ <p className="mt-2 text-xs text-theme-text-secondary">
2751
+ Radar captured evidence of an active problem. The agent explains its
2752
+ interpretation in the note on{" "}
2753
+ {joinTitles(evidenceConflictExplainedBy!)}.
2754
+ {coverageLimited
2755
+ ? " Evidence coverage is also limited — review the limitations in Evidence."
2756
+ : ""}
2757
+ </p>
2381
2758
  ) : coverageLimited ? (
2382
2759
  <p className="mt-2 text-xs text-theme-text-secondary">
2383
2760
  Evidence coverage is limited. Review the limitations in Evidence