@skyhook-io/radar-app 1.14.1 → 1.14.3

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 (74) hide show
  1. package/package.json +2 -2
  2. package/src/api/client.ts +30 -0
  3. package/src/components/CloudConnectFlow.tsx +21 -7
  4. package/src/components/CloudFunnelButton.tsx +85 -8
  5. package/src/components/cloudConnectHandoff.test.ts +10 -1
  6. package/src/components/cloudConnectHandoff.ts +15 -1
  7. package/src/components/diagnose/AIMarkdown.tsx +87 -0
  8. package/src/components/diagnose/ActivityTurn.tsx +1244 -0
  9. package/src/components/diagnose/AgentControls.tsx +603 -0
  10. package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
  11. package/src/components/diagnose/AnalysisStory.tsx +13 -18
  12. package/src/components/diagnose/ApplyDialog.tsx +289 -0
  13. package/src/components/diagnose/AssessmentCard.tsx +1560 -0
  14. package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
  15. package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
  16. package/src/components/diagnose/EvidenceCard.tsx +611 -0
  17. package/src/components/diagnose/Home.test.tsx +1 -3
  18. package/src/components/diagnose/Home.tsx +2 -1
  19. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
  20. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
  21. package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
  22. package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
  23. package/src/components/diagnose/InvestigationView.tsx +11 -28
  24. package/src/components/diagnose/StoryExcerpt.tsx +118 -0
  25. package/src/components/diagnose/assessmentCopy.ts +169 -0
  26. package/src/components/diagnose/investigationCase.test.tsx +631 -2
  27. package/src/components/diagnose/investigationCase.ts +132 -24
  28. package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +660 -0
  29. package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
  30. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
  31. package/src/components/diagnose/investigationEvidence/adapters/listings.ts +134 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
  38. package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
  39. package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
  40. package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
  41. package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
  42. package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
  43. package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
  44. package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
  45. package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
  46. package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
  48. package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
  49. package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
  50. package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
  51. package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
  52. package/src/components/diagnose/investigationEvidence/index.ts +1 -1
  53. package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
  54. package/src/components/diagnose/investigationEvidence/observations.ts +11 -0
  55. package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
  56. package/src/components/diagnose/investigationEvidence/types.ts +97 -2
  57. package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
  58. package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
  59. package/src/components/diagnose/parts.test.tsx +3 -1
  60. package/src/components/diagnose/parts.tsx +26 -3904
  61. package/src/components/diagnose/target.ts +9 -3
  62. package/src/components/diagnose/toolCallLabel.test.ts +47 -1
  63. package/src/components/diagnose/toolCallLabel.ts +41 -2
  64. package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
  65. package/src/components/home/MCPSetupDialog.tsx +1 -1
  66. package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
  67. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  68. package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
  69. package/src/components/resources/PodFilePreview.tsx +1 -1
  70. package/src/components/ui/CommandPalette.tsx +12 -8
  71. package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
  72. package/src/components/ui/Omnibar.tsx +33 -10
  73. package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
  74. package/src/components/ui/Disclosure.tsx +0 -47
