@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
@@ -0,0 +1,603 @@
1
+ import { useState } from "react";
2
+ import { AlertTriangle, Check, ShieldCheck, ChevronDown } from "lucide-react";
3
+ import {
4
+ TRANSITION_MENU,
5
+ overlayExitMs,
6
+ overlayTransitionStyle,
7
+ } from "../../utils/animation";
8
+ import { useAnimatedUnmount } from "../../hooks/useAnimatedUnmount";
9
+ import type { DiagnoseConsentCopy } from "../../context/DiagnoseCustomization";
10
+ import type { AgentInfo, ExecutionProfile } from "../../api/diagnose";
11
+
12
+ const CURSOR_FULL_LOCAL_WARNING =
13
+ "Radar passes Cursor --force, which auto-approves its built-in tools and every MCP server it loads, including your global servers. Cursor’s sandbox does not reliably confine those tools to Radar’s temporary workspace.";
14
+
15
+ // Segmented two-or-more-way selector — shared shape for the agent and execution
16
+ // profile pickers.
17
+ export function Segmented<T extends string | boolean>({
18
+ label,
19
+ options,
20
+ value,
21
+ onChange,
22
+ }: {
23
+ label?: string;
24
+ options: { value: T; label: string }[];
25
+ value: T;
26
+ onChange: (v: T) => void;
27
+ }) {
28
+ return (
29
+ <div>
30
+ {label && (
31
+ <div className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">
32
+ {label}
33
+ </div>
34
+ )}
35
+ <div className="flex gap-1 rounded-lg border border-theme-border bg-theme-base p-1">
36
+ {options.map((o) => (
37
+ <button
38
+ key={String(o.value)}
39
+ onClick={() => onChange(o.value)}
40
+ className={`flex-1 rounded-md px-2 py-1.5 text-xs font-medium transition-colors ${
41
+ o.value === value
42
+ ? "selection-strong selection-text selection-ring"
43
+ : "text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary"
44
+ }`}
45
+ >
46
+ {o.label}
47
+ </button>
48
+ ))}
49
+ </div>
50
+ </div>
51
+ );
52
+ }
53
+
54
+ type Option = { value: string; label: string; description?: string };
55
+
56
+ // Claude Code's --model takes version-stable ALIASES that always resolve to the
57
+ // user's installed latest of that tier (per `claude --help`), so this list never
58
+ // rots across model updates. "" = the agent's own default. Descriptions mirror
59
+ // Claude Code's own /model picker so the tradeoff is legible.
60
+ const CLAUDE_MODEL_OPTIONS: Option[] = [
61
+ {
62
+ value: "",
63
+ label: "Default",
64
+ description: "Use Claude Code's configured model",
65
+ },
66
+ {
67
+ value: "opus",
68
+ label: "Opus",
69
+ description: "Most capable — best for complex problems",
70
+ },
71
+ {
72
+ value: "sonnet",
73
+ label: "Sonnet",
74
+ description: "Balanced — efficient for routine work",
75
+ },
76
+ { value: "haiku", label: "Haiku", description: "Fastest — quick checks" },
77
+ ];
78
+
79
+ // Codex has no stable alias set and no way to enumerate models, and slugs change
80
+ // across versions — so we take a free-text override rather than a list that rots.
81
+ const EFFORT_OPTIONS: Option[] = [
82
+ {
83
+ value: "",
84
+ label: "Default",
85
+ description: "Recommended — Radar's default (medium)",
86
+ },
87
+ { value: "low", label: "Low", description: "Fastest, least reasoning" },
88
+ { value: "medium", label: "Medium", description: "Balanced depth" },
89
+ { value: "high", label: "High", description: "Most thorough, slowest" },
90
+ ];
91
+
92
+ function TextField({
93
+ label,
94
+ value,
95
+ placeholder,
96
+ onChange,
97
+ hint,
98
+ }: {
99
+ label: string;
100
+ value: string;
101
+ placeholder?: string;
102
+ onChange: (v: string) => void;
103
+ hint?: string;
104
+ }) {
105
+ return (
106
+ <div>
107
+ <div className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">
108
+ {label}
109
+ </div>
110
+ <input
111
+ type="text"
112
+ value={value}
113
+ placeholder={placeholder}
114
+ onChange={(e) => onChange(e.target.value)}
115
+ className="w-full rounded-md border border-theme-border bg-theme-base px-2 py-1.5 text-xs text-theme-text-primary placeholder:text-theme-text-tertiary"
116
+ />
117
+ {hint && (
118
+ <p className="mt-1 text-[11px] leading-snug text-theme-text-tertiary">
119
+ {hint}
120
+ </p>
121
+ )}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ // SelectMenu is a themed dropdown (button + popover list) matching the app's other
127
+ // custom dropdowns — unlike a native <select> it renders option descriptions and
128
+ // stays on-theme in both light/dark.
129
+ function SelectMenu({
130
+ label,
131
+ value,
132
+ options,
133
+ onChange,
134
+ hint,
135
+ }: {
136
+ label: string;
137
+ value: string;
138
+ options: Option[];
139
+ onChange: (v: string) => void;
140
+ hint?: string;
141
+ }) {
142
+ const [open, setOpen] = useState(false);
143
+ // Presence outlives `open` by the menu exit so the list can fade out; the
144
+ // click-away backdrop only exists while logically open.
145
+ const { shouldRender, isOpen } = useAnimatedUnmount(
146
+ open,
147
+ overlayExitMs("menu"),
148
+ );
149
+ const current = options.find((o) => o.value === value) ?? options[0];
150
+ return (
151
+ <div>
152
+ <div className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">
153
+ {label}
154
+ </div>
155
+ <div className="relative">
156
+ <button
157
+ onClick={() => setOpen((v) => !v)}
158
+ aria-haspopup="listbox"
159
+ aria-expanded={open}
160
+ className="flex w-full items-center justify-between gap-2 rounded-md border border-theme-border bg-theme-base px-2.5 py-1.5 text-left text-xs text-theme-text-primary hover:bg-theme-hover"
161
+ >
162
+ <span className="truncate">{current?.label}</span>
163
+ <ChevronDown className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary" />
164
+ </button>
165
+ {open && (
166
+ <div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
167
+ )}
168
+ {shouldRender && (
169
+ <ul
170
+ role="listbox"
171
+ inert={!open || undefined}
172
+ className={`absolute left-0 right-0 z-20 mt-1 max-h-72 origin-top overflow-y-auto rounded-md border border-theme-border bg-theme-surface py-1 shadow-theme-lg ${TRANSITION_MENU} ${
173
+ isOpen
174
+ ? "opacity-100 translate-y-0 scale-100"
175
+ : "opacity-0 -translate-y-1 scale-[0.97]"
176
+ } ${open ? "" : "pointer-events-none"}`}
177
+ style={overlayTransitionStyle(isOpen, "menu")}
178
+ >
179
+ {options.map((o) => {
180
+ const sel = o.value === value;
181
+ return (
182
+ <li key={o.value}>
183
+ <button
184
+ role="option"
185
+ aria-selected={sel}
186
+ onClick={() => {
187
+ onChange(o.value);
188
+ setOpen(false);
189
+ }}
190
+ className="flex w-full items-start gap-2 px-2.5 py-1.5 text-left hover:bg-theme-hover"
191
+ >
192
+ <Check
193
+ className={`mt-0.5 h-3.5 w-3.5 shrink-0 ${sel ? "text-accent" : "opacity-0"}`}
194
+ />
195
+ <span className="min-w-0">
196
+ <span className="block text-xs font-medium text-theme-text-primary">
197
+ {o.label}
198
+ </span>
199
+ {o.description && (
200
+ <span className="block text-[11px] leading-snug text-theme-text-tertiary">
201
+ {o.description}
202
+ </span>
203
+ )}
204
+ </span>
205
+ </button>
206
+ </li>
207
+ );
208
+ })}
209
+ </ul>
210
+ )}
211
+ </div>
212
+ {hint && (
213
+ <p className="mt-1 text-[11px] leading-snug text-theme-text-tertiary">
214
+ {hint}
215
+ </p>
216
+ )}
217
+ </div>
218
+ );
219
+ }
220
+
221
+ // AgentControls is the full AI investigation config block (agent, execution profile, model,
222
+ // effort) — pure + prop-driven. It lives in Settings, not the investigation panel,
223
+ // since these are set-once preferences rather than per-run knobs.
224
+ export function AgentControls({
225
+ agents,
226
+ selectedAgent,
227
+ onSelectAgent,
228
+ profile,
229
+ onSetProfile,
230
+ model,
231
+ onSetModel,
232
+ effort,
233
+ onSetEffort,
234
+ }: {
235
+ agents: AgentInfo[];
236
+ selectedAgent: string;
237
+ onSelectAgent: (name: string) => void;
238
+ profile: ExecutionProfile;
239
+ onSetProfile: (v: ExecutionProfile) => void;
240
+ model: string;
241
+ onSetModel: (v: string) => void;
242
+ effort: string;
243
+ onSetEffort: (v: string) => void;
244
+ }) {
245
+ const isCodex = selectedAgent === "codex";
246
+ const isClaude = selectedAgent === "claude";
247
+ const isCursor = selectedAgent === "cursor-agent";
248
+ const selectedAgentInfo = agents.find((a) => a.name === selectedAgent);
249
+ const selectedAgentLabel =
250
+ selectedAgentInfo?.label || selectedAgent || "agent";
251
+ const profiles = selectedAgentInfo?.profiles ?? [];
252
+ const shownProfile = profiles.includes(profile)
253
+ ? profile
254
+ : (profiles[0] ?? profile);
255
+ const profileLabels: Record<ExecutionProfile, string> = {
256
+ safeguarded: "Radar safeguards",
257
+ "full-local": `Your ${selectedAgentLabel} setup`,
258
+ };
259
+ return (
260
+ <div className="space-y-3">
261
+ {agents.length >= 2 && (
262
+ <Segmented
263
+ label="Agent"
264
+ value={selectedAgent}
265
+ onChange={onSelectAgent}
266
+ options={agents.map((a) => ({
267
+ value: a.name,
268
+ label: a.label || a.name,
269
+ }))}
270
+ />
271
+ )}
272
+ {profiles.length > 0 && (
273
+ <div>
274
+ {profiles.length > 1 ? (
275
+ <>
276
+ <Segmented<ExecutionProfile>
277
+ label="How Radar runs it"
278
+ value={shownProfile}
279
+ onChange={onSetProfile}
280
+ options={profiles.map((value) => ({
281
+ value,
282
+ label: profileLabels[value],
283
+ }))}
284
+ />
285
+ {shownProfile === "safeguarded" ? (
286
+ <p className="mt-1.5 text-[11px] leading-snug text-theme-text-tertiary">
287
+ {isClaude
288
+ ? "Claude’s built-in tools are disabled, and MCP access is limited to Radar’s read-only investigation tools. Your Claude settings, hooks, and CLAUDE.md instructions still apply and are outside Radar’s control."
289
+ : isCodex
290
+ ? "Radar excludes your Codex configuration and other MCP servers. Codex’s sandboxed shell can still read files on this machine; it cannot write or reach the network."
291
+ : "Radar uses this agent’s safeguarded execution profile. Review the agent’s documented restrictions before continuing."}
292
+ </p>
293
+ ) : (
294
+ <div className="mt-1.5 flex items-start gap-1.5 rounded border border-amber-500/40 bg-amber-500/10 p-2 text-[11px] leading-snug text-theme-text-secondary">
295
+ <AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
296
+ <span>
297
+ Uses your agent&apos;s normal configuration and other
298
+ configured tools and MCP servers. Radar cannot constrain
299
+ that external tooling; it may access local files or the
300
+ network and may be able to change your cluster.{" "}
301
+ {isCursor
302
+ ? CURSOR_FULL_LOCAL_WARNING
303
+ : isClaude
304
+ ? "Claude uses the permissions from your setup; Radar does not override them."
305
+ : "Radar still enables the agent CLI’s own sandbox, but that sandbox does not constrain external MCP servers."}{" "}
306
+ Choose this only when you need that setup.
307
+ </span>
308
+ </div>
309
+ )}
310
+ </>
311
+ ) : shownProfile === "safeguarded" ? (
312
+ <div className="flex items-start gap-1.5 rounded border border-theme-border bg-theme-base p-2 text-[11px] leading-snug text-theme-text-secondary">
313
+ <ShieldCheck className="mt-0.5 h-3 w-3 shrink-0 text-accent" />
314
+ <span>
315
+ Radar always runs this agent with safeguards.
316
+ {isClaude
317
+ ? " Claude’s built-in tools are disabled, and MCP access is limited to Radar’s read-only investigation tools. Your Claude settings, hooks, and CLAUDE.md instructions still apply and are outside Radar’s control."
318
+ : isCodex
319
+ ? " Your Codex configuration and other MCP servers are excluded. Codex’s sandboxed shell can still read files on this machine; it cannot write or reach the network."
320
+ : " Review the agent’s documented restrictions before continuing."}
321
+ </span>
322
+ </div>
323
+ ) : (
324
+ <div className="flex items-start gap-1.5 rounded border border-amber-500/40 bg-amber-500/10 p-2 text-[11px] leading-snug text-theme-text-secondary">
325
+ <AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
326
+ <span>
327
+ Radar must use this agent&apos;s normal setup. Radar cannot
328
+ constrain its external tools or MCP servers; they may access
329
+ local files or the network and may be able to change your
330
+ cluster.{" "}
331
+ {isCursor ? (
332
+ CURSOR_FULL_LOCAL_WARNING
333
+ ) : (
334
+ <>
335
+ Radar still enables the agent CLI&apos;s own sandbox, but
336
+ that sandbox does not constrain external MCP servers.
337
+ </>
338
+ )}
339
+ </span>
340
+ </div>
341
+ )}
342
+ </div>
343
+ )}
344
+ {isClaude ? (
345
+ <SelectMenu
346
+ label="Model"
347
+ value={model}
348
+ options={CLAUDE_MODEL_OPTIONS}
349
+ onChange={onSetModel}
350
+ hint="Aliases always resolve to the latest of that tier."
351
+ />
352
+ ) : isCodex || isCursor ? (
353
+ <TextField
354
+ label="Model"
355
+ value={model}
356
+ placeholder={
357
+ isCursor
358
+ ? "Default (e.g. auto, gpt-5.2, composer-2.5)"
359
+ : "Default (e.g. gpt-5-codex, o3)"
360
+ }
361
+ onChange={onSetModel}
362
+ hint={
363
+ isCursor
364
+ ? "Leave empty for your Cursor default, or enter a model slug Cursor supports."
365
+ : shownProfile === "full-local"
366
+ ? "Your Codex setup uses its configured model; set a slug here to override it."
367
+ : "Leave empty for Codex's default, or enter a model your Codex version supports."
368
+ }
369
+ />
370
+ ) : (
371
+ <TextField
372
+ label="Model"
373
+ value={model}
374
+ placeholder="Default"
375
+ onChange={onSetModel}
376
+ hint="Leave empty for the agent's default, or enter a model identifier it supports."
377
+ />
378
+ )}
379
+ {isCodex && (
380
+ <SelectMenu
381
+ label="Reasoning effort"
382
+ value={effort}
383
+ options={EFFORT_OPTIONS}
384
+ onChange={onSetEffort}
385
+ />
386
+ )}
387
+ </div>
388
+ );
389
+ }
390
+
391
+ // ConsentCardShell owns the card chrome (icon, layout, settings link, Approve/
392
+ // Cancel) so every copy tier — the OSS default, the hosted fallback, and a host
393
+ // override — feeds the same frame instead of duplicating it.
394
+ function ConsentCardShell({
395
+ title,
396
+ body,
397
+ bullets,
398
+ settingsLabel,
399
+ approveLabel = "Approve & investigate",
400
+ warning = false,
401
+ error,
402
+ onOpenSettings,
403
+ onApprove,
404
+ onCancel,
405
+ }: DiagnoseConsentCopy & {
406
+ warning?: boolean;
407
+ // Why the last approval failed, straight from the server. The card stays up on
408
+ // failure so retrying works in place, which means this is the ONLY chance to
409
+ // say why — a host that records consent above the individual refuses the
410
+ // wrong person here, and only its message knows who to ask.
411
+ error?: string | null;
412
+ onOpenSettings?: () => void;
413
+ onApprove: () => void;
414
+ onCancel: () => void;
415
+ }) {
416
+ // `settingsLabel: null` hides the link outright — a host with one fixed agent
417
+ // has nothing behind it. `undefined` keeps the OSS default label.
418
+ const resolvedSettingsLabel =
419
+ settingsLabel === undefined
420
+ ? "Change the agent and how it runs in Settings"
421
+ : settingsLabel;
422
+ return (
423
+ <div
424
+ className={
425
+ warning
426
+ ? "rounded-lg border border-amber-500/40 bg-amber-500/10 p-4"
427
+ : "rounded-lg border border-theme-border bg-theme-elevated p-4"
428
+ }
429
+ >
430
+ <div className="mb-2 flex items-center gap-2">
431
+ {warning ? (
432
+ <AlertTriangle className="h-4 w-4 text-amber-500" />
433
+ ) : (
434
+ <ShieldCheck className="h-4 w-4 text-accent" />
435
+ )}
436
+ <div className="text-sm font-medium text-theme-text-primary">
437
+ {title}
438
+ </div>
439
+ </div>
440
+ <div className="text-sm leading-relaxed text-theme-text-secondary">
441
+ {body}
442
+ </div>
443
+ {bullets && bullets.length > 0 && (
444
+ <ul className="mt-2 space-y-1 text-xs text-theme-text-tertiary">
445
+ {bullets.map((b, i) => (
446
+ <li key={i}>• {b}</li>
447
+ ))}
448
+ </ul>
449
+ )}
450
+ {onOpenSettings && resolvedSettingsLabel && (
451
+ <button
452
+ onClick={onOpenSettings}
453
+ className="mt-3 text-xs text-accent hover:underline"
454
+ >
455
+ {resolvedSettingsLabel}
456
+ </button>
457
+ )}
458
+ {/* Red, not amber: the full-local card is itself amber, so an amber box on
459
+ it reads as another paragraph of body copy. role="alert" because nothing
460
+ else moves on failure — focus stays on Approve, so without it a screen
461
+ reader says nothing and the user re-presses a button that cannot succeed. */}
462
+ {error && (
463
+ <div
464
+ role="alert"
465
+ className="mt-3 flex items-start gap-2 rounded-md border border-red-500/30 bg-red-500/10 p-2 text-xs text-theme-text-primary"
466
+ >
467
+ <AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-red-400" />
468
+ <span>{error}</span>
469
+ </div>
470
+ )}
471
+ <div className="mt-4 flex gap-2">
472
+ <button
473
+ onClick={onCancel}
474
+ className="flex-1 rounded-lg border border-theme-border py-1.5 text-sm text-theme-text-secondary hover:bg-theme-hover"
475
+ >
476
+ Cancel
477
+ </button>
478
+ <button
479
+ onClick={onApprove}
480
+ className="flex-1 rounded-lg btn-brand py-1.5 text-sm"
481
+ >
482
+ {approveLabel}
483
+ </button>
484
+ </div>
485
+ </div>
486
+ );
487
+ }
488
+
489
+ // The first-run consent + trust card. The copy is checkable fact about a data
490
+ // flow — not marketing — and the wrong claim is a lie, not a typo. It resolves
491
+ // in two tiers:
492
+ //
493
+ // 1. `copy` — the embedding host tells its own trust story. Only the host
494
+ // knows where its agent runs, whose key pays, and where transcripts live,
495
+ // so any host that runs the agent somewhere other than the OSS local CLI
496
+ // MUST override rather than let Radar assert the local story over its flow.
497
+ // 2. No `copy` — the OSS bring-your-own-local-CLI default, the only tier where
498
+ // "on your machine / no Radar cloud / your account" actually holds.
499
+ export function ConsentCard({
500
+ agentName,
501
+ agent,
502
+ profile,
503
+ copy,
504
+ error,
505
+ onOpenSettings,
506
+ onApprove,
507
+ onCancel,
508
+ }: {
509
+ agentName: string;
510
+ agent?: string;
511
+ profile: ExecutionProfile;
512
+ copy?: DiagnoseConsentCopy;
513
+ error?: string | null;
514
+ onOpenSettings?: () => void;
515
+ onApprove: () => void;
516
+ onCancel: () => void;
517
+ }) {
518
+ const chrome = { onOpenSettings, onApprove, onCancel, error };
519
+
520
+ // Tier 1: a host (e.g. radar-hub-web) supplied its own copy — use it verbatim.
521
+ if (copy) return <ConsentCardShell {...copy} {...chrome} />;
522
+
523
+ return (
524
+ <ConsentCardShell
525
+ {...chrome}
526
+ warning={profile === "full-local"}
527
+ approveLabel={
528
+ profile === "full-local"
529
+ ? "Continue with my agent setup"
530
+ : "Approve & investigate"
531
+ }
532
+ title={
533
+ profile === "safeguarded"
534
+ ? "Run an AI investigation with Radar safeguards?"
535
+ : `Run using your ${agentName} setup?`
536
+ }
537
+ body={
538
+ <>
539
+ This runs{" "}
540
+ <span className="font-medium text-theme-text-primary">
541
+ your own {agentName}
542
+ </span>{" "}
543
+ on your machine — no Radar cloud, no API key, no account. Radar sends
544
+ this resource&apos;s spec, recent events, and pod logs to it (and on
545
+ to its model provider under your account, not to Radar). Transcripts
546
+ are kept in your local Radar history on this machine until cleared.
547
+ {profile === "safeguarded" && (
548
+ <>
549
+ {" "}
550
+ Radar&apos;s investigation tools can only{" "}
551
+ <span className="font-medium">read</span> your cluster.
552
+ </>
553
+ )}
554
+ </>
555
+ }
556
+ bullets={
557
+ profile === "safeguarded"
558
+ ? [
559
+ agent === "claude" ? (
560
+ <>
561
+ Radar safeguards disable Claude&apos;s built-in tools and
562
+ limit MCP access to Radar&apos;s read-only investigation
563
+ tools. Your Claude settings, hooks, and CLAUDE.md instructions
564
+ still apply and are outside Radar&apos;s control.
565
+ </>
566
+ ) : agent === "codex" ? (
567
+ <>
568
+ Radar safeguards exclude your Codex configuration and other
569
+ MCP servers. Codex&apos;s sandboxed shell can still read files
570
+ on this machine; it cannot write or reach the network.
571
+ </>
572
+ ) : (
573
+ <>
574
+ Radar uses this agent&apos;s safeguarded execution profile.
575
+ Review the agent&apos;s documented restrictions before
576
+ continuing.
577
+ </>
578
+ ),
579
+ ]
580
+ : [
581
+ <>
582
+ Radar cannot constrain the agent&apos;s other configured tools
583
+ or MCP servers. They may access local files or the network and
584
+ may be able to change your cluster.
585
+ </>,
586
+ agent === "cursor-agent" ? (
587
+ CURSOR_FULL_LOCAL_WARNING
588
+ ) : agent === "claude" ? (
589
+ <>
590
+ Claude uses the permissions from your setup; Radar does not
591
+ override them.
592
+ </>
593
+ ) : (
594
+ <>
595
+ Radar still enables the agent CLI&apos;s own sandbox, but that
596
+ sandbox does not constrain external MCP servers.
597
+ </>
598
+ ),
599
+ ]
600
+ }
601
+ />
602
+ );
603
+ }