@skyhook-io/radar-app 1.12.3 → 1.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/App.tsx +37 -8
- package/src/RadarApp.tsx +1 -1
- package/src/api/client.ts +38 -6
- package/src/api/diagnose.ts +45 -5
- package/src/components/cost/ApplicationCostTab.test.ts +45 -0
- package/src/components/cost/ApplicationCostTab.tsx +115 -64
- package/src/components/cost/CostTrendChart.test.ts +65 -0
- package/src/components/cost/CostTrendChart.tsx +107 -17
- package/src/components/cost/CostView.test.ts +36 -1
- package/src/components/cost/CostView.tsx +133 -51
- package/src/components/cost/CurrentAllocationUse.tsx +8 -4
- package/src/components/cost/WorkloadCostTab.test.ts +50 -0
- package/src/components/cost/WorkloadCostTab.tsx +109 -61
- package/src/components/cost/source.test.ts +33 -0
- package/src/components/cost/source.ts +100 -0
- package/src/components/diagnose/AISettings.tsx +9 -4
- package/src/components/diagnose/DiagnoseContext.tsx +216 -48
- package/src/components/diagnose/DiagnoseSurface.test.tsx +118 -5
- package/src/components/diagnose/DiagnoseSurface.tsx +190 -32
- package/src/components/diagnose/Home.tsx +93 -64
- package/src/components/diagnose/InvestigationView.tsx +190 -124
- package/src/components/diagnose/parts.test.tsx +12 -4
- package/src/components/diagnose/parts.tsx +29 -17
- package/src/components/execution/BatchExecutionView.render.test.tsx +35 -0
- package/src/components/execution/BatchExecutionView.tsx +2 -3
- package/src/components/execution/execution-definition.test.ts +19 -0
- package/src/components/execution/execution-definition.ts +2 -0
- package/src/components/gitops/GitOpsView.tsx +25 -3
- package/src/components/home/CostCard.tsx +3 -2
- package/src/components/nav/navigation.test.ts +26 -0
- package/src/components/nav/navigation.ts +10 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +36 -12
- package/src/components/rightsizing/copy.test.ts +19 -0
- package/src/components/settings/SettingsDialog.tsx +666 -103
- package/src/components/settings/settings-state.test.ts +42 -0
- package/src/components/settings/settings-state.ts +39 -0
- package/src/index.css +11 -1
|
@@ -34,6 +34,45 @@ import {
|
|
|
34
34
|
const RECHECK_QUESTION =
|
|
35
35
|
"Did the fix resolve the issue? Re-check the resource's current status and health now, and say whether it's healthy.";
|
|
36
36
|
|
|
37
|
+
export function canStopInvestigation(
|
|
38
|
+
run: RunSummary,
|
|
39
|
+
busy: boolean,
|
|
40
|
+
gone: boolean,
|
|
41
|
+
latestTurnStatus?: Turn["status"],
|
|
42
|
+
): boolean {
|
|
43
|
+
// The transcript is fresher than the polled run summary. Once it has a
|
|
44
|
+
// terminal frame, a lagging/failed summary refresh must not resurrect Stop.
|
|
45
|
+
const transcriptTerminal =
|
|
46
|
+
latestTurnStatus === "done" || latestTurnStatus === "error";
|
|
47
|
+
return (
|
|
48
|
+
run.trigger !== "background" &&
|
|
49
|
+
run.status !== "stale" &&
|
|
50
|
+
run.status !== "stopping" &&
|
|
51
|
+
!gone &&
|
|
52
|
+
!transcriptTerminal &&
|
|
53
|
+
(busy || run.status === "running")
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function canContinueInvestigation(
|
|
58
|
+
run: RunSummary,
|
|
59
|
+
latestTurnStatus?: Turn["status"],
|
|
60
|
+
gone = false,
|
|
61
|
+
): boolean {
|
|
62
|
+
const transcriptTerminal =
|
|
63
|
+
latestTurnStatus === "done" || latestTurnStatus === "error";
|
|
64
|
+
const summaryIsLaggingTerminalTranscript =
|
|
65
|
+
run.status === "running" &&
|
|
66
|
+
run.trigger !== "background" &&
|
|
67
|
+
transcriptTerminal;
|
|
68
|
+
return (
|
|
69
|
+
!gone &&
|
|
70
|
+
run.status !== "stale" &&
|
|
71
|
+
run.status !== "stopping" &&
|
|
72
|
+
(run.canContinue !== false || summaryIsLaggingTerminalTranscript)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
37
76
|
export function InvestigationView({
|
|
38
77
|
run,
|
|
39
78
|
agentLabel,
|
|
@@ -95,6 +134,14 @@ export function InvestigationView({
|
|
|
95
134
|
// unfolds in beats: "rca" = root cause only (+ a "weighing remediation" beat),
|
|
96
135
|
// "full" = everything. null/"full" for replayed turns (no choreography on rebuild).
|
|
97
136
|
const [reveal, setReveal] = useState<"rca" | "full" | null>(null);
|
|
137
|
+
const latestTurnStatus = turns[turns.length - 1]?.status;
|
|
138
|
+
const canContinue = canContinueInvestigation(run, latestTurnStatus, gone);
|
|
139
|
+
// Continuation needs a resumable session, but stopping does not. A brand-new
|
|
140
|
+
// hosted turn can be running before the SDK has reported its session id, so
|
|
141
|
+
// keep Stop available for human investigations without advertising a
|
|
142
|
+
// follow-up composer that the server would reject. Automatic runs remain
|
|
143
|
+
// immutable even while their stream marks this view busy.
|
|
144
|
+
const canStop = canStopInvestigation(run, busy, gone, latestTurnStatus);
|
|
98
145
|
const synthTimers = useRef<ReturnType<typeof setTimeout>[]>([]);
|
|
99
146
|
const clearSynth = () => {
|
|
100
147
|
synthTimers.current.forEach(clearTimeout);
|
|
@@ -216,6 +263,7 @@ export function InvestigationView({
|
|
|
216
263
|
...prev,
|
|
217
264
|
{
|
|
218
265
|
question: ev.question,
|
|
266
|
+
actor: ev.actor,
|
|
219
267
|
timeline: [],
|
|
220
268
|
diagnosis: null,
|
|
221
269
|
error: null,
|
|
@@ -388,7 +436,7 @@ export function InvestigationView({
|
|
|
388
436
|
|
|
389
437
|
const submitFollowup = () => {
|
|
390
438
|
const q = input.trim();
|
|
391
|
-
if (!q || busy ||
|
|
439
|
+
if (!q || busy || !canContinue) return;
|
|
392
440
|
setInput("");
|
|
393
441
|
setActionError(null);
|
|
394
442
|
pinnedRef.current = true; // a user-initiated turn always follows to the bottom
|
|
@@ -401,7 +449,7 @@ export function InvestigationView({
|
|
|
401
449
|
// Ask a canned follow-up (e.g. "explain simply") — a one-tap path that turns the
|
|
402
450
|
// prompt's plain-language instruction into something the user controls.
|
|
403
451
|
const askFollowup = (q: string) => {
|
|
404
|
-
if (busy ||
|
|
452
|
+
if (busy || !canContinue) return;
|
|
405
453
|
setActionError(null);
|
|
406
454
|
pinnedRef.current = true;
|
|
407
455
|
addTurn(run.id, { question: q }).catch((e) =>
|
|
@@ -423,10 +471,13 @@ export function InvestigationView({
|
|
|
423
471
|
autoRecheckRef.current = true; // verify the write automatically once it lands
|
|
424
472
|
addTurn(run.id, { apply: true, fix: pendingFix }).catch((e) => {
|
|
425
473
|
autoRecheckRef.current = false; // the apply never started — don't auto-recheck
|
|
426
|
-
setActionError(
|
|
474
|
+
setActionError(
|
|
475
|
+
e instanceof DiagnoseError ? e.message : "Couldn't apply.",
|
|
476
|
+
);
|
|
427
477
|
});
|
|
428
478
|
};
|
|
429
|
-
const checkStatus = () =>
|
|
479
|
+
const checkStatus = () =>
|
|
480
|
+
addTurn(run.id, { question: RECHECK_QUESTION }).catch(() => {});
|
|
430
481
|
|
|
431
482
|
// Apply tracks the latest turn that produced remediation (so follow-ups don't
|
|
432
483
|
// strip it) and is blocked on a stale (context-switched) run.
|
|
@@ -461,135 +512,140 @@ export function InvestigationView({
|
|
|
461
512
|
return (
|
|
462
513
|
<div className="relative flex min-h-0 flex-1 flex-col">
|
|
463
514
|
<div className="flex min-h-0 flex-1">
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
515
|
+
<div
|
|
516
|
+
ref={scrollRef}
|
|
517
|
+
onScroll={onScroll}
|
|
518
|
+
className="flex-1 overflow-y-auto overflow-x-hidden px-4 py-3 [scrollbar-gutter:stable]"
|
|
519
|
+
>
|
|
520
|
+
<div className={maximized ? "mx-auto max-w-3xl" : ""}>
|
|
521
|
+
<div className="space-y-4">
|
|
522
|
+
{stale && (
|
|
523
|
+
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 p-3 text-xs text-theme-text-secondary">
|
|
524
|
+
<div className="flex items-start gap-2">
|
|
525
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-500" />
|
|
526
|
+
<span>
|
|
527
|
+
This investigation ran against{" "}
|
|
528
|
+
<span className="font-medium text-theme-text-primary">
|
|
529
|
+
{run.context || "a different cluster"}
|
|
530
|
+
</span>
|
|
531
|
+
. The cluster context has changed — it's read-only now.
|
|
479
532
|
</span>
|
|
480
|
-
|
|
481
|
-
|
|
533
|
+
</div>
|
|
534
|
+
<button
|
|
535
|
+
onClick={retryDiagnosis}
|
|
536
|
+
className="mt-2 inline-flex items-center gap-1.5 rounded-md border border-amber-500/50 px-2.5 py-1 font-medium text-amber-600 hover:bg-amber-500/10 dark:text-amber-400"
|
|
537
|
+
>
|
|
538
|
+
<Send className="h-3 w-3" />
|
|
539
|
+
Re-run on current cluster
|
|
540
|
+
</button>
|
|
482
541
|
</div>
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
<
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
542
|
+
)}
|
|
543
|
+
{gone && turns.length === 0 && (
|
|
544
|
+
<div className="rounded-lg border border-theme-border bg-theme-elevated p-3 text-sm text-theme-text-secondary">
|
|
545
|
+
<div className="flex items-start gap-2">
|
|
546
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-500" />
|
|
547
|
+
<span>
|
|
548
|
+
This investigation is unavailable. It may be private, your
|
|
549
|
+
access may have changed, or its history may have been
|
|
550
|
+
cleared. Check your account and organization, or ask the
|
|
551
|
+
creator for access.
|
|
552
|
+
</span>
|
|
553
|
+
</div>
|
|
554
|
+
<button
|
|
555
|
+
onClick={retryDiagnosis}
|
|
556
|
+
className="mt-2 inline-flex items-center gap-1.5 rounded-md border border-theme-border px-2.5 py-1 font-medium text-theme-text-primary hover:bg-theme-hover"
|
|
557
|
+
>
|
|
558
|
+
<Send className="h-3 w-3" />
|
|
559
|
+
Re-run Diagnose
|
|
560
|
+
</button>
|
|
501
561
|
</div>
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
<div className="flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-theme-text-primary">
|
|
541
|
-
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-400" />
|
|
542
|
-
<span>{actionError}</span>
|
|
543
|
-
</div>
|
|
544
|
-
)}
|
|
545
|
-
{/* A start that failed belongs to the investigation that never began,
|
|
562
|
+
)}
|
|
563
|
+
<RunContextCard run={run} />
|
|
564
|
+
{turns.map((t, i) => {
|
|
565
|
+
const isLast = i === turns.length - 1;
|
|
566
|
+
// Hosted runners are read-only — the server refuses apply turns.
|
|
567
|
+
const canApply = i === lastRemediationIdx && !stale && !hosted;
|
|
568
|
+
const canCheck = isLast && t.status === "done" && !!t.apply;
|
|
569
|
+
return (
|
|
570
|
+
<TurnView
|
|
571
|
+
key={i}
|
|
572
|
+
turn={t}
|
|
573
|
+
synthLabel={isLast ? synth : null}
|
|
574
|
+
reveal={isLast ? (reveal ?? "full") : "full"}
|
|
575
|
+
onApply={canApply ? requestApply : undefined}
|
|
576
|
+
onAsk={
|
|
577
|
+
isLast && !busy && canContinue ? askFollowup : undefined
|
|
578
|
+
}
|
|
579
|
+
onCheckStatus={canCheck ? checkStatus : undefined}
|
|
580
|
+
onRetryDiagnosis={
|
|
581
|
+
isLast &&
|
|
582
|
+
t.status === "error" &&
|
|
583
|
+
!t.question &&
|
|
584
|
+
!t.apply &&
|
|
585
|
+
!stale
|
|
586
|
+
? retryDiagnosis
|
|
587
|
+
: undefined
|
|
588
|
+
}
|
|
589
|
+
hideVerdict={pinned && i === pinnedIdx}
|
|
590
|
+
/>
|
|
591
|
+
);
|
|
592
|
+
})}
|
|
593
|
+
{actionError && (
|
|
594
|
+
<div className="flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-theme-text-primary">
|
|
595
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-400" />
|
|
596
|
+
<span>{actionError}</span>
|
|
597
|
+
</div>
|
|
598
|
+
)}
|
|
599
|
+
{/* A start that failed belongs to the investigation that never began,
|
|
546
600
|
not to the one on screen. Unlabelled at the foot of a finished
|
|
547
601
|
transcript — verdict directly above — it reads as "this
|
|
548
602
|
investigation failed". It also outlives the click that caused it,
|
|
549
603
|
and this is the only place it surfaces while a run is focused, so
|
|
550
604
|
it needs a way out. */}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
605
|
+
{startError && (
|
|
606
|
+
<div
|
|
607
|
+
role="alert"
|
|
608
|
+
className="flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-theme-text-primary"
|
|
609
|
+
>
|
|
610
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-400" />
|
|
611
|
+
<div className="min-w-0 flex-1">
|
|
612
|
+
<div className="font-medium">
|
|
613
|
+
Couldn't start a new investigation
|
|
614
|
+
</div>
|
|
615
|
+
<div className="text-theme-text-secondary">
|
|
616
|
+
{startError}
|
|
617
|
+
</div>
|
|
560
618
|
</div>
|
|
561
|
-
<
|
|
619
|
+
<button
|
|
620
|
+
onClick={dismissError}
|
|
621
|
+
className="shrink-0 rounded px-1.5 py-0.5 text-xs text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
622
|
+
>
|
|
623
|
+
Dismiss
|
|
624
|
+
</button>
|
|
562
625
|
</div>
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
className="shrink-0 rounded px-1.5 py-0.5 text-xs text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
566
|
-
>
|
|
567
|
-
Dismiss
|
|
568
|
-
</button>
|
|
569
|
-
</div>
|
|
570
|
-
)}
|
|
626
|
+
)}
|
|
627
|
+
</div>
|
|
571
628
|
</div>
|
|
572
629
|
</div>
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
)}
|
|
630
|
+
{pinned && (
|
|
631
|
+
<aside
|
|
632
|
+
className={`w-[400px] shrink-0 overflow-y-auto border-l border-theme-border px-4 py-3 ${busy ? "opacity-70" : ""}`}
|
|
633
|
+
>
|
|
634
|
+
<div className="mb-1 text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">
|
|
635
|
+
{busy ? "Verdict · revising…" : "Verdict"}
|
|
636
|
+
</div>
|
|
637
|
+
<ResultCard
|
|
638
|
+
diagnosis={turns[pinnedIdx].diagnosis!}
|
|
639
|
+
onApply={
|
|
640
|
+
pinnedIdx === lastRemediationIdx && !stale && !hosted
|
|
641
|
+
? requestApply
|
|
642
|
+
: undefined
|
|
643
|
+
}
|
|
644
|
+
onAsk={!busy && canContinue ? askFollowup : undefined}
|
|
645
|
+
reveal="full"
|
|
646
|
+
/>
|
|
647
|
+
</aside>
|
|
648
|
+
)}
|
|
593
649
|
</div>
|
|
594
650
|
|
|
595
651
|
{showJump && (
|
|
@@ -616,13 +672,23 @@ export function InvestigationView({
|
|
|
616
672
|
<div
|
|
617
673
|
className={`border-t border-theme-border px-3 py-2.5 ${maximized ? "[&>*]:mx-auto [&>*]:max-w-3xl" : ""}`}
|
|
618
674
|
>
|
|
619
|
-
{
|
|
675
|
+
{canStop ? (
|
|
620
676
|
<button
|
|
621
677
|
onClick={stop}
|
|
622
678
|
className="w-full rounded-lg border border-theme-border py-1.5 text-sm text-theme-text-secondary hover:bg-theme-hover"
|
|
623
679
|
>
|
|
624
680
|
Stop
|
|
625
681
|
</button>
|
|
682
|
+
) : run.status === "stopping" ? (
|
|
683
|
+
<div className="rounded-lg border border-theme-border bg-theme-base px-3 py-2 text-xs text-theme-text-secondary">
|
|
684
|
+
Stopping investigation…
|
|
685
|
+
</div>
|
|
686
|
+
) : !canContinue ? (
|
|
687
|
+
<div className="rounded-lg border border-theme-border bg-theme-base px-3 py-2 text-xs text-theme-text-secondary">
|
|
688
|
+
{run.trigger === "background"
|
|
689
|
+
? "Automatic investigation · read-only. Start a new investigation on this resource to continue digging."
|
|
690
|
+
: "This investigation is read-only."}
|
|
691
|
+
</div>
|
|
626
692
|
) : (
|
|
627
693
|
<div className="flex items-end gap-2">
|
|
628
694
|
<textarea
|
|
@@ -635,7 +701,7 @@ export function InvestigationView({
|
|
|
635
701
|
}
|
|
636
702
|
}}
|
|
637
703
|
rows={1}
|
|
638
|
-
disabled={
|
|
704
|
+
disabled={!canContinue || busy}
|
|
639
705
|
placeholder={
|
|
640
706
|
stale
|
|
641
707
|
? "Cluster changed — re-run Diagnose"
|
|
@@ -645,7 +711,7 @@ export function InvestigationView({
|
|
|
645
711
|
/>
|
|
646
712
|
<button
|
|
647
713
|
onClick={submitFollowup}
|
|
648
|
-
disabled={!input.trim() ||
|
|
714
|
+
disabled={!input.trim() || !canContinue || busy}
|
|
649
715
|
className="shrink-0 rounded-lg btn-brand p-2 disabled:opacity-40"
|
|
650
716
|
aria-label="Send follow-up"
|
|
651
717
|
>
|
|
@@ -41,9 +41,12 @@ describe("AgentControls execution profile explanation", () => {
|
|
|
41
41
|
const html = renderAgent("cursor-agent", ["full-local"], "safeguarded");
|
|
42
42
|
expect(html).toContain("must use this agent");
|
|
43
43
|
expect(html).toContain("normal setup");
|
|
44
|
-
expect(html).toContain("
|
|
45
|
-
expect(html).toContain("
|
|
46
|
-
expect(html).toContain("
|
|
44
|
+
expect(html).toContain("--force");
|
|
45
|
+
expect(html).toContain("auto-approves its built-in tools");
|
|
46
|
+
expect(html).toContain("including your global servers");
|
|
47
|
+
expect(html).toContain("does not reliably confine those tools");
|
|
48
|
+
expect(html).not.toContain("still enables the agent CLI");
|
|
49
|
+
expect(html).not.toContain("does not constrain external MCP servers");
|
|
47
50
|
expect(html).not.toContain("always runs this agent with safeguards");
|
|
48
51
|
});
|
|
49
52
|
|
|
@@ -104,7 +107,12 @@ describe("ConsentCard execution profile treatment", () => {
|
|
|
104
107
|
expect(html).toContain("border-amber-500/40");
|
|
105
108
|
expect(html).toContain("text-amber-500");
|
|
106
109
|
expect(html).toContain("Radar cannot constrain");
|
|
107
|
-
expect(html).toContain("
|
|
110
|
+
expect(html).toContain("--force");
|
|
111
|
+
expect(html).toContain("auto-approves its built-in tools");
|
|
112
|
+
expect(html).toContain("including your global servers");
|
|
113
|
+
expect(html).toContain("does not reliably confine those tools");
|
|
114
|
+
expect(html).not.toContain("still enables the agent CLI");
|
|
115
|
+
expect(html).not.toContain("does not constrain external MCP servers");
|
|
108
116
|
expect(html).not.toContain("text-accent");
|
|
109
117
|
});
|
|
110
118
|
|
|
@@ -33,6 +33,9 @@ import {
|
|
|
33
33
|
import { StatusDot } from "@skyhook-io/k8s-ui";
|
|
34
34
|
import { Markdown } from "../ui/Markdown";
|
|
35
35
|
|
|
36
|
+
const CURSOR_FULL_LOCAL_WARNING =
|
|
37
|
+
"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.";
|
|
38
|
+
|
|
36
39
|
// Segmented two-or-more-way selector — shared shape for the agent and execution
|
|
37
40
|
// profile pickers.
|
|
38
41
|
function Segmented<T extends string | boolean>({
|
|
@@ -309,9 +312,11 @@ export function AgentControls({
|
|
|
309
312
|
configured tools and MCP servers. Radar cannot constrain that
|
|
310
313
|
external tooling; it may access local files or the network
|
|
311
314
|
and may be able to change your cluster.{" "}
|
|
312
|
-
{
|
|
313
|
-
?
|
|
314
|
-
:
|
|
315
|
+
{isCursor
|
|
316
|
+
? CURSOR_FULL_LOCAL_WARNING
|
|
317
|
+
: isClaude
|
|
318
|
+
? "Claude uses the permissions from your setup; Radar does not override them."
|
|
319
|
+
: "Radar still enables the agent CLI’s own sandbox, but that sandbox does not constrain external MCP servers."}{" "}
|
|
315
320
|
Choose this only when you need that setup.
|
|
316
321
|
</span>
|
|
317
322
|
</div>
|
|
@@ -336,10 +341,14 @@ export function AgentControls({
|
|
|
336
341
|
Radar must use this agent's normal setup. Radar cannot
|
|
337
342
|
constrain its external tools or MCP servers; they may access
|
|
338
343
|
local files or the network and may be able to change your
|
|
339
|
-
cluster.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
cluster. {isCursor ? (
|
|
345
|
+
CURSOR_FULL_LOCAL_WARNING
|
|
346
|
+
) : (
|
|
347
|
+
<>
|
|
348
|
+
Radar still enables the agent CLI's own sandbox, but that
|
|
349
|
+
sandbox does not constrain external MCP servers.
|
|
350
|
+
</>
|
|
351
|
+
)}
|
|
343
352
|
</span>
|
|
344
353
|
</div>
|
|
345
354
|
)}
|
|
@@ -396,6 +405,7 @@ export function AgentControls({
|
|
|
396
405
|
// or a follow-up, each with its own transcript + result.
|
|
397
406
|
export type Turn = {
|
|
398
407
|
question?: string;
|
|
408
|
+
actor?: string;
|
|
399
409
|
timeline: TimelineItem[];
|
|
400
410
|
diagnosis: Diagnosis | null;
|
|
401
411
|
error: string | null;
|
|
@@ -500,8 +510,15 @@ export function TurnView({
|
|
|
500
510
|
<div className="space-y-2">
|
|
501
511
|
{turn.question && (
|
|
502
512
|
<div className="flex justify-end">
|
|
503
|
-
<div className="max-w-[85%]
|
|
504
|
-
{turn.
|
|
513
|
+
<div className="max-w-[85%]">
|
|
514
|
+
{turn.actor && (
|
|
515
|
+
<div className="mb-0.5 text-right text-[10px] text-theme-text-tertiary">
|
|
516
|
+
{turn.actor}
|
|
517
|
+
</div>
|
|
518
|
+
)}
|
|
519
|
+
<div className="rounded-lg rounded-br-sm bg-accent/10 px-3 py-1.5 text-sm text-theme-text-primary [overflow-wrap:anywhere]">
|
|
520
|
+
{turn.question}
|
|
521
|
+
</div>
|
|
505
522
|
</div>
|
|
506
523
|
</div>
|
|
507
524
|
)}
|
|
@@ -832,7 +849,9 @@ export function ConsentCard({
|
|
|
832
849
|
MCP servers. They may access local files or the network and may
|
|
833
850
|
be able to change your cluster.
|
|
834
851
|
</>,
|
|
835
|
-
agent === "
|
|
852
|
+
agent === "cursor-agent" ? (
|
|
853
|
+
CURSOR_FULL_LOCAL_WARNING
|
|
854
|
+
) : agent === "claude" ? (
|
|
836
855
|
<>
|
|
837
856
|
Claude uses the permissions from your setup; Radar does not
|
|
838
857
|
override them.
|
|
@@ -841,13 +860,6 @@ export function ConsentCard({
|
|
|
841
860
|
<>
|
|
842
861
|
Radar still enables the agent CLI's own sandbox, but that
|
|
843
862
|
sandbox does not constrain external MCP servers.
|
|
844
|
-
{agent === "cursor-agent" && (
|
|
845
|
-
<>
|
|
846
|
-
{" "}
|
|
847
|
-
Cursor always loads your global MCP servers; Radar cannot
|
|
848
|
-
exclude them.
|
|
849
|
-
</>
|
|
850
|
-
)}
|
|
851
863
|
</>
|
|
852
864
|
),
|
|
853
865
|
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { BatchExecutionFullscreen } from './BatchExecutionView'
|
|
4
|
+
|
|
5
|
+
vi.mock('../../api/client', () => ({
|
|
6
|
+
useResource: () => ({}),
|
|
7
|
+
useWorkloadPods: () => ({}),
|
|
8
|
+
useWorkloadRuns: () => ({ data: { runs: [] } }),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
describe.each(['Job', 'CronJob', 'ScaledJob'])('%s cleanup retention display', (kind) => {
|
|
12
|
+
function render(ttlSecondsAfterFinished?: number) {
|
|
13
|
+
const jobSpec = { ttlSecondsAfterFinished, activeDeadlineSeconds: 60 }
|
|
14
|
+
const spec = kind === 'CronJob'
|
|
15
|
+
? { jobTemplate: { spec: jobSpec } }
|
|
16
|
+
: kind === 'ScaledJob'
|
|
17
|
+
? { jobTargetRef: jobSpec }
|
|
18
|
+
: jobSpec
|
|
19
|
+
return renderToStaticMarkup(
|
|
20
|
+
<BatchExecutionFullscreen kind={kind} apiKind={`${kind.toLowerCase()}s`} namespace="default" name="example" resource={{ spec }} />,
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
it.each([0, 300])('shows a configured TTL of %i seconds exactly once alongside the definition', (ttl) => {
|
|
25
|
+
const html = render(ttl)
|
|
26
|
+
expect(html.match(/TTL after finish/g)).toHaveLength(1)
|
|
27
|
+
expect(html).toContain(`>${ttl}s<`)
|
|
28
|
+
expect(html).toContain('>Deadline<')
|
|
29
|
+
expect(html).toContain('>60s<')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('omits TTL when cleanup retention is unconfigured', () => {
|
|
33
|
+
expect(render()).not.toContain('TTL after finish')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -475,9 +475,7 @@ function sourceFacts(kind: string, resource: any, runs: WorkloadRun[]) {
|
|
|
475
475
|
progress: latest?.progress,
|
|
476
476
|
duration: latest ? formatRunDuration(latest) : '',
|
|
477
477
|
work: latest ? workCount(latest) : '',
|
|
478
|
-
facts: [
|
|
479
|
-
['TTL after finish', spec.ttlSecondsAfterFinished != null ? `${spec.ttlSecondsAfterFinished}s` : 'None'],
|
|
480
|
-
],
|
|
478
|
+
facts: [],
|
|
481
479
|
definition,
|
|
482
480
|
}
|
|
483
481
|
}
|
|
@@ -544,6 +542,7 @@ function ExecutionDefinitionDetails({ summary, namespace, compact = false }: { s
|
|
|
544
542
|
<DefinitionFact label="Service account" value={summary.serviceAccount} mono />
|
|
545
543
|
{summary.configMaps.length > 0 && <DefinitionFact label="ConfigMaps" value={summary.configMaps.join(', ')} mono />}
|
|
546
544
|
{summary.secrets.length > 0 && <DefinitionFact label="Secrets" value={summary.secrets.join(', ')} mono />}
|
|
545
|
+
{summary.ttlAfterFinished && <DefinitionFact label="TTL after finish" value={summary.ttlAfterFinished} />}
|
|
547
546
|
</>
|
|
548
547
|
)}
|
|
549
548
|
</div>
|
|
@@ -48,6 +48,25 @@ describe('executionDefinitionSummary', () => {
|
|
|
48
48
|
})
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
+
describe.each(['Job', 'CronJob', 'ScaledJob'])('%s retention', (kind) => {
|
|
52
|
+
function resource(jobSpec: Record<string, unknown>) {
|
|
53
|
+
if (kind === 'CronJob') return { spec: { jobTemplate: { spec: jobSpec } } }
|
|
54
|
+
if (kind === 'ScaledJob') return { spec: { jobTargetRef: jobSpec } }
|
|
55
|
+
return { spec: jobSpec }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
it.each([0, 300])('keeps a configured TTL of %i seconds separate from the execution deadline', (ttl) => {
|
|
59
|
+
expect(executionDefinitionSummary(kind, resource({
|
|
60
|
+
ttlSecondsAfterFinished: ttl,
|
|
61
|
+
activeDeadlineSeconds: 60,
|
|
62
|
+
}))).toMatchObject({ ttlAfterFinished: `${ttl}s`, deadline: '60s' })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('does not invent a TTL when cleanup retention is unconfigured', () => {
|
|
66
|
+
expect(executionDefinitionSummary(kind, resource({}))).not.toHaveProperty('ttlAfterFinished')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
51
70
|
it('explains Argo DAG shape, executable templates, policy, and dependencies', () => {
|
|
52
71
|
const summary = executionDefinitionSummary('WorkflowTemplate', {
|
|
53
72
|
spec: {
|
|
@@ -19,6 +19,7 @@ export interface ExecutionDefinitionSummary {
|
|
|
19
19
|
serviceAccount: string
|
|
20
20
|
retry: string
|
|
21
21
|
deadline?: string
|
|
22
|
+
ttlAfterFinished?: string
|
|
22
23
|
parallelism?: string
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -79,6 +80,7 @@ function kubernetesJobSummary(jobSpec: any): ExecutionDefinitionSummary {
|
|
|
79
80
|
serviceAccount: podSpec.serviceAccountName || 'default',
|
|
80
81
|
retry: `Backoff limit ${jobSpec.backoffLimit ?? 6} · restart ${podSpec.restartPolicy || 'Never'}`,
|
|
81
82
|
...(jobSpec.activeDeadlineSeconds != null ? { deadline: `${jobSpec.activeDeadlineSeconds}s` } : {}),
|
|
83
|
+
...(jobSpec.ttlSecondsAfterFinished != null ? { ttlAfterFinished: `${jobSpec.ttlSecondsAfterFinished}s` } : {}),
|
|
82
84
|
parallelism: `${parallelism} parallel · ${completions} ${completions === 1 ? 'completion' : 'completions'} · ${completionMode}`,
|
|
83
85
|
}
|
|
84
86
|
}
|