@@ -54,8 +54,10 @@ describe("resolveStoryPlacements", () => {
54
54
  expect(repeated.placedAt.get(0)).toBe(0);
55
55
  const html = renderToStaticMarkup(
56
56
  <AnalysisStory
57
- report={"Opening paragraph.\n\n[[radar:evidence=2]]\n\nMore."}
58
- resolveItem={resolver({ 2: "unlinked" })}
57
+ placements={resolveStoryPlacements(
58
+ "Opening paragraph.\n\n[[radar:evidence=2]]\n\nMore.",
59
+ resolver({ 2: "unlinked" }),
60
+ )}
59
61
  renderPlacement={() => null}
60
62
  onReveal={vi.fn()}
61
63
  />,
@@ -88,10 +90,10 @@ describe("resolveStoryPlacements", () => {
88
90
  );
89
91
  const html = renderToStaticMarkup(
90
92
  <AnalysisStory
91
- report={
92
- "Envoy boots [[radar:evidence=0]] then hangs on xDS.\n\nLater, see [[radar:evidence=0]] again."
93
- }
94
- resolveItem={resolver({ 0: target(0, "a") })}
93
+ placements={resolveStoryPlacements(
94
+ "Envoy boots [[radar:evidence=0]] then hangs on xDS.\n\nLater, see [[radar:evidence=0]] again.",
95
+ resolver({ 0: target(0, "a") }),
96
+ )}
95
97
  renderPlacement={() => <div data-card>card</div>}
96
98
  onReveal={vi.fn()}
97
99
  openRequest={1}
@@ -104,10 +106,10 @@ describe("resolveStoryPlacements", () => {
104
106
  it("keeps an inline mention a reference when the agent places that card on its own line later, pointing down", () => {
105
107
  const html = renderToStaticMarkup(
106
108
  <AnalysisStory
107
- report={
108
- "First [[radar:evidence=0]] here.\n\nThen:\n\n[[radar:evidence=0]]"
109
- }
110
- resolveItem={resolver({ 0: target(0, "a") })}
109
+ placements={resolveStoryPlacements(
110
+ "First [[radar:evidence=0]] here.\n\nThen:\n\n[[radar:evidence=0]]",
111
+ resolver({ 0: target(0, "a") }),
112
+ )}
111
113
  renderPlacement={() => <div data-card>card</div>}
112
114
  onReveal={vi.fn()}
113
115
  openRequest={1}
@@ -187,8 +189,10 @@ describe("resolveStoryPlacements", () => {
187
189
  ).join("\n\n");
188
190
  const html = renderToStaticMarkup(
189
191
  <AnalysisStory
190
- report={`${markers}\n\nSee [[radar:evidence=7]] here.`}
191
- resolveItem={resolver(table)}
192
+ placements={resolveStoryPlacements(
193
+ `${markers}\n\nSee [[radar:evidence=7]] here.`,
194
+ resolver(table),
195
+ )}
192
196
  renderPlacement={() => <div data-card>card</div>}
193
197
  onReveal={vi.fn()}
194
198
  openRequest={1}
@@ -199,11 +203,12 @@ describe("resolveStoryPlacements", () => {
199
203
 
200
204
  const ownLine = renderToStaticMarkup(
201
205
  <AnalysisStory
202
- report={Array.from(
203
- { length: 7 },
204
- (_, i) => `[[radar:evidence=${i}]]`,
205
- ).join("\n\n")}
206
- resolveItem={resolver(table)}
206
+ placements={resolveStoryPlacements(
207
+ Array.from({ length: 7 }, (_, i) => `[[radar:evidence=${i}]]`).join(
208
+ "\n\n",
209
+ ),
210
+ resolver(table),
211
+ )}
207
212
  renderPlacement={() => <div data-card>card</div>}
208
213
  onReveal={vi.fn()}
209
214
  openRequest={1}
@@ -238,12 +243,14 @@ describe("AnalysisStory", () => {
238
243
  const render = (report: string, open = false) =>
239
244
  renderToStaticMarkup(
240
245
  <AnalysisStory
241
- report={report}
242
- resolveItem={resolver({
243
- 0: target(0, "crash"),
244
- 1: target(1, "chart"),
245
- 2: "unlinked",
246
- })}
246
+ placements={resolveStoryPlacements(
247
+ report,
248
+ resolver({
249
+ 0: target(0, "crash"),
250
+ 1: target(1, "chart"),
251
+ 2: "unlinked",
252
+ }),
253
+ )}
247
254
  renderPlacement={(t, { compact }) => (
248
255
  <div data-placed={t.domId} data-compact={compact ? "1" : "0"}>
249
256
  {t.item.observation?.title}
@@ -288,8 +295,7 @@ describe("AnalysisStory", () => {
288
295
  expect(collapsed).not.toContain("Expand");
289
296
  const withTrailing = renderToStaticMarkup(
290
297
  <AnalysisStory
291
- report="One paragraph only."
292
- resolveItem={resolver({})}
298
+ placements={resolveStoryPlacements("One paragraph only.", resolver({}))}
293
299
  renderPlacement={() => null}
294
300
  onReveal={vi.fn()}
295
301
  trailing={<p data-trailing>Also checked: nothing else.</p>}
@@ -1,4 +1,4 @@
1
- import { useEffect, useId, useMemo, useState, type ReactNode } from "react";
1
+ import { useEffect, useId, useState, type ReactNode } from "react";
2
2
  import { clsx } from "clsx";
3
3
  import { TRANSITION_CHEVRON } from "@skyhook-io/k8s-ui/utils/animation";
4
4
  import { AlertTriangle, ChevronDown, FileSearch } from "lucide-react";
@@ -50,17 +50,20 @@ function isLoss(
50
50
  * marker reference it, and anything unresolvable is a visible loss. Dedupe is
51
51
  * by resolved observation, not by index — two items can address one read.
52
52
  */
53
- export function resolveStoryPlacements(
54
- report: string,
55
- resolveItem: (index: number) => StoryPlacementTarget | StoryPlacementLoss,
56
- resolveRef?: StoryRefResolver,
57
- ): {
53
+ /** The resolved story: what each marker became and where each card sits. */
54
+ export interface StoryPlacements {
58
55
  segments: StorySegment[];
59
56
  byIndex: Map<number, StoryPlacementResolution>;
60
57
  placedAt: Map<number, number>;
61
58
  placedCount: number;
62
59
  lostItems: number;
63
- } {
60
+ }
61
+
62
+ export function resolveStoryPlacements(
63
+ report: string,
64
+ resolveItem: (index: number) => StoryPlacementTarget | StoryPlacementLoss,
65
+ resolveRef?: StoryRefResolver,
66
+ ): StoryPlacements {
64
67
  const { segments: written } = splitStory(report, resolveRef);
65
68
  const byIndex = new Map<number, StoryPlacementResolution>();
66
69
  // The segment position that renders each placed index's card; a repeated
@@ -312,9 +315,7 @@ const STORY_PROSE_CLASS =
312
315
  * why the summary and the unresolved list live above it.
313
316
  */
314
317
  export function AnalysisStory({
315
- report,
316
- resolveItem,
317
- resolveRef,
318
+ placements: story,
318
319
  renderPlacement,
319
320
  onReveal,
320
321
  onViewSource,
@@ -322,10 +323,8 @@ export function AnalysisStory({
322
323
  openRequest = 0,
323
324
  className,
324
325
  }: {
325
- report: string;
326
- resolveItem: (index: number) => StoryPlacementTarget | StoryPlacementLoss;
327
- /** Maps a ledger ref an agent wrote as a marker to its evidence index. */
328
- resolveRef?: StoryRefResolver;
326
+ /** Resolved once by the host, which also needs it for its own counts. */
327
+ placements: StoryPlacements;
329
328
  /** Renders the card for a placed item; `compact` when it sits in the collapsed preview. */
330
329
  renderPlacement: (
331
330
  target: StoryPlacementTarget,
@@ -348,10 +347,6 @@ export function AnalysisStory({
348
347
  if (openRequest > 0) setOpen(true);
349
348
  }, [openRequest]);
350
349
  const regionId = useId();
351
- const story = useMemo(
352
- () => resolveStoryPlacements(report, resolveItem, resolveRef),
353
- [report, resolveItem, resolveRef],
354
- );
355
350
  const firstPlacedAt = story.segments.findIndex(
356
351
  (segment) =>
357
352
  segment.kind === "placement" &&
@@ -0,0 +1,289 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ CheckCircle2,
4
+ AlertTriangle,
5
+ ShieldCheck,
6
+ Wrench,
7
+ Sparkles,
8
+ RefreshCw,
9
+ } from "lucide-react";
10
+ import { DialogPortal } from "@skyhook-io/k8s-ui/components/ui/DialogPortal";
11
+ import { parseContextName } from "../../utils/context-name";
12
+ import type { Diagnosis, ApplyMutationOutcome } from "../../api/diagnose";
13
+ import { AIMarkdown, CopyButton } from "./AIMarkdown";
14
+
15
+ // The Apply confirmation — wider than a generic confirm so the recommended fix
16
+ // (rendered markdown) is legible, making it unambiguous what the one click does.
17
+ export function ApplyDialog({
18
+ open,
19
+ onClose,
20
+ onConfirm,
21
+ agentLabel,
22
+ resourceLabel,
23
+ context,
24
+ fix,
25
+ reason,
26
+ precondition,
27
+ managedBy,
28
+ confidence,
29
+ }: {
30
+ open: boolean;
31
+ onClose: () => void;
32
+ onConfirm: () => void;
33
+ agentLabel: string;
34
+ resourceLabel: string;
35
+ context: string;
36
+ fix?: string;
37
+ /** The agent's one-clause case for this step, repeated at the decision. */
38
+ reason?: string;
39
+ /** The condition the agent attached to this step; shown before the operator confirms. */
40
+ precondition?: string;
41
+ managedBy?: string; // GitOps/Helm owner of the resource, if any
42
+ confidence?: number;
43
+ }) {
44
+ const fixText = fix?.trim();
45
+ const lowConfidence = confidence != null && confidence < 0.5;
46
+ // A GitOps/Helm-managed resource needs an explicit acknowledgment before applying
47
+ // a direct change — it's the canonical footgun (the controller reverts it). Gating
48
+ // (not just warning) makes the user opt into "yes, I know this may be undone."
49
+ // TODO(SKY-1075): once Radar can connect the user's SCM (GitHub/GitLab/…), replace
50
+ // direct apply on managed resources with "open a PR against the Git source"
51
+ // instead — the durable fix. See Linear SKY-1075.
52
+ const [acked, setAcked] = useState(false);
53
+ useEffect(() => {
54
+ if (open) setAcked(false);
55
+ }, [open]);
56
+ const applyBlocked = !!managedBy && !acked;
57
+ return (
58
+ <DialogPortal
59
+ open={open}
60
+ onClose={onClose}
61
+ className="flex max-h-[calc(100dvh-2rem)] w-full max-w-2xl flex-col overflow-hidden"
62
+ >
63
+ <div className="flex shrink-0 items-start gap-3 border-b border-theme-border p-4">
64
+ <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-amber-500/20">
65
+ <AlertTriangle className="h-5 w-5 text-amber-500" />
66
+ </div>
67
+ <div className="min-w-0 flex-1">
68
+ <h3 className="text-lg font-semibold text-theme-text-primary">
69
+ Apply this fix?
70
+ </h3>
71
+ <p className="mt-1 text-sm text-theme-text-secondary">
72
+ Let {agentLabel} apply the recommended change to{" "}
73
+ <span className="font-medium text-theme-text-primary">
74
+ {resourceLabel}
75
+ </span>
76
+ .
77
+ </p>
78
+ <p className="mt-2 text-sm text-theme-text-secondary">
79
+ Cluster:{" "}
80
+ <span className="font-medium text-theme-text-primary">
81
+ {parseContextName(context).clusterName}
82
+ </span>
83
+ </p>
84
+ {context !== parseContextName(context).clusterName && (
85
+ <p className="mt-0.5 break-all font-mono text-xs text-theme-text-tertiary">
86
+ {context}
87
+ </p>
88
+ )}
89
+ </div>
90
+ </div>
91
+
92
+ <div className="min-h-0 overflow-y-auto">
93
+ {fixText && (
94
+ <div className="border-b border-theme-border p-4">
95
+ <div className="mb-1.5 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-accent">
96
+ <Sparkles className="h-3.5 w-3.5" />
97
+ Proposed change
98
+ </div>
99
+ <AIMarkdown className="text-sm text-theme-text-primary [overflow-wrap:anywhere] [&_code]:font-normal [&_p]:my-0 [&_p]:text-theme-text-primary [&_pre]:my-1.5 [&_pre]:whitespace-pre-wrap [&_pre_code]:whitespace-pre-wrap">
100
+ {fixText}
101
+ </AIMarkdown>
102
+ {reason ? (
103
+ <p
104
+ data-apply-reason
105
+ className="mt-2 text-sm text-theme-text-secondary"
106
+ >
107
+ <span className="font-medium text-theme-text-primary">
108
+ Why this step:
109
+ </span>{" "}
110
+ {reason}
111
+ </p>
112
+ ) : null}
113
+ {precondition ? (
114
+ <p
115
+ data-apply-precondition
116
+ className="mt-2 flex items-start gap-1.5 rounded-md border border-amber-500/30 bg-amber-500/5 px-2.5 py-1.5 text-xs text-theme-text-primary"
117
+ >
118
+ <AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500" />
119
+ <span>
120
+ <span className="font-medium">
121
+ The agent said this applies only if:
122
+ </span>{" "}
123
+ {precondition}. Radar has not checked that condition.
124
+ </span>
125
+ </p>
126
+ ) : null}
127
+ </div>
128
+ )}
129
+ </div>
130
+ <div className="shrink-0 space-y-2 p-4">
131
+ {/* The star warning: when we KNOW a controller owns this resource, a live
132
+ change reverts on the next reconcile — say so authoritatively. */}
133
+ {managedBy && (
134
+ <div className="space-y-2 rounded border border-amber-500/40 bg-amber-500/10 p-3 text-sm text-theme-text-primary">
135
+ <div className="flex items-start gap-2">
136
+ <RefreshCw className="mt-0.5 h-4 w-4 shrink-0 text-amber-500" />
137
+ <span>
138
+ <span className="font-medium">Managed by {managedBy}.</span>{" "}
139
+ Unless you turn off auto-sync, a direct change here will be
140
+ undone within minutes when {managedBy} re-syncs from Git — the
141
+ durable fix is to change it in Git (the {managedBy} source).
142
+ </span>
143
+ </div>
144
+ <label className="flex cursor-pointer items-center gap-2 pl-6 text-xs text-theme-text-secondary">
145
+ <input
146
+ type="checkbox"
147
+ checked={acked}
148
+ onChange={(e) => setAcked(e.target.checked)}
149
+ className="h-3.5 w-3.5 accent-amber-500"
150
+ />
151
+ I understand {managedBy} may revert this — apply anyway.
152
+ </label>
153
+ </div>
154
+ )}
155
+ {lowConfidence && (
156
+ <div className="flex items-start gap-2 rounded border border-theme-border bg-theme-elevated p-3 text-sm text-theme-text-secondary">
157
+ <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
158
+ <span>
159
+ The agent had <span className="font-medium">low confidence</span>{" "}
160
+ in this conclusion — consider asking a follow-up to verify before
161
+ applying.
162
+ </span>
163
+ </div>
164
+ )}
165
+ <div className="flex items-start gap-2 rounded border border-theme-border bg-theme-base/50 p-3 text-sm text-theme-text-secondary">
166
+ <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
167
+ <span>
168
+ {agentLabel} will change your cluster using your kubeconfig
169
+ credentials. Review the change above; if you&apos;re not sure, ask a
170
+ follow-up first.
171
+ </span>
172
+ </div>
173
+ </div>
174
+ <div className="flex shrink-0 items-center justify-end gap-3 border-t border-theme-border p-4">
175
+ <button
176
+ onClick={onClose}
177
+ className="rounded-lg px-4 py-2 text-sm font-medium text-theme-text-secondary transition-colors hover:bg-theme-elevated hover:text-theme-text-primary"
178
+ >
179
+ Cancel
180
+ </button>
181
+ <button
182
+ onClick={onConfirm}
183
+ disabled={applyBlocked}
184
+ className="flex items-center gap-1.5 rounded-lg btn-brand px-4 py-2 text-sm font-medium disabled:cursor-not-allowed disabled:opacity-50"
185
+ >
186
+ <Wrench className="h-4 w-4" />
187
+ Apply fix
188
+ </button>
189
+ </div>
190
+ </DialogPortal>
191
+ );
192
+ }
193
+
194
+ // The result of an apply turn. Mutation truth comes from Radar write-tool
195
+ // results, not the agent's prose or process exit. Missing outcome metadata is
196
+ // therefore fail-closed as unknown; only explicit confirmation renders green.
197
+ export function ApplyOutcomeCard({
198
+ diagnosis,
199
+ error,
200
+ applyOutcome,
201
+ onCheckStatus,
202
+ animate,
203
+ }: {
204
+ diagnosis: Diagnosis | null;
205
+ error?: string | null;
206
+ applyOutcome?: ApplyMutationOutcome;
207
+ onCheckStatus?: () => void;
208
+ animate: boolean;
209
+ }) {
210
+ const outcome = diagnosis?.report || diagnosis?.rootCause;
211
+ const status = applyOutcome ?? "unknown";
212
+ const confirmed = status === "confirmed";
213
+ const failed = status === "failed";
214
+ const heading = confirmed
215
+ ? "Applied"
216
+ : failed
217
+ ? "Not applied"
218
+ : "Outcome unknown";
219
+ const detail = error || outcome;
220
+ const fallbackDetail = confirmed
221
+ ? "Radar confirmed that the change was applied. Check the current state to confirm its effect."
222
+ : failed
223
+ ? "Radar could not confirm that the change was applied. Review Activity before retrying."
224
+ : "Radar could not confirm whether the change was applied. Check the current state before retrying.";
225
+ const containerClass = confirmed
226
+ ? "border-emerald-500/30 bg-emerald-500/5"
227
+ : failed
228
+ ? "border-red-500/30 bg-red-500/5"
229
+ : "border-amber-500/40 bg-amber-500/10";
230
+ const accentClass = confirmed
231
+ ? "text-emerald-500"
232
+ : failed
233
+ ? "text-red-400"
234
+ : "text-amber-400";
235
+ const buttonClass = confirmed
236
+ ? "border-emerald-500/40 text-emerald-500 hover:bg-emerald-500/10"
237
+ : "border-amber-500/40 text-amber-400 hover:bg-amber-500/10";
238
+ const provenance = confirmed
239
+ ? error
240
+ ? "Radar confirmed the change — the agent report is incomplete"
241
+ : "Radar confirmed the change was applied"
242
+ : failed
243
+ ? "Radar did not confirm that the change was applied"
244
+ : "Radar cannot confirm whether the change was applied";
245
+ return (
246
+ <div className={`mt-3 space-y-2 ${animate ? "animate-result-in" : ""}`}>
247
+ <div className={`rounded-lg border p-3 ${containerClass}`}>
248
+ <div className="mb-1 flex items-center justify-between gap-2">
249
+ <div
250
+ className={`flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide ${accentClass}`}
251
+ >
252
+ {confirmed ? (
253
+ <CheckCircle2 className="h-3.5 w-3.5" />
254
+ ) : (
255
+ <AlertTriangle className="h-3.5 w-3.5" />
256
+ )}
257
+ {heading}
258
+ </div>
259
+ {detail && <CopyButton text={detail} label="Copy apply result" />}
260
+ </div>
261
+ <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">
262
+ {detail || fallbackDetail}
263
+ </AIMarkdown>
264
+ {!confirmed && !failed && (
265
+ <div className="mt-2 flex items-start gap-1.5 text-xs font-medium text-amber-300">
266
+ <RefreshCw className="mt-0.5 h-3.5 w-3.5 shrink-0" />
267
+ <span>
268
+ Check the current state before trying to apply this change again.
269
+ </span>
270
+ </div>
271
+ )}
272
+ {onCheckStatus && !failed && (
273
+ <button
274
+ type="button"
275
+ onClick={onCheckStatus}
276
+ className={`mt-3 flex w-full items-center justify-center gap-1.5 rounded-lg border py-2 text-sm font-medium ${buttonClass}`}
277
+ >
278
+ <RefreshCw className="h-4 w-4" />
279
+ Check current status
280
+ </button>
281
+ )}
282
+ </div>
283
+ <div className="flex items-center gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
284
+ <ShieldCheck className="h-3 w-3 shrink-0" />
285
+ <span className="truncate">{provenance}</span>
286
+ </div>
287
+ </div>
288
+ );
289
+ }