@skyhook-io/radar-app 1.13.1 → 1.13.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.
- package/package.json +2 -2
- package/src/App.tsx +19 -1
- package/src/RadarApp.tsx +2 -2
- package/src/api/diagnose.test.ts +268 -0
- package/src/api/diagnose.ts +74 -9
- package/src/components/diagnose/AISettings.tsx +1 -1
- package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
- package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
- package/src/components/diagnose/DiagnoseContext.tsx +12 -17
- package/src/components/diagnose/DiagnoseSurface.test.tsx +211 -16
- package/src/components/diagnose/DiagnoseSurface.tsx +464 -133
- package/src/components/diagnose/Home.test.tsx +293 -0
- package/src/components/diagnose/Home.tsx +289 -119
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +2170 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +2253 -0
- package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +257 -0
- package/src/components/diagnose/InvestigationResourceEvidence.tsx +214 -0
- package/src/components/diagnose/InvestigationView.test.ts +17 -0
- package/src/components/diagnose/InvestigationView.tsx +1913 -395
- package/src/components/diagnose/LocalDiagnoseAction.tsx +42 -25
- package/src/components/diagnose/agentCatalog.ts +1 -1
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +151 -0
- package/src/components/diagnose/investigationEvidence.test.ts +3109 -0
- package/src/components/diagnose/investigationEvidence.ts +3492 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +447 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +167 -0
- package/src/components/diagnose/investigationExplanation.test.ts +92 -0
- package/src/components/diagnose/investigationExplanation.ts +29 -0
- package/src/components/diagnose/investigationResourceEvidenceModel.ts +322 -0
- package/src/components/diagnose/investigationSourceFocus.test.ts +143 -0
- package/src/components/diagnose/investigationSourceFocus.ts +98 -0
- package/src/components/diagnose/investigationState.test.ts +695 -0
- package/src/components/diagnose/investigationState.ts +451 -0
- package/src/components/diagnose/parts.test.tsx +864 -3
- package/src/components/diagnose/parts.tsx +1337 -541
- package/src/components/diagnose/target.test.ts +39 -0
- package/src/components/diagnose/target.ts +36 -0
- package/src/components/diagnose/useDisclosureReveal.ts +117 -0
- package/src/components/home/MCPSetupDialog.tsx +2 -2
- package/src/components/home/mcpToolCatalog.test.ts +22 -0
- package/src/components/home/mcpToolCatalog.ts +3 -2
- package/src/components/issues/IssuesPane.tsx +5 -1
- package/src/components/settings/SettingsDialog.tsx +11 -13
- package/src/components/workload/WorkloadView.tsx +1 -1
- package/src/context/DiagnoseCustomization.tsx +11 -8
- package/src/index.css +63 -79
- package/src/index.ts +1 -1
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
// persistence, no app/routing knowledge) so they lift cleanly into k8s-ui later
|
|
3
3
|
// and Cloud can reuse them. The stateful controller lives in DiagnoseContext;
|
|
4
4
|
// the run logic in InvestigationView.
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
useEffect,
|
|
7
|
+
useId,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from "react";
|
|
6
13
|
import {
|
|
7
14
|
Loader2,
|
|
8
15
|
CheckCircle2,
|
|
@@ -10,28 +17,43 @@ import {
|
|
|
10
17
|
Copy,
|
|
11
18
|
Check,
|
|
12
19
|
ShieldCheck,
|
|
13
|
-
ChevronRight,
|
|
14
20
|
ChevronDown,
|
|
15
21
|
Wrench,
|
|
16
22
|
Sparkles,
|
|
17
23
|
RefreshCw,
|
|
18
24
|
Maximize2,
|
|
19
25
|
HelpCircle,
|
|
26
|
+
FileSearch,
|
|
20
27
|
} from "lucide-react";
|
|
21
28
|
import { stringify as toYaml } from "yaml";
|
|
22
29
|
import { codeToHtml } from "shiki";
|
|
23
30
|
import { DialogPortal } from "@skyhook-io/k8s-ui/components/ui/DialogPortal";
|
|
31
|
+
import { parseContextName } from "../../utils/context-name";
|
|
24
32
|
import { useTheme } from "../../context/ThemeContext";
|
|
25
33
|
import { type DiagnoseConsentCopy } from "../../context/DiagnoseCustomization";
|
|
26
34
|
import {
|
|
27
35
|
type Diagnosis,
|
|
28
36
|
type DiagnoseStep,
|
|
29
37
|
type AgentInfo,
|
|
38
|
+
type ApplyMutationOutcome,
|
|
30
39
|
type ExecutionProfile,
|
|
31
|
-
type RunSummary,
|
|
32
40
|
} from "../../api/diagnose";
|
|
33
|
-
import {
|
|
41
|
+
import { Collapse, CollapseChevron } from "@skyhook-io/k8s-ui";
|
|
34
42
|
import { Markdown } from "../ui/Markdown";
|
|
43
|
+
import { Tooltip } from "../ui/Tooltip";
|
|
44
|
+
import type { InvestigationSourceExcerpt } from "./investigationSourceFocus";
|
|
45
|
+
import {
|
|
46
|
+
highlightRelatedEvidence,
|
|
47
|
+
locateSourceExcerpt,
|
|
48
|
+
} from "./investigationSourceFocus";
|
|
49
|
+
import {
|
|
50
|
+
investigationActivitySourceDomId,
|
|
51
|
+
investigationEvidenceSourceId,
|
|
52
|
+
type InvestigationRootCauseEvidenceResolution,
|
|
53
|
+
type InvestigationEvidenceSource,
|
|
54
|
+
} from "./investigationEvidence";
|
|
55
|
+
|
|
56
|
+
import { useDisclosureReveal } from "./useDisclosureReveal";
|
|
35
57
|
|
|
36
58
|
const CURSOR_FULL_LOCAL_WARNING =
|
|
37
59
|
"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.";
|
|
@@ -232,7 +254,7 @@ function SelectMenu({
|
|
|
232
254
|
);
|
|
233
255
|
}
|
|
234
256
|
|
|
235
|
-
// AgentControls is the full AI
|
|
257
|
+
// AgentControls is the full AI investigation config block (agent, execution profile, model,
|
|
236
258
|
// effort) — pure + prop-driven. It lives in Settings, not the investigation panel,
|
|
237
259
|
// since these are set-once preferences rather than per-run knobs.
|
|
238
260
|
export function AgentControls({
|
|
@@ -309,9 +331,9 @@ export function AgentControls({
|
|
|
309
331
|
<AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
|
|
310
332
|
<span>
|
|
311
333
|
Uses your agent's normal configuration and other
|
|
312
|
-
configured tools and MCP servers. Radar cannot constrain
|
|
313
|
-
external tooling; it may access local files or the
|
|
314
|
-
and may be able to change your cluster.{" "}
|
|
334
|
+
configured tools and MCP servers. Radar cannot constrain
|
|
335
|
+
that external tooling; it may access local files or the
|
|
336
|
+
network and may be able to change your cluster.{" "}
|
|
315
337
|
{isCursor
|
|
316
338
|
? CURSOR_FULL_LOCAL_WARNING
|
|
317
339
|
: isClaude
|
|
@@ -341,12 +363,13 @@ export function AgentControls({
|
|
|
341
363
|
Radar must use this agent's normal setup. Radar cannot
|
|
342
364
|
constrain its external tools or MCP servers; they may access
|
|
343
365
|
local files or the network and may be able to change your
|
|
344
|
-
cluster.
|
|
366
|
+
cluster.{" "}
|
|
367
|
+
{isCursor ? (
|
|
345
368
|
CURSOR_FULL_LOCAL_WARNING
|
|
346
369
|
) : (
|
|
347
370
|
<>
|
|
348
|
-
Radar still enables the agent CLI's own sandbox, but
|
|
349
|
-
sandbox does not constrain external MCP servers.
|
|
371
|
+
Radar still enables the agent CLI's own sandbox, but
|
|
372
|
+
that sandbox does not constrain external MCP servers.
|
|
350
373
|
</>
|
|
351
374
|
)}
|
|
352
375
|
</span>
|
|
@@ -404,6 +427,8 @@ export function AgentControls({
|
|
|
404
427
|
// Turn is one round of the conversation: the initial investigation (no question)
|
|
405
428
|
// or a follow-up, each with its own transcript + result.
|
|
406
429
|
export type Turn = {
|
|
430
|
+
resultSequence?: number;
|
|
431
|
+
explainAssessment?: number;
|
|
407
432
|
question?: string;
|
|
408
433
|
actor?: string;
|
|
409
434
|
timeline: TimelineItem[];
|
|
@@ -413,11 +438,21 @@ export type Turn = {
|
|
|
413
438
|
// apply turns execute the recommended fix (write tools) — they report an
|
|
414
439
|
// outcome, not a root cause, so the UI frames them differently.
|
|
415
440
|
apply?: boolean;
|
|
441
|
+
// Set from the apply turn's terminal stream event. Green success is reserved
|
|
442
|
+
// for an explicit producer-confirmed mutation.
|
|
443
|
+
applyOutcome?: ApplyMutationOutcome;
|
|
444
|
+
// Verification turns are structurally a fresh health assessment, even though
|
|
445
|
+
// they carry a question. Keeping the bit explicit prevents the UI from
|
|
446
|
+
// misclassifying them as ordinary conversational follow-ups on replay.
|
|
447
|
+
verify?: boolean;
|
|
448
|
+
// Set from the replay/live boundary when the terminal event arrives. Historical
|
|
449
|
+
// conclusions render immediately; conclusions observed live enter smoothly.
|
|
450
|
+
animateResult?: boolean;
|
|
416
451
|
};
|
|
417
452
|
|
|
418
453
|
// TimelineItem is one ordered transcript entry: agent reasoning, or a tool call.
|
|
419
|
-
type TimelineItem =
|
|
420
|
-
| { kind: "thinking"; text: string }
|
|
454
|
+
export type TimelineItem =
|
|
455
|
+
| { kind: "thinking"; text: string; animate?: boolean }
|
|
421
456
|
| {
|
|
422
457
|
kind: "tool";
|
|
423
458
|
id: string;
|
|
@@ -426,25 +461,66 @@ type TimelineItem =
|
|
|
426
461
|
ms?: number;
|
|
427
462
|
summary?: string;
|
|
428
463
|
result?: string;
|
|
464
|
+
evidenceRef?: string;
|
|
465
|
+
radarEvidence?: boolean;
|
|
429
466
|
truncated?: boolean;
|
|
467
|
+
// Tri-state by design: false = producer confirmed success, true = producer
|
|
468
|
+
// confirmed failure, undefined = this replay cannot establish the outcome.
|
|
469
|
+
isError?: boolean;
|
|
470
|
+
// Arrival motion is event-local so a replayed running turn can keep receiving
|
|
471
|
+
// live tool calls without reanimating the history reconstructed before it.
|
|
472
|
+
animate?: boolean;
|
|
430
473
|
};
|
|
431
474
|
|
|
432
475
|
export function appendThinking(
|
|
433
476
|
prev: TimelineItem[],
|
|
434
477
|
text: string,
|
|
478
|
+
animate = true,
|
|
435
479
|
): TimelineItem[] {
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
480
|
+
const normalized = text.replace(/\*\*\r?\n(?=\*\*)/g, "**\n\n");
|
|
481
|
+
// Agent CLIs commonly emit each short bold planning update as a complete
|
|
482
|
+
// stream event. Preserve those as discrete chronological beats instead of
|
|
483
|
+
// producing invalid/run-on Markdown such as `**one****two**`. Ordinary token
|
|
484
|
+
// chunks still concatenate into their current beat exactly as emitted.
|
|
485
|
+
const blocks = normalized.split(/\n{2,}(?=\s*\*\*)/);
|
|
486
|
+
const next = [...prev];
|
|
487
|
+
for (const block of blocks) {
|
|
488
|
+
if (!block) continue;
|
|
489
|
+
const last = next[next.length - 1];
|
|
490
|
+
const beginsBeat = /^\s*\*\*/.test(block);
|
|
491
|
+
const priorBeatComplete =
|
|
492
|
+
last?.kind === "thinking" && /\*\*\s*$/.test(last.text);
|
|
493
|
+
// Some agents repeat a bold phase heading at stream boundaries. Suppress
|
|
494
|
+
// only that presentation artifact; identical ordinary lines can be real
|
|
495
|
+
// evidence/reasoning and must survive both replay and live chunking.
|
|
496
|
+
if (
|
|
497
|
+
last?.kind === "thinking" &&
|
|
498
|
+
beginsBeat &&
|
|
499
|
+
priorBeatComplete &&
|
|
500
|
+
last.text.trim() === block.trim()
|
|
501
|
+
)
|
|
502
|
+
continue;
|
|
503
|
+
if (last?.kind === "thinking" && !(beginsBeat && priorBeatComplete)) {
|
|
504
|
+
next[next.length - 1] = {
|
|
505
|
+
...last,
|
|
506
|
+
text: (last.text + block).slice(-4000),
|
|
507
|
+
animate: last.animate === true || animate,
|
|
508
|
+
};
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
next.push({
|
|
512
|
+
kind: "thinking",
|
|
513
|
+
text: block.trimStart(),
|
|
514
|
+
animate,
|
|
515
|
+
});
|
|
441
516
|
}
|
|
442
|
-
return
|
|
517
|
+
return next;
|
|
443
518
|
}
|
|
444
519
|
|
|
445
520
|
export function upsertTool(
|
|
446
521
|
prev: TimelineItem[],
|
|
447
522
|
step: DiagnoseStep,
|
|
523
|
+
animate = true,
|
|
448
524
|
): TimelineItem[] {
|
|
449
525
|
const i = prev.findIndex((it) => it.kind === "tool" && it.id === step.id);
|
|
450
526
|
if (i >= 0) {
|
|
@@ -457,201 +533,171 @@ export function upsertTool(
|
|
|
457
533
|
kind: "tool",
|
|
458
534
|
tool: step.tool || cur.tool,
|
|
459
535
|
summary: step.summary || cur.summary,
|
|
536
|
+
animate: cur.animate === true || animate,
|
|
460
537
|
};
|
|
461
538
|
return next;
|
|
462
539
|
}
|
|
463
|
-
return [...prev, { kind: "tool", ...step }];
|
|
540
|
+
return [...prev, { kind: "tool", ...step, animate }];
|
|
464
541
|
}
|
|
465
542
|
|
|
466
543
|
export function TurnView({
|
|
467
544
|
turn,
|
|
468
|
-
synthLabel,
|
|
469
|
-
reveal = "full",
|
|
470
545
|
onApply,
|
|
471
|
-
|
|
546
|
+
onViewExplanation,
|
|
472
547
|
onCheckStatus,
|
|
473
548
|
onRetryDiagnosis,
|
|
474
|
-
|
|
549
|
+
hideConclusion = false,
|
|
550
|
+
turnIndex,
|
|
551
|
+
evidenceStepIds,
|
|
552
|
+
onViewEvidence,
|
|
553
|
+
sourceRevealRequest,
|
|
475
554
|
}: {
|
|
476
555
|
turn: Turn;
|
|
477
|
-
synthLabel?: string | null;
|
|
478
|
-
reveal?: "rca" | "full";
|
|
479
556
|
onApply?: (fix: string) => void;
|
|
480
|
-
|
|
557
|
+
onViewExplanation?: () => void;
|
|
481
558
|
onCheckStatus?: () => void;
|
|
482
559
|
onRetryDiagnosis?: () => void;
|
|
483
|
-
// In the maximized workspace the pinned turn's
|
|
560
|
+
// In the maximized workspace the pinned turn's conclusion renders in the side rail,
|
|
484
561
|
// so the transcript suppresses its own copy (reasoning + tool calls still show).
|
|
485
|
-
|
|
562
|
+
hideConclusion?: boolean;
|
|
563
|
+
turnIndex?: number;
|
|
564
|
+
evidenceStepIds?: ReadonlySet<string>;
|
|
565
|
+
onViewEvidence?: (sourceId: string) => void;
|
|
566
|
+
sourceRevealRequest?: {
|
|
567
|
+
sourceId: string;
|
|
568
|
+
requestId: number;
|
|
569
|
+
excerpt?: InvestigationSourceExcerpt;
|
|
570
|
+
};
|
|
486
571
|
}) {
|
|
487
572
|
// A follow-up (a turn the user asked a question on) is a conversational reply,
|
|
488
573
|
// not a fresh diagnosis — render it as a plain answer, never the root-cause
|
|
489
574
|
// anchor or a remediation card.
|
|
490
|
-
const followup = !!turn.question && !turn.apply;
|
|
575
|
+
const followup = !!turn.question && !turn.apply && !turn.verify;
|
|
491
576
|
// Whether the done turn has anything for ResultCard to render — mirrors its
|
|
492
577
|
// branch order exactly (apply → followup → structured/healthy), since a followup
|
|
493
578
|
// ONLY ever renders FollowupAnswer (report/rootCause), never the remediation list.
|
|
494
579
|
// When false, TurnView shows the narration or an explicit empty note, not a blank.
|
|
495
580
|
const dx = turn.diagnosis;
|
|
496
|
-
const
|
|
497
|
-
?
|
|
498
|
-
?
|
|
581
|
+
const hasResult = dx
|
|
582
|
+
? followup
|
|
583
|
+
? !!(dx.report?.trim() || dx.rootCause?.trim()) // FollowupAnswer
|
|
499
584
|
: dx.healthy && !dx.rootCause
|
|
500
|
-
? true // AllClearCard
|
|
585
|
+
? true // AllClearCard
|
|
501
586
|
: dx.inconclusive && !dx.rootCause
|
|
502
587
|
? true // InconclusiveCard
|
|
503
|
-
:
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
(dx.remediation?.length ?? 0) > 0 ||
|
|
507
|
-
!!dx.report?.trim()
|
|
588
|
+
: !!dx.rootCause ||
|
|
589
|
+
(dx.remediation?.length ?? 0) > 0 ||
|
|
590
|
+
!!dx.report?.trim()
|
|
508
591
|
: false;
|
|
509
592
|
return (
|
|
510
593
|
<div className="space-y-2">
|
|
511
|
-
{turn.
|
|
512
|
-
<
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
594
|
+
{turn.explainAssessment ? (
|
|
595
|
+
<button
|
|
596
|
+
type="button"
|
|
597
|
+
onClick={onViewExplanation}
|
|
598
|
+
className="flex items-center gap-1.5 rounded-md py-2 text-xs text-accent-text hover:underline"
|
|
599
|
+
>
|
|
600
|
+
<HelpCircle className="h-3.5 w-3.5" />
|
|
601
|
+
{turn.status === "running"
|
|
602
|
+
? "Explaining assessment…"
|
|
603
|
+
: turn.status === "error"
|
|
604
|
+
? "Explanation failed"
|
|
605
|
+
: "Plain-language explanation"}
|
|
606
|
+
<span className="text-theme-text-tertiary">· View in Findings</span>
|
|
607
|
+
</button>
|
|
608
|
+
) : (
|
|
609
|
+
turn.question &&
|
|
610
|
+
(turn.verify ? (
|
|
611
|
+
<div className="flex items-center gap-2 rounded-md border border-theme-border/60 bg-theme-base/40 px-2.5 py-2 text-xs text-theme-text-secondary">
|
|
612
|
+
<RefreshCw className="h-3.5 w-3.5 shrink-0 text-accent" />
|
|
613
|
+
<span className="font-medium text-theme-text-primary">
|
|
614
|
+
Automatic verification
|
|
615
|
+
</span>
|
|
616
|
+
<span className="text-theme-text-tertiary">
|
|
617
|
+
Re-checking after apply
|
|
618
|
+
</span>
|
|
619
|
+
</div>
|
|
620
|
+
) : (
|
|
621
|
+
<div className="flex justify-end">
|
|
622
|
+
<div className="max-w-[85%]">
|
|
623
|
+
{turn.actor && (
|
|
624
|
+
<div className="mb-0.5 text-right text-[10px] text-theme-text-tertiary">
|
|
625
|
+
{turn.actor}
|
|
626
|
+
</div>
|
|
627
|
+
)}
|
|
628
|
+
<div className="rounded-lg rounded-br-sm bg-accent/10 px-3 py-1.5 text-sm text-theme-text-primary [overflow-wrap:anywhere]">
|
|
629
|
+
{turn.question}
|
|
517
630
|
</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
631
|
</div>
|
|
522
632
|
</div>
|
|
523
|
-
|
|
633
|
+
))
|
|
524
634
|
)}
|
|
525
635
|
<Timeline
|
|
526
636
|
items={turn.timeline}
|
|
527
637
|
running={turn.status === "running"}
|
|
528
638
|
applyMode={turn.apply}
|
|
529
639
|
followup={followup}
|
|
530
|
-
|
|
640
|
+
turnIndex={turnIndex}
|
|
641
|
+
evidenceStepIds={evidenceStepIds}
|
|
642
|
+
onViewEvidence={onViewEvidence}
|
|
643
|
+
sourceRevealRequest={sourceRevealRequest}
|
|
531
644
|
/>
|
|
532
|
-
{turn.
|
|
533
|
-
|
|
645
|
+
{!turn.explainAssessment &&
|
|
646
|
+
turn.status === "done" &&
|
|
647
|
+
(turn.apply ? (
|
|
648
|
+
<ApplyOutcomeCard
|
|
649
|
+
diagnosis={turn.diagnosis}
|
|
650
|
+
applyOutcome={turn.applyOutcome}
|
|
651
|
+
onCheckStatus={onCheckStatus}
|
|
652
|
+
animate={turn.animateResult !== false}
|
|
653
|
+
/>
|
|
654
|
+
) : hideConclusion && hasResult ? null : hasResult ? (
|
|
534
655
|
<ResultCard
|
|
535
656
|
diagnosis={turn.diagnosis!}
|
|
536
657
|
onApply={onApply}
|
|
537
|
-
onAsk={onAsk}
|
|
538
|
-
apply={turn.apply}
|
|
539
658
|
followup={followup}
|
|
540
|
-
reveal={reveal}
|
|
541
659
|
onCheckStatus={onCheckStatus}
|
|
660
|
+
animate={turn.animateResult !== false}
|
|
542
661
|
/>
|
|
543
662
|
) : (
|
|
544
|
-
<EmptyResult />
|
|
663
|
+
<EmptyResult animate={turn.animateResult !== false} />
|
|
545
664
|
))}
|
|
546
|
-
{turn.status === "error" && turn.
|
|
665
|
+
{turn.explainAssessment ? null : turn.status === "error" && turn.apply ? (
|
|
666
|
+
<ApplyOutcomeCard
|
|
667
|
+
diagnosis={turn.diagnosis}
|
|
668
|
+
error={turn.error}
|
|
669
|
+
applyOutcome={turn.applyOutcome}
|
|
670
|
+
onCheckStatus={onCheckStatus}
|
|
671
|
+
animate={turn.animateResult !== false}
|
|
672
|
+
/>
|
|
673
|
+
) : turn.status === "error" && turn.error ? (
|
|
547
674
|
<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">
|
|
548
675
|
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-400" />
|
|
549
676
|
<div className="flex min-w-0 flex-col gap-2">
|
|
550
|
-
<span className="whitespace-pre-wrap break-words">
|
|
677
|
+
<span className="whitespace-pre-wrap break-words">
|
|
678
|
+
{turn.error}
|
|
679
|
+
</span>
|
|
551
680
|
{onRetryDiagnosis && (
|
|
552
681
|
<button
|
|
553
682
|
type="button"
|
|
554
683
|
onClick={onRetryDiagnosis}
|
|
555
684
|
className="btn-brand self-start px-3 py-1 text-xs"
|
|
556
685
|
>
|
|
557
|
-
Retry
|
|
686
|
+
Retry investigation
|
|
687
|
+
</button>
|
|
688
|
+
)}
|
|
689
|
+
{onCheckStatus && (
|
|
690
|
+
<button
|
|
691
|
+
type="button"
|
|
692
|
+
onClick={onCheckStatus}
|
|
693
|
+
className="btn-brand self-start px-3 py-1 text-xs"
|
|
694
|
+
>
|
|
695
|
+
Check current status
|
|
558
696
|
</button>
|
|
559
697
|
)}
|
|
560
698
|
</div>
|
|
561
699
|
</div>
|
|
562
|
-
)}
|
|
563
|
-
</div>
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// RunContextCard opens every investigation with what RADAR already knows — the
|
|
568
|
-
// health frame the server captured at run start. It renders instantly (no agent
|
|
569
|
-
// round-trip), so the agent's boot time reads as "context, then deepening"
|
|
570
|
-
// instead of dead air — and it anchors the verdict against Radar's own signal.
|
|
571
|
-
function healthLineTone(severity?: string): "unhealthy" | "degraded" | "alert" {
|
|
572
|
-
if (severity === "critical") return "unhealthy";
|
|
573
|
-
if (severity === "warning") return "degraded";
|
|
574
|
-
return "alert";
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
export function RunContextCard({ run }: { run: RunSummary }) {
|
|
578
|
-
const h = run.health;
|
|
579
|
-
const issueCount = h?.issueCount ?? 0;
|
|
580
|
-
const issues = h?.issues ?? [];
|
|
581
|
-
const findings = h?.auditFindings ?? [];
|
|
582
|
-
const lines: ReactNode[] = [];
|
|
583
|
-
if (issues.length > 0) {
|
|
584
|
-
// The actual issue rows Radar's engine flagged — the reason bolded, the
|
|
585
|
-
// engine's own detail sentence after it. Concrete beats a count.
|
|
586
|
-
for (const [i, line] of issues.entries()) {
|
|
587
|
-
lines.push(
|
|
588
|
-
<div key={`issue-${i}`} className="flex items-start gap-1.5">
|
|
589
|
-
<StatusDot
|
|
590
|
-
tone={healthLineTone(line.severity)}
|
|
591
|
-
className="mt-1 shrink-0"
|
|
592
|
-
/>
|
|
593
|
-
<span className="min-w-0">
|
|
594
|
-
<span className="font-medium text-theme-text-primary">
|
|
595
|
-
{line.reason}
|
|
596
|
-
</span>
|
|
597
|
-
{line.message ? <> — {line.message}</> : null}
|
|
598
|
-
</span>
|
|
599
|
-
</div>,
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
if (issueCount > issues.length) {
|
|
603
|
-
lines.push(
|
|
604
|
-
<div key="more" className="pl-3.5 text-theme-text-tertiary">
|
|
605
|
-
+{issueCount - issues.length} more active issue
|
|
606
|
-
{issueCount - issues.length === 1 ? "" : "s"}
|
|
607
|
-
</div>,
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
} else if (h?.health === "healthy") {
|
|
611
|
-
lines.push(
|
|
612
|
-
<div key="healthy" className="flex items-center gap-1.5">
|
|
613
|
-
<StatusDot tone="healthy" className="shrink-0" />
|
|
614
|
-
Reported healthy — 0 active issues
|
|
615
|
-
</div>,
|
|
616
|
-
);
|
|
617
|
-
} else if (h) {
|
|
618
|
-
lines.push(
|
|
619
|
-
<div key="none" className="flex items-center gap-1.5">
|
|
620
|
-
<StatusDot tone="unknown" className="shrink-0" />0 active issues
|
|
621
|
-
{h.health ? ` — status ${h.health}` : ""}
|
|
622
|
-
</div>,
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
for (const [i, f] of findings.entries()) {
|
|
626
|
-
lines.push(
|
|
627
|
-
<div key={`audit-${i}`} className="pl-3.5 text-theme-text-tertiary">
|
|
628
|
-
Audit: <span className="font-medium">{f.reason}</span>
|
|
629
|
-
{f.message ? <> — {f.message}</> : null}
|
|
630
|
-
</div>,
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
if ((h?.auditCount ?? 0) > findings.length && findings.length > 0) {
|
|
634
|
-
lines.push(
|
|
635
|
-
<div key="audit-more" className="pl-3.5 text-theme-text-tertiary">
|
|
636
|
-
+{h!.auditCount! - findings.length} more audit finding
|
|
637
|
-
{h!.auditCount! - findings.length === 1 ? "" : "s"}
|
|
638
|
-
</div>,
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
if (run.managedBy) {
|
|
642
|
-
lines.push(
|
|
643
|
-
<div key="managed" className="pl-3.5 text-theme-text-tertiary">
|
|
644
|
-
Managed by {run.managedBy}
|
|
645
|
-
</div>,
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
if (lines.length === 0) return null;
|
|
649
|
-
return (
|
|
650
|
-
<div className="rounded-md border border-theme-border/60 bg-theme-base/40 px-2.5 py-2">
|
|
651
|
-
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
652
|
-
Radar's read at start
|
|
653
|
-
</div>
|
|
654
|
-
<div className="space-y-1 text-xs text-theme-text-secondary">{lines}</div>
|
|
700
|
+
) : null}
|
|
655
701
|
</div>
|
|
656
702
|
);
|
|
657
703
|
}
|
|
@@ -705,7 +751,9 @@ function ConsentCardShell({
|
|
|
705
751
|
{title}
|
|
706
752
|
</div>
|
|
707
753
|
</div>
|
|
708
|
-
<div className="text-sm leading-relaxed text-theme-text-secondary">
|
|
754
|
+
<div className="text-sm leading-relaxed text-theme-text-secondary">
|
|
755
|
+
{body}
|
|
756
|
+
</div>
|
|
709
757
|
{bullets && bullets.length > 0 && (
|
|
710
758
|
<ul className="mt-2 space-y-1 text-xs text-theme-text-tertiary">
|
|
711
759
|
{bullets.map((b, i) => (
|
|
@@ -824,16 +872,16 @@ export function ConsentCard({
|
|
|
824
872
|
? [
|
|
825
873
|
agent === "claude" ? (
|
|
826
874
|
<>
|
|
827
|
-
Radar safeguards disable Claude's built-in tools and
|
|
828
|
-
MCP access to Radar's read-only investigation
|
|
829
|
-
Claude settings, hooks, and CLAUDE.md instructions
|
|
830
|
-
and are outside Radar's control.
|
|
875
|
+
Radar safeguards disable Claude's built-in tools and
|
|
876
|
+
limit MCP access to Radar's read-only investigation
|
|
877
|
+
tools. Your Claude settings, hooks, and CLAUDE.md instructions
|
|
878
|
+
still apply and are outside Radar's control.
|
|
831
879
|
</>
|
|
832
880
|
) : agent === "codex" ? (
|
|
833
881
|
<>
|
|
834
|
-
Radar safeguards exclude your Codex configuration and other
|
|
835
|
-
servers. Codex's sandboxed shell can still read files
|
|
836
|
-
this machine; it cannot write or reach the network.
|
|
882
|
+
Radar safeguards exclude your Codex configuration and other
|
|
883
|
+
MCP servers. Codex's sandboxed shell can still read files
|
|
884
|
+
on this machine; it cannot write or reach the network.
|
|
837
885
|
</>
|
|
838
886
|
) : (
|
|
839
887
|
<>
|
|
@@ -845,9 +893,9 @@ export function ConsentCard({
|
|
|
845
893
|
]
|
|
846
894
|
: [
|
|
847
895
|
<>
|
|
848
|
-
Radar cannot constrain the agent's other configured tools
|
|
849
|
-
MCP servers. They may access local files or the network and
|
|
850
|
-
be able to change your cluster.
|
|
896
|
+
Radar cannot constrain the agent's other configured tools
|
|
897
|
+
or MCP servers. They may access local files or the network and
|
|
898
|
+
may be able to change your cluster.
|
|
851
899
|
</>,
|
|
852
900
|
agent === "cursor-agent" ? (
|
|
853
901
|
CURSOR_FULL_LOCAL_WARNING
|
|
@@ -876,6 +924,7 @@ export function ApplyDialog({
|
|
|
876
924
|
onConfirm,
|
|
877
925
|
agentLabel,
|
|
878
926
|
resourceLabel,
|
|
927
|
+
context,
|
|
879
928
|
fix,
|
|
880
929
|
managedBy,
|
|
881
930
|
confidence,
|
|
@@ -885,6 +934,7 @@ export function ApplyDialog({
|
|
|
885
934
|
onConfirm: () => void;
|
|
886
935
|
agentLabel: string;
|
|
887
936
|
resourceLabel: string;
|
|
937
|
+
context: string;
|
|
888
938
|
fix?: string;
|
|
889
939
|
managedBy?: string; // GitOps/Helm owner of the resource, if any
|
|
890
940
|
confidence?: number;
|
|
@@ -903,8 +953,12 @@ export function ApplyDialog({
|
|
|
903
953
|
}, [open]);
|
|
904
954
|
const applyBlocked = !!managedBy && !acked;
|
|
905
955
|
return (
|
|
906
|
-
<DialogPortal
|
|
907
|
-
|
|
956
|
+
<DialogPortal
|
|
957
|
+
open={open}
|
|
958
|
+
onClose={onClose}
|
|
959
|
+
className="flex max-h-[calc(100dvh-2rem)] w-full max-w-2xl flex-col overflow-hidden"
|
|
960
|
+
>
|
|
961
|
+
<div className="flex shrink-0 items-start gap-3 border-b border-theme-border p-4">
|
|
908
962
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-amber-500/20">
|
|
909
963
|
<AlertTriangle className="h-5 w-5 text-amber-500" />
|
|
910
964
|
</div>
|
|
@@ -919,22 +973,34 @@ export function ApplyDialog({
|
|
|
919
973
|
</span>
|
|
920
974
|
.
|
|
921
975
|
</p>
|
|
976
|
+
<p className="mt-2 text-sm text-theme-text-secondary">
|
|
977
|
+
Cluster:{" "}
|
|
978
|
+
<span className="font-medium text-theme-text-primary">
|
|
979
|
+
{parseContextName(context).clusterName}
|
|
980
|
+
</span>
|
|
981
|
+
</p>
|
|
982
|
+
{context !== parseContextName(context).clusterName && (
|
|
983
|
+
<p className="mt-0.5 break-all font-mono text-xs text-theme-text-tertiary">
|
|
984
|
+
{context}
|
|
985
|
+
</p>
|
|
986
|
+
)}
|
|
922
987
|
</div>
|
|
923
988
|
</div>
|
|
924
989
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
<div className="
|
|
928
|
-
<
|
|
929
|
-
|
|
990
|
+
<div className="min-h-0 overflow-y-auto">
|
|
991
|
+
{fixText && (
|
|
992
|
+
<div className="border-b border-theme-border p-4">
|
|
993
|
+
<div className="mb-1.5 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-accent">
|
|
994
|
+
<Sparkles className="h-3.5 w-3.5" />
|
|
995
|
+
Proposed change
|
|
996
|
+
</div>
|
|
997
|
+
<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">
|
|
998
|
+
{fixText}
|
|
999
|
+
</AIMarkdown>
|
|
930
1000
|
</div>
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
</div>
|
|
935
|
-
)}
|
|
936
|
-
|
|
937
|
-
<div className="space-y-2 p-4">
|
|
1001
|
+
)}
|
|
1002
|
+
</div>
|
|
1003
|
+
<div className="shrink-0 space-y-2 p-4">
|
|
938
1004
|
{/* The star warning: when we KNOW a controller owns this resource, a live
|
|
939
1005
|
change reverts on the next reconcile — say so authoritatively. */}
|
|
940
1006
|
{managedBy && (
|
|
@@ -964,7 +1030,7 @@ export function ApplyDialog({
|
|
|
964
1030
|
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
965
1031
|
<span>
|
|
966
1032
|
The agent had <span className="font-medium">low confidence</span>{" "}
|
|
967
|
-
in this
|
|
1033
|
+
in this conclusion — consider asking a follow-up to verify before
|
|
968
1034
|
applying.
|
|
969
1035
|
</span>
|
|
970
1036
|
</div>
|
|
@@ -978,8 +1044,7 @@ export function ApplyDialog({
|
|
|
978
1044
|
</span>
|
|
979
1045
|
</div>
|
|
980
1046
|
</div>
|
|
981
|
-
|
|
982
|
-
<div className="flex items-center justify-end gap-3 border-t border-theme-border p-4">
|
|
1047
|
+
<div className="flex shrink-0 items-center justify-end gap-3 border-t border-theme-border p-4">
|
|
983
1048
|
<button
|
|
984
1049
|
onClick={onClose}
|
|
985
1050
|
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"
|
|
@@ -1004,13 +1069,23 @@ export function Timeline({
|
|
|
1004
1069
|
running,
|
|
1005
1070
|
applyMode,
|
|
1006
1071
|
followup,
|
|
1007
|
-
|
|
1072
|
+
turnIndex,
|
|
1073
|
+
evidenceStepIds,
|
|
1074
|
+
onViewEvidence,
|
|
1075
|
+
sourceRevealRequest,
|
|
1008
1076
|
}: {
|
|
1009
1077
|
items: TimelineItem[];
|
|
1010
1078
|
running: boolean;
|
|
1011
1079
|
applyMode?: boolean;
|
|
1012
1080
|
followup?: boolean;
|
|
1013
|
-
|
|
1081
|
+
turnIndex?: number;
|
|
1082
|
+
evidenceStepIds?: ReadonlySet<string>;
|
|
1083
|
+
onViewEvidence?: (sourceId: string) => void;
|
|
1084
|
+
sourceRevealRequest?: {
|
|
1085
|
+
sourceId: string;
|
|
1086
|
+
requestId: number;
|
|
1087
|
+
excerpt?: InvestigationSourceExcerpt;
|
|
1088
|
+
};
|
|
1014
1089
|
}) {
|
|
1015
1090
|
const heading = applyMode
|
|
1016
1091
|
? "Applying fix"
|
|
@@ -1022,8 +1097,7 @@ export function Timeline({
|
|
|
1022
1097
|
const activeTool = [...items]
|
|
1023
1098
|
.reverse()
|
|
1024
1099
|
.find((it) => it.kind === "tool" && it.status !== "done") as
|
|
1025
|
-
|
|
1026
|
-
| undefined;
|
|
1100
|
+
Extract<TimelineItem, { kind: "tool" }> | undefined;
|
|
1027
1101
|
const runningLabel = applyMode
|
|
1028
1102
|
? "Applying the fix…"
|
|
1029
1103
|
: activeTool
|
|
@@ -1040,26 +1114,115 @@ export function Timeline({
|
|
|
1040
1114
|
{heading}
|
|
1041
1115
|
</div>
|
|
1042
1116
|
)}
|
|
1043
|
-
{items.map((it, i) =>
|
|
1044
|
-
it.kind === "thinking"
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1117
|
+
{items.map((it, i) => {
|
|
1118
|
+
if (it.kind === "thinking") {
|
|
1119
|
+
return (
|
|
1120
|
+
<ThinkingBlock
|
|
1121
|
+
key={i}
|
|
1122
|
+
text={it.text}
|
|
1123
|
+
animate={it.animate !== false}
|
|
1124
|
+
live={running && i === items.length - 1}
|
|
1125
|
+
/>
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
const sourceId =
|
|
1129
|
+
turnIndex === undefined
|
|
1130
|
+
? undefined
|
|
1131
|
+
: investigationEvidenceSourceId(turnIndex, it.id);
|
|
1132
|
+
return (
|
|
1133
|
+
<ToolRow
|
|
1134
|
+
key={it.id}
|
|
1135
|
+
step={it}
|
|
1136
|
+
sourceId={sourceId}
|
|
1137
|
+
hasEvidence={evidenceStepIds?.has(it.id) ?? false}
|
|
1138
|
+
onViewEvidence={onViewEvidence}
|
|
1139
|
+
revealRequestId={
|
|
1140
|
+
sourceRevealRequest && sourceId === sourceRevealRequest.sourceId
|
|
1141
|
+
? sourceRevealRequest.requestId
|
|
1142
|
+
: undefined
|
|
1143
|
+
}
|
|
1144
|
+
sourceExcerpt={
|
|
1145
|
+
sourceRevealRequest && sourceRevealRequest.sourceId === sourceId
|
|
1146
|
+
? sourceRevealRequest.excerpt
|
|
1147
|
+
: undefined
|
|
1148
|
+
}
|
|
1149
|
+
animate={it.animate !== false}
|
|
1150
|
+
/>
|
|
1151
|
+
);
|
|
1152
|
+
})}
|
|
1153
|
+
{running && <RunningStatus label={runningLabel} />}
|
|
1154
|
+
</div>
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// The model's reasoning between tool calls — muted + subordinate to the tool
|
|
1159
|
+
// rows, and clamped once its beat is over so the chronology stays scannable.
|
|
1160
|
+
// The final beat remains fully visible while it is streaming; completed prose
|
|
1161
|
+
// is still available through an overflow-aware disclosure.
|
|
1162
|
+
function ThinkingBlock({
|
|
1163
|
+
text,
|
|
1164
|
+
animate,
|
|
1165
|
+
live,
|
|
1166
|
+
}: {
|
|
1167
|
+
text: string;
|
|
1168
|
+
animate: boolean;
|
|
1169
|
+
live: boolean;
|
|
1170
|
+
}) {
|
|
1171
|
+
const [expanded, setExpanded] = useState(false);
|
|
1172
|
+
const [contentHeight, setContentHeight] = useState<number>();
|
|
1173
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
1174
|
+
const contentId = useId();
|
|
1175
|
+
const reveal = useDisclosureReveal<HTMLDivElement>();
|
|
1176
|
+
// Two lines of 12px/19.5px prose plus 8px paragraph/container spacing. Unlike a disclosure
|
|
1177
|
+
// this starts with a visible preview, so animate measured height using the
|
|
1178
|
+
// same 200ms ease-out/reduced-motion treatment as Collapse.
|
|
1179
|
+
const previewHeight = 47;
|
|
1180
|
+
const clamped = !live && !expanded;
|
|
1181
|
+
useEffect(() => {
|
|
1182
|
+
const content = contentRef.current;
|
|
1183
|
+
if (!content) return;
|
|
1184
|
+
const measure = () => setContentHeight(content.scrollHeight);
|
|
1185
|
+
measure();
|
|
1186
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
1187
|
+
const observer = new ResizeObserver(measure);
|
|
1188
|
+
observer.observe(content);
|
|
1189
|
+
return () => observer.disconnect();
|
|
1190
|
+
}, [text]);
|
|
1191
|
+
|
|
1192
|
+
return (
|
|
1193
|
+
<div
|
|
1194
|
+
ref={reveal.elementRef}
|
|
1195
|
+
className={animate ? "animate-transcript-enter" : ""}
|
|
1196
|
+
>
|
|
1197
|
+
<div
|
|
1198
|
+
id={contentId}
|
|
1199
|
+
className="overflow-hidden transition-[height] duration-200 ease-out motion-reduce:transition-none"
|
|
1200
|
+
style={{
|
|
1201
|
+
height: clamped
|
|
1202
|
+
? Math.min(contentHeight ?? previewHeight, previewHeight)
|
|
1203
|
+
: contentHeight,
|
|
1204
|
+
}}
|
|
1205
|
+
>
|
|
1206
|
+
<div ref={contentRef}>
|
|
1207
|
+
<AIMarkdown className="py-0.5 text-xs leading-relaxed text-theme-text-tertiary [overflow-wrap:anywhere] [&_li]:text-theme-text-tertiary [&_p]:my-0.5 [&_strong]:font-medium [&_strong]:text-theme-text-secondary">
|
|
1208
|
+
{text}
|
|
1052
1209
|
</AIMarkdown>
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1210
|
+
</div>
|
|
1211
|
+
</div>
|
|
1212
|
+
{!live && ((contentHeight ?? 0) > previewHeight || expanded) ? (
|
|
1213
|
+
<button
|
|
1214
|
+
type="button"
|
|
1215
|
+
aria-controls={contentId}
|
|
1216
|
+
aria-expanded={expanded}
|
|
1217
|
+
onClick={() => {
|
|
1218
|
+
setExpanded(!expanded);
|
|
1219
|
+
reveal.revealAfterToggle(!expanded);
|
|
1220
|
+
}}
|
|
1221
|
+
className="text-[11px] font-medium text-theme-text-tertiary hover:text-accent-text"
|
|
1222
|
+
>
|
|
1223
|
+
{expanded ? "Show less" : "Show reasoning"}
|
|
1224
|
+
</button>
|
|
1225
|
+
) : null}
|
|
1063
1226
|
</div>
|
|
1064
1227
|
);
|
|
1065
1228
|
}
|
|
@@ -1107,17 +1270,6 @@ function RunningStatus({ label }: { label: string }) {
|
|
|
1107
1270
|
);
|
|
1108
1271
|
}
|
|
1109
1272
|
|
|
1110
|
-
// A staged "thinking" beat — a calm breathing dot + shimmering label. Used both in
|
|
1111
|
-
// the timeline (pre-verdict) and between the root-cause and remediation cards.
|
|
1112
|
-
function SynthBeat({ label }: { label: string }) {
|
|
1113
|
-
return (
|
|
1114
|
-
<div className="flex items-center gap-2 pt-1 text-xs animate-transcript-enter">
|
|
1115
|
-
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-accent animate-synth-pulse" />
|
|
1116
|
-
<span className="ai-shimmer font-medium">{label}…</span>
|
|
1117
|
-
</div>
|
|
1118
|
-
);
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
1273
|
// Maps a running tool to a human verb so the status line reads as activity, not
|
|
1122
1274
|
// machinery. Falls back to the prettified tool name for anything unmapped.
|
|
1123
1275
|
function toolActivity(tool: string): string {
|
|
@@ -1134,69 +1286,194 @@ function toolActivity(tool: string): string {
|
|
|
1134
1286
|
return `${prettyTool(tool)}…`;
|
|
1135
1287
|
}
|
|
1136
1288
|
|
|
1137
|
-
function ToolRow({
|
|
1138
|
-
|
|
1289
|
+
function ToolRow({
|
|
1290
|
+
step,
|
|
1291
|
+
sourceId,
|
|
1292
|
+
hasEvidence,
|
|
1293
|
+
onViewEvidence,
|
|
1294
|
+
revealRequestId,
|
|
1295
|
+
sourceExcerpt,
|
|
1296
|
+
animate,
|
|
1297
|
+
}: {
|
|
1298
|
+
step: Extract<TimelineItem, { kind: "tool" }>;
|
|
1299
|
+
sourceId?: string;
|
|
1300
|
+
hasEvidence?: boolean;
|
|
1301
|
+
onViewEvidence?: (sourceId: string) => void;
|
|
1302
|
+
revealRequestId?: number;
|
|
1303
|
+
sourceExcerpt?: InvestigationSourceExcerpt;
|
|
1304
|
+
animate: boolean;
|
|
1305
|
+
}) {
|
|
1306
|
+
const [open, setOpen] = useState(revealRequestId !== undefined);
|
|
1139
1307
|
const [showFull, setShowFull] = useState(false);
|
|
1308
|
+
const [argumentsOpen, setArgumentsOpen] = useState(false);
|
|
1309
|
+
const toolReveal = useDisclosureReveal<HTMLDivElement>();
|
|
1310
|
+
const argumentsReveal = useDisclosureReveal<HTMLDivElement>();
|
|
1311
|
+
const argumentsId = useId();
|
|
1312
|
+
const argumentsPreview = step.summary ? compactArgs(step.summary) : "";
|
|
1313
|
+
const inlineArguments =
|
|
1314
|
+
argumentsPreview.length <= 240 && !argumentsPreview.includes("\n");
|
|
1315
|
+
const detailId = `investigation-tool-detail-${useId().replaceAll(":", "")}`;
|
|
1140
1316
|
const hasDetail = !!(step.summary || step.result);
|
|
1317
|
+
useEffect(() => {
|
|
1318
|
+
if (revealRequestId !== undefined && hasDetail) setOpen(true);
|
|
1319
|
+
}, [hasDetail, revealRequestId]);
|
|
1141
1320
|
// Offer the rich dialog when the result is structured or non-trivial in size.
|
|
1142
1321
|
const richResult =
|
|
1143
1322
|
!!step.result && (isJsonPayload(step.result) || step.result.length > 200);
|
|
1323
|
+
const done = step.status === "done";
|
|
1324
|
+
const outcomeLabel = !done
|
|
1325
|
+
? "Running"
|
|
1326
|
+
: step.isError === true
|
|
1327
|
+
? "Tool failed"
|
|
1328
|
+
: step.isError === false
|
|
1329
|
+
? "Tool completed"
|
|
1330
|
+
: "Tool finished; outcome not recorded";
|
|
1331
|
+
const rowContent = (
|
|
1332
|
+
<>
|
|
1333
|
+
{!done ? (
|
|
1334
|
+
<Loader2
|
|
1335
|
+
aria-label={outcomeLabel}
|
|
1336
|
+
className="h-3.5 w-3.5 shrink-0 animate-spin text-accent"
|
|
1337
|
+
/>
|
|
1338
|
+
) : step.isError === true ? (
|
|
1339
|
+
<AlertTriangle
|
|
1340
|
+
aria-label={outcomeLabel}
|
|
1341
|
+
className="h-3.5 w-3.5 shrink-0 text-red-400"
|
|
1342
|
+
/>
|
|
1343
|
+
) : step.isError === false ? (
|
|
1344
|
+
<CheckCircle2
|
|
1345
|
+
aria-label={outcomeLabel}
|
|
1346
|
+
className="h-3.5 w-3.5 shrink-0 text-emerald-400"
|
|
1347
|
+
/>
|
|
1348
|
+
) : (
|
|
1349
|
+
<HelpCircle
|
|
1350
|
+
aria-label={outcomeLabel}
|
|
1351
|
+
className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
|
|
1352
|
+
/>
|
|
1353
|
+
)}
|
|
1354
|
+
<span className="shrink-0 font-mono text-xs text-theme-text-secondary">
|
|
1355
|
+
{prettyTool(step.tool)}
|
|
1356
|
+
</span>
|
|
1357
|
+
{step.summary && !open && (
|
|
1358
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[11px] text-theme-text-tertiary">
|
|
1359
|
+
{argumentsPreview}
|
|
1360
|
+
</span>
|
|
1361
|
+
)}
|
|
1362
|
+
{step.ms != null && (
|
|
1363
|
+
<span className="ml-auto shrink-0 text-[11px] text-theme-text-tertiary">
|
|
1364
|
+
{step.ms}ms
|
|
1365
|
+
</span>
|
|
1366
|
+
)}
|
|
1367
|
+
{hasDetail && <CollapseChevron open={open} className="h-3.5 w-3.5" />}
|
|
1368
|
+
</>
|
|
1369
|
+
);
|
|
1144
1370
|
return (
|
|
1145
|
-
<div
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1371
|
+
<div
|
|
1372
|
+
onMouseEnter={(event) =>
|
|
1373
|
+
highlightRelatedEvidence(event.currentTarget, sourceId)
|
|
1374
|
+
}
|
|
1375
|
+
onMouseLeave={(event) => highlightRelatedEvidence(event.currentTarget)}
|
|
1376
|
+
ref={toolReveal.elementRef}
|
|
1377
|
+
id={sourceId ? investigationActivitySourceDomId(sourceId) : undefined}
|
|
1378
|
+
tabIndex={sourceId ? -1 : undefined}
|
|
1379
|
+
role={sourceId ? "group" : undefined}
|
|
1380
|
+
aria-label={
|
|
1381
|
+
sourceId
|
|
1382
|
+
? `${prettyTool(step.tool)} ${outcomeLabel.toLowerCase()}`
|
|
1383
|
+
: undefined
|
|
1384
|
+
}
|
|
1385
|
+
className={`${animate ? "animate-transcript-enter" : ""} scroll-mt-3 rounded-md border border-theme-border/60 bg-theme-base/40 outline-none focus:ring-2 focus:ring-accent/50`}
|
|
1386
|
+
>
|
|
1387
|
+
<div className="flex min-w-0 items-stretch">
|
|
1388
|
+
{hasDetail ? (
|
|
1389
|
+
<button
|
|
1390
|
+
type="button"
|
|
1391
|
+
onClick={() => {
|
|
1392
|
+
setOpen(!open);
|
|
1393
|
+
toolReveal.revealAfterToggle(!open);
|
|
1394
|
+
}}
|
|
1395
|
+
aria-expanded={open}
|
|
1396
|
+
aria-controls={detailId}
|
|
1397
|
+
className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-left text-sm hover:bg-theme-hover"
|
|
1398
|
+
>
|
|
1399
|
+
{rowContent}
|
|
1400
|
+
</button>
|
|
1154
1401
|
) : (
|
|
1155
|
-
<
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
{prettyTool(step.tool)}
|
|
1159
|
-
</span>
|
|
1160
|
-
{step.summary && (
|
|
1161
|
-
<span className="min-w-0 flex-1 truncate font-mono text-[11px] text-theme-text-tertiary">
|
|
1162
|
-
{compactArgs(step.summary)}
|
|
1163
|
-
</span>
|
|
1164
|
-
)}
|
|
1165
|
-
{step.ms != null && (
|
|
1166
|
-
<span className="ml-auto shrink-0 text-[11px] text-theme-text-tertiary">
|
|
1167
|
-
{step.ms}ms
|
|
1168
|
-
</span>
|
|
1169
|
-
)}
|
|
1170
|
-
{hasDetail && (
|
|
1171
|
-
<ChevronRight
|
|
1172
|
-
className={`h-3.5 w-3.5 shrink-0 text-theme-text-tertiary transition-transform ${open ? "rotate-90" : ""}`}
|
|
1173
|
-
/>
|
|
1402
|
+
<div className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-left text-sm">
|
|
1403
|
+
{rowContent}
|
|
1404
|
+
</div>
|
|
1174
1405
|
)}
|
|
1175
|
-
|
|
1406
|
+
{hasEvidence && sourceId && onViewEvidence ? (
|
|
1407
|
+
<button
|
|
1408
|
+
type="button"
|
|
1409
|
+
onClick={() => onViewEvidence(sourceId)}
|
|
1410
|
+
aria-label={`View evidence from ${prettyTool(step.tool)}`}
|
|
1411
|
+
className="investigation-evidence-jump shrink-0 px-2 text-[11px] font-medium text-accent-text hover:bg-theme-hover"
|
|
1412
|
+
>
|
|
1413
|
+
Show evidence
|
|
1414
|
+
</button>
|
|
1415
|
+
) : null}
|
|
1416
|
+
</div>
|
|
1176
1417
|
{hasDetail && (
|
|
1177
|
-
<
|
|
1178
|
-
<
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1418
|
+
<div id={detailId}>
|
|
1419
|
+
<Collapse open={open}>
|
|
1420
|
+
<div className="space-y-2 border-t border-theme-border/60 px-2 py-2">
|
|
1421
|
+
{step.summary &&
|
|
1422
|
+
(inlineArguments ? (
|
|
1423
|
+
<div
|
|
1424
|
+
className="break-words font-mono text-[11px] leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]"
|
|
1425
|
+
aria-label="Tool arguments"
|
|
1426
|
+
>
|
|
1427
|
+
{argumentsPreview}
|
|
1428
|
+
</div>
|
|
1429
|
+
) : (
|
|
1430
|
+
<div ref={argumentsReveal.elementRef}>
|
|
1187
1431
|
<button
|
|
1188
|
-
|
|
1189
|
-
|
|
1432
|
+
type="button"
|
|
1433
|
+
aria-expanded={argumentsOpen}
|
|
1434
|
+
aria-controls={argumentsId}
|
|
1435
|
+
onClick={() => {
|
|
1436
|
+
setArgumentsOpen(!argumentsOpen);
|
|
1437
|
+
argumentsReveal.revealAfterToggle(!argumentsOpen);
|
|
1438
|
+
}}
|
|
1439
|
+
className="flex items-center gap-1 py-1 text-xs text-theme-text-tertiary hover:text-theme-text-primary"
|
|
1190
1440
|
>
|
|
1191
|
-
<
|
|
1192
|
-
|
|
1441
|
+
<CollapseChevron
|
|
1442
|
+
open={argumentsOpen}
|
|
1443
|
+
className="h-3.5 w-3.5"
|
|
1444
|
+
/>
|
|
1445
|
+
{argumentsOpen ? "Hide arguments" : "Show arguments"}
|
|
1193
1446
|
</button>
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1447
|
+
<div id={argumentsId}>
|
|
1448
|
+
<Collapse open={argumentsOpen} mountLazily>
|
|
1449
|
+
<PayloadBlock label="Arguments" text={step.summary} />
|
|
1450
|
+
</Collapse>
|
|
1451
|
+
</div>
|
|
1452
|
+
</div>
|
|
1453
|
+
))}
|
|
1454
|
+
{step.result && (
|
|
1455
|
+
<PayloadBlock
|
|
1456
|
+
label="Original result"
|
|
1457
|
+
text={step.result}
|
|
1458
|
+
sourceExcerpt={sourceExcerpt}
|
|
1459
|
+
revealRequestId={revealRequestId}
|
|
1460
|
+
truncated={step.truncated}
|
|
1461
|
+
action={
|
|
1462
|
+
richResult ? (
|
|
1463
|
+
<button
|
|
1464
|
+
onClick={() => setShowFull(true)}
|
|
1465
|
+
className="flex items-center gap-1 text-[11px] text-accent hover:underline"
|
|
1466
|
+
>
|
|
1467
|
+
<Maximize2 className="h-3 w-3" />
|
|
1468
|
+
View payload
|
|
1469
|
+
</button>
|
|
1470
|
+
) : undefined
|
|
1471
|
+
}
|
|
1472
|
+
/>
|
|
1473
|
+
)}
|
|
1474
|
+
</div>
|
|
1475
|
+
</Collapse>
|
|
1476
|
+
</div>
|
|
1200
1477
|
)}
|
|
1201
1478
|
{step.result && (
|
|
1202
1479
|
<ToolResultDialog
|
|
@@ -1235,13 +1512,36 @@ function PayloadBlock({
|
|
|
1235
1512
|
text,
|
|
1236
1513
|
truncated,
|
|
1237
1514
|
action,
|
|
1515
|
+
sourceExcerpt,
|
|
1516
|
+
revealRequestId,
|
|
1238
1517
|
}: {
|
|
1239
1518
|
label: string;
|
|
1240
1519
|
text: string;
|
|
1241
1520
|
truncated?: boolean;
|
|
1242
1521
|
action?: ReactNode;
|
|
1522
|
+
sourceExcerpt?: InvestigationSourceExcerpt;
|
|
1523
|
+
revealRequestId?: number;
|
|
1243
1524
|
}) {
|
|
1244
1525
|
const json = formatJson(text);
|
|
1526
|
+
const display = json ?? text;
|
|
1527
|
+
const range = locateSourceExcerpt(display, sourceExcerpt);
|
|
1528
|
+
const rangeStart = range?.start;
|
|
1529
|
+
const rangeEnd = range?.end;
|
|
1530
|
+
const preRef = useRef<HTMLPreElement>(null);
|
|
1531
|
+
const markRef = useRef<HTMLElement>(null);
|
|
1532
|
+
useEffect(() => {
|
|
1533
|
+
if (rangeStart === undefined || revealRequestId === undefined) return;
|
|
1534
|
+
const frame = requestAnimationFrame(() => {
|
|
1535
|
+
const pre = preRef.current,
|
|
1536
|
+
mark = markRef.current;
|
|
1537
|
+
if (pre && mark)
|
|
1538
|
+
pre.scrollTop +=
|
|
1539
|
+
mark.getBoundingClientRect().top -
|
|
1540
|
+
pre.getBoundingClientRect().top -
|
|
1541
|
+
pre.clientHeight / 3;
|
|
1542
|
+
});
|
|
1543
|
+
return () => cancelAnimationFrame(frame);
|
|
1544
|
+
}, [revealRequestId, rangeStart, rangeEnd]);
|
|
1245
1545
|
return (
|
|
1246
1546
|
<div>
|
|
1247
1547
|
<div className="mb-0.5 flex items-center justify-between gap-2">
|
|
@@ -1250,13 +1550,30 @@ function PayloadBlock({
|
|
|
1250
1550
|
</span>
|
|
1251
1551
|
<div className="flex items-center gap-2">
|
|
1252
1552
|
{action}
|
|
1253
|
-
<CopyButton
|
|
1553
|
+
<CopyButton
|
|
1554
|
+
text={json ?? text}
|
|
1555
|
+
label={`Copy ${label.toLowerCase()}`}
|
|
1556
|
+
/>
|
|
1254
1557
|
</div>
|
|
1255
1558
|
</div>
|
|
1256
1559
|
<pre
|
|
1257
|
-
|
|
1560
|
+
ref={preRef}
|
|
1561
|
+
className={`max-h-64 overflow-auto rounded bg-theme-elevated p-1.5 font-mono text-[11px] text-theme-text-secondary ${json && !range ? "" : "whitespace-pre-wrap [overflow-wrap:anywhere]"}`}
|
|
1258
1562
|
>
|
|
1259
|
-
{
|
|
1563
|
+
{range ? (
|
|
1564
|
+
<>
|
|
1565
|
+
{display.slice(0, range.start)}
|
|
1566
|
+
<mark
|
|
1567
|
+
ref={markRef}
|
|
1568
|
+
className="bg-accent-muted text-theme-text-primary ring-1 ring-accent/40"
|
|
1569
|
+
>
|
|
1570
|
+
{display.slice(range.start, range.end)}
|
|
1571
|
+
</mark>
|
|
1572
|
+
{display.slice(range.end)}
|
|
1573
|
+
</>
|
|
1574
|
+
) : (
|
|
1575
|
+
display
|
|
1576
|
+
)}
|
|
1260
1577
|
</pre>
|
|
1261
1578
|
{truncated && (
|
|
1262
1579
|
<div className="mt-0.5 text-[10px] text-amber-500">
|
|
@@ -1345,7 +1662,7 @@ function ToolResultDialog({
|
|
|
1345
1662
|
/>
|
|
1346
1663
|
</div>
|
|
1347
1664
|
)}
|
|
1348
|
-
<CopyButton text={display} />
|
|
1665
|
+
<CopyButton text={display} label="Copy tool result" />
|
|
1349
1666
|
</div>
|
|
1350
1667
|
</div>
|
|
1351
1668
|
{html ? (
|
|
@@ -1370,18 +1687,6 @@ function safeYaml(value: unknown): string {
|
|
|
1370
1687
|
}
|
|
1371
1688
|
}
|
|
1372
1689
|
|
|
1373
|
-
// Collapse — the Radar-standard expand/collapse motion (grid-template-rows
|
|
1374
|
-
// 0fr↔1fr) used across issue rows. Children stay mounted so close animates too.
|
|
1375
|
-
function Collapse({ open, children }: { open: boolean; children: ReactNode }) {
|
|
1376
|
-
return (
|
|
1377
|
-
<div
|
|
1378
|
-
className={`issue-details-motion ${open ? "issue-details-motion-open" : ""}`}
|
|
1379
|
-
>
|
|
1380
|
-
<div className="overflow-hidden">{children}</div>
|
|
1381
|
-
</div>
|
|
1382
|
-
);
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
1690
|
function compactArgs(raw: string): string {
|
|
1386
1691
|
try {
|
|
1387
1692
|
const o = JSON.parse(raw);
|
|
@@ -1396,71 +1701,271 @@ function compactArgs(raw: string): string {
|
|
|
1396
1701
|
export function ResultCard({
|
|
1397
1702
|
diagnosis,
|
|
1398
1703
|
onApply,
|
|
1399
|
-
|
|
1704
|
+
explanation,
|
|
1400
1705
|
apply,
|
|
1706
|
+
applyOutcome,
|
|
1401
1707
|
followup,
|
|
1402
|
-
|
|
1708
|
+
section = "full",
|
|
1403
1709
|
onCheckStatus,
|
|
1710
|
+
animate = true,
|
|
1711
|
+
showDisclaimer = true,
|
|
1712
|
+
coverageLimited = false,
|
|
1713
|
+
evidenceConflict = false,
|
|
1714
|
+
compactActions = false,
|
|
1715
|
+
assessmentAction,
|
|
1716
|
+
assessmentSources,
|
|
1717
|
+
actionNotice,
|
|
1404
1718
|
}: {
|
|
1405
1719
|
diagnosis: Diagnosis;
|
|
1406
1720
|
onApply?: (fix: string) => void;
|
|
1407
|
-
|
|
1721
|
+
explanation?: AssessmentExplanation;
|
|
1408
1722
|
apply?: boolean;
|
|
1723
|
+
applyOutcome?: ApplyMutationOutcome;
|
|
1409
1724
|
followup?: boolean;
|
|
1410
|
-
|
|
1725
|
+
section?: "full" | "conclusion" | "actions";
|
|
1411
1726
|
onCheckStatus?: () => void;
|
|
1727
|
+
animate?: boolean;
|
|
1728
|
+
/** Additional navigation placed in the assessment action row. */
|
|
1729
|
+
assessmentAction?: ReactNode;
|
|
1730
|
+
assessmentSources?: ReactNode;
|
|
1731
|
+
/** Hide the repeated disclaimer when the enclosing surface provides context. */
|
|
1732
|
+
showDisclaimer?: boolean;
|
|
1733
|
+
/** Qualifies a healthy assessment when structured evidence is absent or partial. */
|
|
1734
|
+
coverageLimited?: boolean;
|
|
1735
|
+
/** Marks a healthy agent assessment that conflicts with same-turn Key evidence. */
|
|
1736
|
+
evidenceConflict?: boolean;
|
|
1737
|
+
/** Show only the recommended (or first) action until the user asks for more. */
|
|
1738
|
+
compactActions?: boolean;
|
|
1739
|
+
actionNotice?: string;
|
|
1412
1740
|
}) {
|
|
1413
|
-
// Apply turns report
|
|
1414
|
-
//
|
|
1741
|
+
// Apply turns report mutation truth, not a diagnosis. The outcome-specific
|
|
1742
|
+
// card decides whether that truth is confirmed, failed, or still unknown.
|
|
1415
1743
|
if (apply)
|
|
1416
1744
|
return (
|
|
1417
|
-
<ApplyOutcomeCard
|
|
1745
|
+
<ApplyOutcomeCard
|
|
1746
|
+
diagnosis={diagnosis}
|
|
1747
|
+
applyOutcome={applyOutcome}
|
|
1748
|
+
onCheckStatus={onCheckStatus}
|
|
1749
|
+
animate={animate}
|
|
1750
|
+
/>
|
|
1418
1751
|
);
|
|
1752
|
+
// A question remains conversational even if the model happens to set a health
|
|
1753
|
+
// flag in its structured envelope. Never promote an ordinary answer into an
|
|
1754
|
+
// authoritative investigation conclusion.
|
|
1755
|
+
if (followup)
|
|
1756
|
+
return <FollowupAnswer diagnosis={diagnosis} animate={animate} />;
|
|
1419
1757
|
if (diagnosis.healthy && !diagnosis.rootCause)
|
|
1420
|
-
return
|
|
1758
|
+
return section === "actions" ? null : (
|
|
1759
|
+
<AllClearCard
|
|
1760
|
+
diagnosis={diagnosis}
|
|
1761
|
+
animate={animate}
|
|
1762
|
+
showDisclaimer={showDisclaimer}
|
|
1763
|
+
coverageLimited={coverageLimited}
|
|
1764
|
+
evidenceConflict={evidenceConflict}
|
|
1765
|
+
assessmentAction={assessmentAction}
|
|
1766
|
+
assessmentSources={assessmentSources}
|
|
1767
|
+
/>
|
|
1768
|
+
);
|
|
1421
1769
|
// Couldn't-determine is its own honest state — never a confident all-clear, never
|
|
1422
1770
|
// the alarming root-cause anchor.
|
|
1423
1771
|
if (diagnosis.inconclusive && !diagnosis.rootCause)
|
|
1424
|
-
return
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1772
|
+
return section === "actions" ? null : (
|
|
1773
|
+
<>
|
|
1774
|
+
<InconclusiveCard diagnosis={diagnosis} animate={animate} />
|
|
1775
|
+
{assessmentSources ? (
|
|
1776
|
+
<AssessmentSourceDetails>{assessmentSources}</AssessmentSourceDetails>
|
|
1777
|
+
) : null}
|
|
1778
|
+
{assessmentAction && (
|
|
1779
|
+
<div className="mt-2 flex justify-end">{assessmentAction}</div>
|
|
1780
|
+
)}
|
|
1781
|
+
</>
|
|
1782
|
+
);
|
|
1428
1783
|
// A turn with no structured root cause and no remediation (e.g. "looks healthy",
|
|
1429
1784
|
// or a clarifying question) is not a diagnosis — render it neutrally rather than
|
|
1430
1785
|
// forcing the alarming root-cause anchor onto a non-problem.
|
|
1431
1786
|
const structured =
|
|
1432
1787
|
!!diagnosis.rootCause || (diagnosis.remediation?.length ?? 0) > 0;
|
|
1433
|
-
if (!structured)
|
|
1788
|
+
if (!structured)
|
|
1789
|
+
return (
|
|
1790
|
+
<>
|
|
1791
|
+
<FollowupAnswer diagnosis={diagnosis} animate={animate} />
|
|
1792
|
+
{assessmentSources ? (
|
|
1793
|
+
<AssessmentSourceDetails>{assessmentSources}</AssessmentSourceDetails>
|
|
1794
|
+
) : null}
|
|
1795
|
+
{assessmentAction && (
|
|
1796
|
+
<div className="mt-2 flex justify-end">{assessmentAction}</div>
|
|
1797
|
+
)}
|
|
1798
|
+
</>
|
|
1799
|
+
);
|
|
1434
1800
|
|
|
1435
1801
|
return (
|
|
1436
1802
|
<DiagnosisResult
|
|
1437
1803
|
diagnosis={diagnosis}
|
|
1438
1804
|
onApply={onApply}
|
|
1439
|
-
|
|
1440
|
-
|
|
1805
|
+
explanation={explanation}
|
|
1806
|
+
section={section}
|
|
1807
|
+
animate={animate}
|
|
1808
|
+
showDisclaimer={showDisclaimer}
|
|
1809
|
+
compactActions={compactActions}
|
|
1810
|
+
assessmentAction={assessmentAction}
|
|
1811
|
+
assessmentSources={assessmentSources}
|
|
1812
|
+
actionNotice={actionNotice}
|
|
1441
1813
|
/>
|
|
1442
1814
|
);
|
|
1443
1815
|
}
|
|
1444
1816
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1817
|
+
export function AssessmentSources({
|
|
1818
|
+
resolution,
|
|
1819
|
+
onViewSource,
|
|
1820
|
+
}: {
|
|
1821
|
+
resolution?: InvestigationRootCauseEvidenceResolution;
|
|
1822
|
+
onViewSource: (sourceId: string) => void;
|
|
1823
|
+
}) {
|
|
1824
|
+
if (resolution?.status !== "linked" || !resolution.links.length) return null;
|
|
1825
|
+
return (
|
|
1826
|
+
<div className="mt-3 border-t border-theme-border/60 pt-2">
|
|
1827
|
+
<h4 className="text-xs font-medium text-theme-text-secondary">
|
|
1828
|
+
Sources used for this assessment
|
|
1829
|
+
</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
|
+
))}
|
|
1850
|
+
</ul>
|
|
1851
|
+
</div>
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1447
1854
|
|
|
1448
|
-
|
|
1855
|
+
function AssessmentSourceDetails({ children }: { children: ReactNode }) {
|
|
1856
|
+
const [open, setOpen] = useState(false);
|
|
1857
|
+
const id = useId();
|
|
1858
|
+
const reveal = useDisclosureReveal<HTMLDivElement>();
|
|
1859
|
+
return (
|
|
1860
|
+
<div ref={reveal.elementRef}>
|
|
1861
|
+
<button
|
|
1862
|
+
type="button"
|
|
1863
|
+
aria-expanded={open}
|
|
1864
|
+
aria-controls={id}
|
|
1865
|
+
onClick={() => {
|
|
1866
|
+
setOpen(!open);
|
|
1867
|
+
reveal.revealAfterToggle(!open);
|
|
1868
|
+
}}
|
|
1869
|
+
className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
|
|
1870
|
+
>
|
|
1871
|
+
<CollapseChevron open={open} className="h-3.5 w-3.5" />
|
|
1872
|
+
Assessment details
|
|
1873
|
+
</button>
|
|
1874
|
+
<div id={id}>
|
|
1875
|
+
<Collapse open={open}>{children}</Collapse>
|
|
1876
|
+
</div>
|
|
1877
|
+
</div>
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function CitedSourceScope({ source }: { source: InvestigationEvidenceSource }) {
|
|
1882
|
+
let input: unknown;
|
|
1883
|
+
try {
|
|
1884
|
+
input = JSON.parse(source.args ?? "");
|
|
1885
|
+
} catch {
|
|
1886
|
+
return null;
|
|
1887
|
+
}
|
|
1888
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return null;
|
|
1889
|
+
const args = input as Record<string, unknown>;
|
|
1890
|
+
const fields = ["query", "kind", "namespace", "name", "filter"].flatMap(
|
|
1891
|
+
(key) =>
|
|
1892
|
+
typeof args[key] === "string" && args[key].trim()
|
|
1893
|
+
? [`${key === "query" ? "" : `${key}: `}${args[key]}`]
|
|
1894
|
+
: [],
|
|
1895
|
+
);
|
|
1896
|
+
return fields.length > 0 ? (
|
|
1897
|
+
<span className="mt-0.5 block break-words text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
1898
|
+
{fields.join(" · ")}
|
|
1899
|
+
</span>
|
|
1900
|
+
) : null;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
export type AssessmentExplanation = {
|
|
1904
|
+
status: "idle" | "running" | "done" | "error";
|
|
1905
|
+
text?: string;
|
|
1906
|
+
error?: string;
|
|
1907
|
+
onGenerate?: () => void;
|
|
1908
|
+
openRequest?: number;
|
|
1909
|
+
};
|
|
1910
|
+
|
|
1911
|
+
// The diagnosis result: likely cause + remediation + the
|
|
1449
1912
|
// agent's full analysis on demand.
|
|
1450
1913
|
function DiagnosisResult({
|
|
1451
1914
|
diagnosis,
|
|
1452
1915
|
onApply,
|
|
1453
|
-
|
|
1454
|
-
|
|
1916
|
+
explanation,
|
|
1917
|
+
section = "full",
|
|
1918
|
+
animate,
|
|
1919
|
+
showDisclaimer,
|
|
1920
|
+
compactActions,
|
|
1921
|
+
assessmentAction,
|
|
1922
|
+
assessmentSources,
|
|
1923
|
+
actionNotice,
|
|
1455
1924
|
}: {
|
|
1456
1925
|
diagnosis: Diagnosis;
|
|
1457
1926
|
onApply?: (fix: string) => void;
|
|
1458
|
-
|
|
1459
|
-
|
|
1927
|
+
explanation?: AssessmentExplanation;
|
|
1928
|
+
section?: "full" | "conclusion" | "actions";
|
|
1929
|
+
animate: boolean;
|
|
1930
|
+
showDisclaimer: boolean;
|
|
1931
|
+
compactActions: boolean;
|
|
1932
|
+
assessmentAction?: ReactNode;
|
|
1933
|
+
assessmentSources?: ReactNode;
|
|
1934
|
+
actionNotice?: string;
|
|
1460
1935
|
}) {
|
|
1461
|
-
const [
|
|
1462
|
-
|
|
1463
|
-
|
|
1936
|
+
const [detail, setDetail] = useState<"analysis" | "explanation" | null>(
|
|
1937
|
+
explanation?.status === "running" ? "explanation" : null,
|
|
1938
|
+
);
|
|
1939
|
+
const showAnalysis = detail === "analysis";
|
|
1940
|
+
const analysisReveal = useDisclosureReveal<HTMLDivElement>();
|
|
1941
|
+
const { elementRef: analysisElementRef, revealAfterToggle: revealAnalysis } =
|
|
1942
|
+
analysisReveal;
|
|
1943
|
+
useEffect(() => {
|
|
1944
|
+
if (explanation?.openRequest) {
|
|
1945
|
+
setDetail("explanation");
|
|
1946
|
+
revealAnalysis(true);
|
|
1947
|
+
}
|
|
1948
|
+
}, [explanation?.openRequest, revealAnalysis]);
|
|
1949
|
+
useEffect(() => {
|
|
1950
|
+
if (
|
|
1951
|
+
detail === "explanation" &&
|
|
1952
|
+
(explanation?.status === "done" || explanation?.status === "error")
|
|
1953
|
+
) {
|
|
1954
|
+
const element = analysisElementRef.current;
|
|
1955
|
+
const scroller = element?.closest("[data-investigation-findings-scroll]");
|
|
1956
|
+
if (element && scroller) {
|
|
1957
|
+
const top = element.getBoundingClientRect().top;
|
|
1958
|
+
const viewport = scroller.getBoundingClientRect();
|
|
1959
|
+
if (top >= viewport.top && top < viewport.bottom) revealAnalysis(true);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}, [explanation?.status, detail, analysisElementRef, revealAnalysis]);
|
|
1963
|
+
const [showAllSteps, setShowAllSteps] = useState(false);
|
|
1964
|
+
const stepsId = useId();
|
|
1965
|
+
const stepsReveal = useDisclosureReveal<HTMLDivElement>();
|
|
1966
|
+
const analysisId = useId();
|
|
1967
|
+
// Only a real structured cause anchors the amber card; the full prose lives in
|
|
1968
|
+
// "Full analysis" (never relabel the report as a causal assessment).
|
|
1464
1969
|
const rootCause = diagnosis.rootCause;
|
|
1465
1970
|
const remediation = diagnosis.remediation || [];
|
|
1466
1971
|
const hasRemediation = remediation.length > 0;
|
|
@@ -1471,161 +1976,321 @@ function DiagnosisResult({
|
|
|
1471
1976
|
// When it returns 0 / none ("needs human judgement"), we honor that and don't
|
|
1472
1977
|
// offer one-click apply — the steps stay copy-only with a note.
|
|
1473
1978
|
const canApply = !!onApply && recValid;
|
|
1979
|
+
const showConclusion = section !== "actions";
|
|
1980
|
+
const showActions = section !== "conclusion";
|
|
1981
|
+
const remediationEntries = remediation.map((text, index) => ({
|
|
1982
|
+
text,
|
|
1983
|
+
index,
|
|
1984
|
+
}));
|
|
1985
|
+
const primaryActionIndex = recValid ? recIdx! - 1 : 0;
|
|
1986
|
+
const hiddenStepCount = remediationEntries.length - 1;
|
|
1987
|
+
const renderRemediationStep = ({
|
|
1988
|
+
text: r,
|
|
1989
|
+
index: i,
|
|
1990
|
+
}: {
|
|
1991
|
+
text: string;
|
|
1992
|
+
index: number;
|
|
1993
|
+
}) => {
|
|
1994
|
+
const isRec = recValid && i === recIdx! - 1;
|
|
1995
|
+
return (
|
|
1996
|
+
<div
|
|
1997
|
+
key={i}
|
|
1998
|
+
className={
|
|
1999
|
+
isRec ? "rounded-lg border border-accent/40 bg-accent/5 p-2.5" : ""
|
|
2000
|
+
}
|
|
2001
|
+
>
|
|
2002
|
+
<div className="flex items-start gap-2">
|
|
2003
|
+
<span
|
|
2004
|
+
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[10px] ${
|
|
2005
|
+
isRec
|
|
2006
|
+
? "bg-accent/20 text-accent"
|
|
2007
|
+
: "bg-theme-base text-theme-text-tertiary"
|
|
2008
|
+
}`}
|
|
2009
|
+
>
|
|
2010
|
+
{i + 1}
|
|
2011
|
+
</span>
|
|
2012
|
+
<div className="min-w-0 flex-1">
|
|
2013
|
+
{isRec && (
|
|
2014
|
+
<div className="mb-1">
|
|
2015
|
+
<div className="flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wide text-accent">
|
|
2016
|
+
<Sparkles className="h-3 w-3" />
|
|
2017
|
+
Recommended
|
|
2018
|
+
</div>
|
|
2019
|
+
{diagnosis.recommendedReason && (
|
|
2020
|
+
<div className="mt-0.5 max-w-[100ch] text-[11px] leading-snug text-theme-text-tertiary">
|
|
2021
|
+
{diagnosis.recommendedReason}
|
|
2022
|
+
</div>
|
|
2023
|
+
)}
|
|
2024
|
+
</div>
|
|
2025
|
+
)}
|
|
2026
|
+
<AIMarkdown className="max-w-[100ch] text-sm [overflow-wrap:anywhere] [&_p]:my-0 [&_pre]:my-1.5">
|
|
2027
|
+
{r}
|
|
2028
|
+
</AIMarkdown>
|
|
2029
|
+
</div>
|
|
2030
|
+
{/* Action cluster: compact Apply (recommended = subtly
|
|
2031
|
+
filled, others = ghost) sits next to Copy so each row's
|
|
2032
|
+
actions stay together. The ellipsis signals a confirm
|
|
2033
|
+
dialog follows — it doesn't apply immediately. */}
|
|
2034
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
2035
|
+
{canApply && isRec && (
|
|
2036
|
+
<button
|
|
2037
|
+
onClick={() => onApply!(r)}
|
|
2038
|
+
className={`inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-accent transition-colors ${
|
|
2039
|
+
isRec
|
|
2040
|
+
? "border border-accent/40 bg-accent/10 hover:bg-accent/20"
|
|
2041
|
+
: "hover:bg-accent/10"
|
|
2042
|
+
}`}
|
|
2043
|
+
>
|
|
2044
|
+
<Wrench className="h-3 w-3" />
|
|
2045
|
+
Apply…
|
|
2046
|
+
</button>
|
|
2047
|
+
)}
|
|
2048
|
+
<CopyButton text={r} label={`Copy remediation step ${i + 1}`} />
|
|
2049
|
+
</div>
|
|
2050
|
+
</div>
|
|
2051
|
+
</div>
|
|
2052
|
+
);
|
|
2053
|
+
};
|
|
1474
2054
|
return (
|
|
1475
|
-
<div className=
|
|
1476
|
-
{/*
|
|
1477
|
-
{rootCause && (
|
|
2055
|
+
<div className={`mt-3 space-y-2 ${animate ? "animate-result-in" : ""}`}>
|
|
2056
|
+
{/* Likely cause — agent-authored, visually prominent without claiming proof. */}
|
|
2057
|
+
{showConclusion && rootCause && (
|
|
1478
2058
|
<div
|
|
1479
|
-
className=
|
|
1480
|
-
|
|
2059
|
+
className={
|
|
2060
|
+
section === "conclusion"
|
|
2061
|
+
? "grid grid-cols-[minmax(0,1fr)_auto] items-start gap-2"
|
|
2062
|
+
: "rounded-lg border border-theme-border bg-theme-surface p-3"
|
|
2063
|
+
}
|
|
1481
2064
|
>
|
|
1482
|
-
<div
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
2065
|
+
<div
|
|
2066
|
+
className={
|
|
2067
|
+
section === "conclusion"
|
|
2068
|
+
? "col-start-2 row-start-1"
|
|
2069
|
+
: "mb-1 flex items-center justify-between gap-2"
|
|
2070
|
+
}
|
|
2071
|
+
>
|
|
2072
|
+
{section !== "conclusion" && (
|
|
2073
|
+
<div className="text-xs font-medium text-theme-text-secondary">
|
|
2074
|
+
Likely cause
|
|
2075
|
+
</div>
|
|
2076
|
+
)}
|
|
1488
2077
|
<div className="flex items-center gap-2">
|
|
1489
|
-
{
|
|
1490
|
-
<ConfidenceMeter value={diagnosis.confidence} />
|
|
1491
|
-
) : (
|
|
1492
|
-
<ConfidenceUnstated />
|
|
1493
|
-
)}
|
|
1494
|
-
<CopyButton text={rootCause} />
|
|
2078
|
+
<CopyButton text={rootCause} label="Copy likely cause" />
|
|
1495
2079
|
</div>
|
|
1496
2080
|
</div>
|
|
1497
|
-
<AIMarkdown
|
|
2081
|
+
<AIMarkdown
|
|
2082
|
+
className={`${section === "conclusion" ? "col-start-1 row-start-1 max-w-[100ch]" : "max-w-prose"} text-sm leading-relaxed text-theme-text-primary [overflow-wrap:anywhere] [&_code]:font-normal [&_p]:my-0 [&_p]:text-theme-text-primary`}
|
|
2083
|
+
>
|
|
1498
2084
|
{rootCause}
|
|
1499
2085
|
</AIMarkdown>
|
|
1500
|
-
{onAsk && reveal === "full" && (
|
|
1501
|
-
<button
|
|
1502
|
-
onClick={() => onAsk(EXPLAIN_SIMPLY_PROMPT)}
|
|
1503
|
-
className="mt-2 inline-flex items-center gap-1 rounded-md border border-theme-border px-2 py-1 text-[11px] font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
1504
|
-
>
|
|
1505
|
-
<HelpCircle className="h-3 w-3" />
|
|
1506
|
-
Explain simply
|
|
1507
|
-
</button>
|
|
1508
|
-
)}
|
|
1509
2086
|
</div>
|
|
1510
2087
|
)}
|
|
1511
2088
|
|
|
1512
|
-
{/*
|
|
1513
|
-
|
|
1514
|
-
{
|
|
1515
|
-
<SynthBeat label="Weighing remediation options" />
|
|
1516
|
-
)}
|
|
1517
|
-
|
|
1518
|
-
{/* Remediation — copyable steps; the recommended one is highlighted as the
|
|
1519
|
-
default, and any step can be applied (Apply binds to that step's text). */}
|
|
1520
|
-
{reveal === "full" && hasRemediation && (
|
|
2089
|
+
{/* Remediation — every step is copyable. Only the explicitly recommended
|
|
2090
|
+
step can be applied, and only when the caller enables apply. */}
|
|
2091
|
+
{showActions && hasRemediation && (
|
|
1521
2092
|
<div
|
|
1522
|
-
|
|
1523
|
-
|
|
2093
|
+
ref={stepsReveal.elementRef}
|
|
2094
|
+
className={
|
|
2095
|
+
section === "actions"
|
|
2096
|
+
? "space-y-2"
|
|
2097
|
+
: "rounded-lg border border-theme-border bg-theme-surface p-3"
|
|
2098
|
+
}
|
|
1524
2099
|
>
|
|
1525
|
-
|
|
1526
|
-
<
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
}`}
|
|
1549
|
-
>
|
|
1550
|
-
{i + 1}
|
|
1551
|
-
</span>
|
|
1552
|
-
<div className="min-w-0 flex-1">
|
|
1553
|
-
{isRec && (
|
|
1554
|
-
<div className="mb-1">
|
|
1555
|
-
<div className="flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wide text-accent">
|
|
1556
|
-
<Sparkles className="h-3 w-3" />
|
|
1557
|
-
Recommended
|
|
1558
|
-
</div>
|
|
1559
|
-
{diagnosis.recommendedReason && (
|
|
1560
|
-
<div className="mt-0.5 text-[11px] leading-snug text-theme-text-tertiary">
|
|
1561
|
-
{diagnosis.recommendedReason}
|
|
1562
|
-
</div>
|
|
1563
|
-
)}
|
|
1564
|
-
</div>
|
|
1565
|
-
)}
|
|
1566
|
-
<AIMarkdown className="text-sm [overflow-wrap:anywhere] [&_p]:my-0 [&_pre]:my-1.5">
|
|
1567
|
-
{r}
|
|
1568
|
-
</AIMarkdown>
|
|
1569
|
-
</div>
|
|
1570
|
-
{/* Action cluster: compact Apply (recommended = subtly
|
|
1571
|
-
filled, others = ghost) sits next to Copy so each row's
|
|
1572
|
-
actions stay together. The ellipsis signals a confirm
|
|
1573
|
-
dialog follows — it doesn't apply immediately. */}
|
|
1574
|
-
<div className="flex shrink-0 items-center gap-0.5">
|
|
1575
|
-
{canApply && (
|
|
1576
|
-
<button
|
|
1577
|
-
onClick={() => onApply!(r)}
|
|
1578
|
-
className={`inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-accent transition-colors ${
|
|
1579
|
-
isRec
|
|
1580
|
-
? "border border-accent/40 bg-accent/10 hover:bg-accent/20"
|
|
1581
|
-
: "hover:bg-accent/10"
|
|
1582
|
-
}`}
|
|
1583
|
-
>
|
|
1584
|
-
<Wrench className="h-3 w-3" />
|
|
1585
|
-
Apply…
|
|
1586
|
-
</button>
|
|
1587
|
-
)}
|
|
1588
|
-
<CopyButton text={r} />
|
|
1589
|
-
</div>
|
|
1590
|
-
</div>
|
|
2100
|
+
{section !== "actions" && (
|
|
2101
|
+
<div className="mb-2 flex items-center gap-1.5 text-xs font-medium text-theme-text-secondary">
|
|
2102
|
+
<Wrench className="h-3.5 w-3.5 text-accent" />
|
|
2103
|
+
Remediation
|
|
2104
|
+
</div>
|
|
2105
|
+
)}
|
|
2106
|
+
{actionNotice && (
|
|
2107
|
+
<p className="text-xs text-theme-text-secondary">{actionNotice}</p>
|
|
2108
|
+
)}
|
|
2109
|
+
<div id={stepsId}>
|
|
2110
|
+
<ol>
|
|
2111
|
+
{remediationEntries.map((entry) => (
|
|
2112
|
+
<li key={entry.index} value={entry.index + 1}>
|
|
2113
|
+
<Collapse
|
|
2114
|
+
open={
|
|
2115
|
+
!compactActions ||
|
|
2116
|
+
showAllSteps ||
|
|
2117
|
+
entry.index === primaryActionIndex
|
|
2118
|
+
}
|
|
2119
|
+
mountLazily
|
|
2120
|
+
>
|
|
2121
|
+
<div className="pt-2">{renderRemediationStep(entry)}</div>
|
|
2122
|
+
</Collapse>
|
|
1591
2123
|
</li>
|
|
1592
|
-
)
|
|
1593
|
-
|
|
1594
|
-
</
|
|
1595
|
-
{
|
|
2124
|
+
))}
|
|
2125
|
+
</ol>
|
|
2126
|
+
</div>
|
|
2127
|
+
{compactActions && remediation.length > 1 ? (
|
|
2128
|
+
<button
|
|
2129
|
+
type="button"
|
|
2130
|
+
aria-expanded={showAllSteps}
|
|
2131
|
+
aria-controls={stepsId}
|
|
2132
|
+
onClick={() => {
|
|
2133
|
+
setShowAllSteps(!showAllSteps);
|
|
2134
|
+
stepsReveal.revealAfterToggle(!showAllSteps);
|
|
2135
|
+
}}
|
|
2136
|
+
className="mt-2 inline-flex items-center gap-1 rounded-md px-1.5 py-1 text-[11px] font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
2137
|
+
>
|
|
2138
|
+
<CollapseChevron open={showAllSteps} className="h-3.5 w-3.5" />
|
|
2139
|
+
{showAllSteps
|
|
2140
|
+
? recValid
|
|
2141
|
+
? "Show only recommended step"
|
|
2142
|
+
: "Show only first step"
|
|
2143
|
+
: `Show ${hiddenStepCount} more ${hiddenStepCount === 1 ? "step" : "steps"}`}
|
|
2144
|
+
</button>
|
|
2145
|
+
) : null}
|
|
2146
|
+
{!actionNotice && !recValid && (
|
|
1596
2147
|
<p className="mt-2 flex items-start gap-1.5 text-[11px] leading-snug text-theme-text-tertiary">
|
|
1597
2148
|
<ShieldCheck className="mt-0.5 h-3 w-3 shrink-0" />
|
|
1598
|
-
No
|
|
1599
|
-
|
|
1600
|
-
interactively.
|
|
2149
|
+
No one-click fix is available. Review these steps and apply them
|
|
2150
|
+
manually, or ask the agent to continue.
|
|
1601
2151
|
</p>
|
|
1602
2152
|
)}
|
|
1603
2153
|
</div>
|
|
1604
2154
|
)}
|
|
1605
2155
|
|
|
1606
2156
|
{/* Full analysis — the agent's detailed evidence, on demand. */}
|
|
1607
|
-
{
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
2157
|
+
{showConclusion &&
|
|
2158
|
+
(diagnosis.report ||
|
|
2159
|
+
diagnosis.confidence != null ||
|
|
2160
|
+
explanation ||
|
|
2161
|
+
assessmentAction ||
|
|
2162
|
+
assessmentSources) && (
|
|
2163
|
+
<div>
|
|
2164
|
+
<div
|
|
2165
|
+
className="flex flex-wrap items-center gap-x-3 gap-y-1 pt-2"
|
|
2166
|
+
data-assessment-actions
|
|
2167
|
+
>
|
|
2168
|
+
{(diagnosis.report ||
|
|
2169
|
+
diagnosis.confidence != null ||
|
|
2170
|
+
assessmentSources) && (
|
|
2171
|
+
<button
|
|
2172
|
+
type="button"
|
|
2173
|
+
aria-expanded={showAnalysis}
|
|
2174
|
+
aria-controls={`${analysisId}-analysis`}
|
|
2175
|
+
onClick={() => {
|
|
2176
|
+
setDetail(showAnalysis ? null : "analysis");
|
|
2177
|
+
analysisReveal.revealAfterToggle(!showAnalysis);
|
|
2178
|
+
}}
|
|
2179
|
+
className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
|
|
2180
|
+
>
|
|
2181
|
+
<CollapseChevron
|
|
2182
|
+
open={showAnalysis}
|
|
2183
|
+
className="h-3.5 w-3.5"
|
|
2184
|
+
/>
|
|
2185
|
+
{diagnosis.report ? "Full analysis" : "Assessment details"}
|
|
2186
|
+
</button>
|
|
2187
|
+
)}
|
|
2188
|
+
{explanation && (
|
|
2189
|
+
<Tooltip
|
|
2190
|
+
content={
|
|
2191
|
+
explanation.status === "idle"
|
|
2192
|
+
? explanation.onGenerate
|
|
2193
|
+
? "Ask the agent to explain this assessment and its proposed next steps in plain language."
|
|
2194
|
+
: "Wait for the current agent request to finish before requesting an explanation."
|
|
2195
|
+
: explanation.status === "running"
|
|
2196
|
+
? "The agent is preparing an explanation. You can close this and return while it runs."
|
|
2197
|
+
: explanation.status === "error"
|
|
2198
|
+
? "View the explanation error and retry when the agent is available."
|
|
2199
|
+
: "Show the saved plain-language explanation. No new request is needed."
|
|
2200
|
+
}
|
|
2201
|
+
>
|
|
2202
|
+
<button
|
|
2203
|
+
type="button"
|
|
2204
|
+
aria-expanded={detail === "explanation"}
|
|
2205
|
+
aria-controls={`${analysisId}-explanation`}
|
|
2206
|
+
disabled={
|
|
2207
|
+
explanation.status === "idle" && !explanation.onGenerate
|
|
2208
|
+
}
|
|
2209
|
+
onClick={() => {
|
|
2210
|
+
const open = detail !== "explanation";
|
|
2211
|
+
setDetail(open ? "explanation" : null);
|
|
2212
|
+
analysisReveal.revealAfterToggle(open);
|
|
2213
|
+
if (open && explanation.status === "idle")
|
|
2214
|
+
explanation.onGenerate?.();
|
|
2215
|
+
}}
|
|
2216
|
+
className="inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-xs font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary disabled:opacity-50"
|
|
2217
|
+
>
|
|
2218
|
+
<HelpCircle className="h-3 w-3" />
|
|
2219
|
+
Explain simply
|
|
2220
|
+
</button>
|
|
2221
|
+
</Tooltip>
|
|
2222
|
+
)}
|
|
2223
|
+
{assessmentAction && (
|
|
2224
|
+
<div className="ml-auto">{assessmentAction}</div>
|
|
2225
|
+
)}
|
|
1623
2226
|
</div>
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
2227
|
+
<div id={analysisId} ref={analysisReveal.elementRef}>
|
|
2228
|
+
<div id={`${analysisId}-analysis`}>
|
|
2229
|
+
<Collapse open={detail === "analysis"} mountLazily>
|
|
2230
|
+
<div className="border-t border-theme-border/60 px-3 py-2">
|
|
2231
|
+
<p className="mb-2 text-xs text-theme-text-tertiary">
|
|
2232
|
+
Agent confidence:{" "}
|
|
2233
|
+
{diagnosis.confidence != null
|
|
2234
|
+
? confidenceLabel(diagnosis.confidence)
|
|
2235
|
+
: "not stated"}
|
|
2236
|
+
{diagnosis.confidence != null ? " · self-reported" : ""}
|
|
2237
|
+
</p>
|
|
2238
|
+
<AIMarkdown className="text-sm [overflow-wrap:anywhere] [&_h2:first-child]:mt-0 [&_h2]:mb-1.5 [&_h2]:mt-3 [&_h2]:text-xs [&_h2]:font-semibold [&_h2]:uppercase [&_h2]:tracking-wide [&_h2]:text-theme-text-tertiary [&_h3]:text-sm [&_li]:text-theme-text-secondary [&_p]:my-1.5 [&_p]:text-theme-text-secondary">
|
|
2239
|
+
{diagnosis.report}
|
|
2240
|
+
</AIMarkdown>
|
|
2241
|
+
{assessmentSources}
|
|
2242
|
+
</div>
|
|
2243
|
+
</Collapse>
|
|
2244
|
+
</div>
|
|
2245
|
+
<div id={`${analysisId}-explanation`}>
|
|
2246
|
+
<Collapse open={detail === "explanation"} mountLazily>
|
|
2247
|
+
<div className="border-t border-theme-border/60 px-3 py-2">
|
|
2248
|
+
{explanation?.status === "running" ? (
|
|
2249
|
+
<div
|
|
2250
|
+
role="status"
|
|
2251
|
+
className="flex items-center gap-2 py-2 text-sm text-theme-text-secondary"
|
|
2252
|
+
>
|
|
2253
|
+
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
|
2254
|
+
Explaining this assessment…
|
|
2255
|
+
</div>
|
|
2256
|
+
) : explanation?.status === "done" ? (
|
|
2257
|
+
<div className="flex items-start gap-2">
|
|
2258
|
+
<AIMarkdown className="min-w-0 flex-1 text-sm text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
2259
|
+
{explanation.text || ""}
|
|
2260
|
+
</AIMarkdown>
|
|
2261
|
+
<CopyButton
|
|
2262
|
+
text={explanation.text || ""}
|
|
2263
|
+
label="Copy explanation"
|
|
2264
|
+
/>
|
|
2265
|
+
</div>
|
|
2266
|
+
) : explanation?.status === "error" ? (
|
|
2267
|
+
<div
|
|
2268
|
+
role="alert"
|
|
2269
|
+
className="flex flex-wrap items-center gap-2 py-2 text-sm text-theme-text-secondary"
|
|
2270
|
+
>
|
|
2271
|
+
<span>
|
|
2272
|
+
{explanation.error ||
|
|
2273
|
+
"The agent did not return an explanation."}
|
|
2274
|
+
</span>
|
|
2275
|
+
{explanation.onGenerate && (
|
|
2276
|
+
<button
|
|
2277
|
+
type="button"
|
|
2278
|
+
onClick={explanation.onGenerate}
|
|
2279
|
+
className="rounded-md px-2 py-1 text-xs font-medium text-accent-text hover:bg-theme-hover"
|
|
2280
|
+
>
|
|
2281
|
+
Try again
|
|
2282
|
+
</button>
|
|
2283
|
+
)}
|
|
2284
|
+
</div>
|
|
2285
|
+
) : null}
|
|
2286
|
+
</div>
|
|
2287
|
+
</Collapse>
|
|
2288
|
+
</div>
|
|
2289
|
+
</div>
|
|
2290
|
+
</div>
|
|
2291
|
+
)}
|
|
1627
2292
|
|
|
1628
|
-
{
|
|
2293
|
+
{showConclusion && showDisclaimer && (
|
|
1629
2294
|
<div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
|
|
1630
2295
|
<ShieldCheck className="mt-0.5 h-3 w-3 shrink-0" />
|
|
1631
2296
|
<span>AI-generated — review before applying</span>
|
|
@@ -1635,30 +2300,124 @@ function DiagnosisResult({
|
|
|
1635
2300
|
);
|
|
1636
2301
|
}
|
|
1637
2302
|
|
|
1638
|
-
function AllClearCard({
|
|
1639
|
-
|
|
1640
|
-
|
|
2303
|
+
function AllClearCard({
|
|
2304
|
+
diagnosis,
|
|
2305
|
+
animate,
|
|
2306
|
+
showDisclaimer,
|
|
2307
|
+
coverageLimited,
|
|
2308
|
+
evidenceConflict,
|
|
2309
|
+
assessmentAction,
|
|
2310
|
+
assessmentSources,
|
|
2311
|
+
}: {
|
|
2312
|
+
diagnosis: Diagnosis;
|
|
2313
|
+
animate: boolean;
|
|
2314
|
+
showDisclaimer: boolean;
|
|
2315
|
+
coverageLimited: boolean;
|
|
2316
|
+
evidenceConflict: boolean;
|
|
2317
|
+
assessmentAction?: ReactNode;
|
|
2318
|
+
assessmentSources?: ReactNode;
|
|
2319
|
+
}) {
|
|
2320
|
+
const [showAnalysis, setShowAnalysis] = useState(false);
|
|
2321
|
+
const analysisReveal = useDisclosureReveal<HTMLDivElement>();
|
|
2322
|
+
const analysisId = useId();
|
|
2323
|
+
const report =
|
|
2324
|
+
diagnosis.report ||
|
|
2325
|
+
"The agent did not identify a problem in the evidence it checked.";
|
|
2326
|
+
const detailed = report.length > 320 || report.split("\n").length > 2;
|
|
2327
|
+
const summary = detailed
|
|
2328
|
+
? "The agent found no active problem in the evidence it reviewed."
|
|
2329
|
+
: report;
|
|
1641
2330
|
return (
|
|
1642
|
-
<div className=
|
|
2331
|
+
<div className={`mt-3 space-y-2 ${animate ? "animate-result-in" : ""}`}>
|
|
1643
2332
|
<div
|
|
1644
|
-
className=
|
|
1645
|
-
|
|
2333
|
+
className={`rounded-lg border p-3 ${
|
|
2334
|
+
evidenceConflict
|
|
2335
|
+
? "border-amber-500/40 bg-amber-500/5"
|
|
2336
|
+
: coverageLimited
|
|
2337
|
+
? "border-amber-500/30 bg-amber-500/5"
|
|
2338
|
+
: "border-emerald-500/30 bg-emerald-500/5"
|
|
2339
|
+
}`}
|
|
1646
2340
|
>
|
|
1647
2341
|
<div className="mb-1 flex items-center justify-between gap-2">
|
|
1648
|
-
<div
|
|
1649
|
-
|
|
1650
|
-
|
|
2342
|
+
<div
|
|
2343
|
+
className={`flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide ${
|
|
2344
|
+
evidenceConflict || coverageLimited
|
|
2345
|
+
? "text-amber-500"
|
|
2346
|
+
: "text-emerald-500"
|
|
2347
|
+
}`}
|
|
2348
|
+
>
|
|
2349
|
+
{evidenceConflict || coverageLimited ? (
|
|
2350
|
+
<AlertTriangle className="h-3.5 w-3.5" />
|
|
2351
|
+
) : (
|
|
2352
|
+
<CheckCircle2 className="h-3.5 w-3.5" />
|
|
2353
|
+
)}
|
|
2354
|
+
{evidenceConflict
|
|
2355
|
+
? "Assessment conflicts with captured evidence"
|
|
2356
|
+
: coverageLimited
|
|
2357
|
+
? "No problem identified in available evidence"
|
|
2358
|
+
: "No problem found in checked evidence"}
|
|
1651
2359
|
</div>
|
|
1652
|
-
<CopyButton text={
|
|
2360
|
+
<CopyButton text={report} label="Copy assessment" />
|
|
1653
2361
|
</div>
|
|
1654
2362
|
<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">
|
|
1655
|
-
{
|
|
2363
|
+
{summary}
|
|
1656
2364
|
</AIMarkdown>
|
|
2365
|
+
{evidenceConflict ? (
|
|
2366
|
+
<p className="mt-2 text-xs text-theme-text-secondary">
|
|
2367
|
+
Radar also captured evidence of an active problem. Review that
|
|
2368
|
+
evidence before treating the agent's conclusion as an
|
|
2369
|
+
all-clear.
|
|
2370
|
+
</p>
|
|
2371
|
+
) : coverageLimited ? (
|
|
2372
|
+
<p className="mt-2 text-xs text-theme-text-secondary">
|
|
2373
|
+
Evidence coverage is limited. Review the limitations in Evidence
|
|
2374
|
+
before treating this as an all-clear.
|
|
2375
|
+
</p>
|
|
2376
|
+
) : null}
|
|
1657
2377
|
</div>
|
|
1658
|
-
|
|
1659
|
-
<
|
|
1660
|
-
|
|
1661
|
-
|
|
2378
|
+
{detailed || assessmentAction || assessmentSources ? (
|
|
2379
|
+
<div>
|
|
2380
|
+
<div
|
|
2381
|
+
className="flex flex-wrap items-center gap-3 pt-2"
|
|
2382
|
+
data-assessment-actions
|
|
2383
|
+
>
|
|
2384
|
+
{(detailed || assessmentSources) && (
|
|
2385
|
+
<button
|
|
2386
|
+
type="button"
|
|
2387
|
+
aria-expanded={showAnalysis}
|
|
2388
|
+
aria-controls={analysisId}
|
|
2389
|
+
onClick={() => {
|
|
2390
|
+
setShowAnalysis(!showAnalysis);
|
|
2391
|
+
analysisReveal.revealAfterToggle(!showAnalysis);
|
|
2392
|
+
}}
|
|
2393
|
+
className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
|
|
2394
|
+
>
|
|
2395
|
+
<CollapseChevron open={showAnalysis} className="h-3.5 w-3.5" />
|
|
2396
|
+
{detailed ? "Full analysis" : "Assessment details"}
|
|
2397
|
+
</button>
|
|
2398
|
+
)}
|
|
2399
|
+
{assessmentAction && (
|
|
2400
|
+
<div className="ml-auto">{assessmentAction}</div>
|
|
2401
|
+
)}
|
|
2402
|
+
</div>
|
|
2403
|
+
<div id={analysisId} ref={analysisReveal.elementRef}>
|
|
2404
|
+
<Collapse open={showAnalysis}>
|
|
2405
|
+
<div className="border-t border-theme-border/60 px-3 py-2">
|
|
2406
|
+
<AIMarkdown className="text-sm [overflow-wrap:anywhere] [&_p]:my-1.5 [&_p]:text-theme-text-secondary [&_p:first-child]:mt-0 [&_p:last-child]:mb-0">
|
|
2407
|
+
{detailed ? report : ""}
|
|
2408
|
+
</AIMarkdown>
|
|
2409
|
+
{assessmentSources}
|
|
2410
|
+
</div>
|
|
2411
|
+
</Collapse>
|
|
2412
|
+
</div>
|
|
2413
|
+
</div>
|
|
2414
|
+
) : null}
|
|
2415
|
+
{showDisclaimer ? (
|
|
2416
|
+
<div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
|
|
2417
|
+
<ShieldCheck className="mt-0.5 h-3 w-3 shrink-0" />
|
|
2418
|
+
<span>AI-generated — verify if symptoms persist</span>
|
|
2419
|
+
</div>
|
|
2420
|
+
) : null}
|
|
1662
2421
|
</div>
|
|
1663
2422
|
);
|
|
1664
2423
|
}
|
|
@@ -1666,22 +2425,25 @@ function AllClearCard({ diagnosis }: { diagnosis: Diagnosis }) {
|
|
|
1666
2425
|
// The agent investigated but couldn't determine an answer. A distinct, honest
|
|
1667
2426
|
// state — neutral (not the alarming amber root cause, not the reassuring emerald
|
|
1668
2427
|
// all-clear) — so "I couldn't tell" never reads as "you're fine."
|
|
1669
|
-
function InconclusiveCard({
|
|
2428
|
+
function InconclusiveCard({
|
|
2429
|
+
diagnosis,
|
|
2430
|
+
animate,
|
|
2431
|
+
}: {
|
|
2432
|
+
diagnosis: Diagnosis;
|
|
2433
|
+
animate: boolean;
|
|
2434
|
+
}) {
|
|
1670
2435
|
const text =
|
|
1671
2436
|
diagnosis.report ||
|
|
1672
|
-
"The investigation couldn't reach a clear conclusion — some
|
|
2437
|
+
"The investigation couldn't reach a clear conclusion — some information was unavailable or the evidence was ambiguous.";
|
|
1673
2438
|
return (
|
|
1674
|
-
<div className=
|
|
1675
|
-
<div
|
|
1676
|
-
className="rounded-lg border border-theme-border bg-theme-elevated p-3 animate-verdict-reveal"
|
|
1677
|
-
style={{ "--glow": "rgb(100 116 139)" } as React.CSSProperties}
|
|
1678
|
-
>
|
|
2439
|
+
<div className={`mt-3 space-y-2 ${animate ? "animate-result-in" : ""}`}>
|
|
2440
|
+
<div className="rounded-lg border border-theme-border bg-theme-elevated p-3">
|
|
1679
2441
|
<div className="mb-1 flex items-center justify-between gap-2">
|
|
1680
2442
|
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-theme-text-secondary">
|
|
1681
2443
|
<HelpCircle className="h-3.5 w-3.5" />
|
|
1682
2444
|
Couldn't determine
|
|
1683
2445
|
</div>
|
|
1684
|
-
<CopyButton text={text} />
|
|
2446
|
+
<CopyButton text={text} label="Copy assessment" />
|
|
1685
2447
|
</div>
|
|
1686
2448
|
<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">
|
|
1687
2449
|
{text}
|
|
@@ -1690,7 +2452,8 @@ function InconclusiveCard({ diagnosis }: { diagnosis: Diagnosis }) {
|
|
|
1690
2452
|
<div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
|
|
1691
2453
|
<ShieldCheck className="mt-0.5 h-3 w-3 shrink-0" />
|
|
1692
2454
|
<span>
|
|
1693
|
-
Try a follow-up with more
|
|
2455
|
+
Try a follow-up with more context, or investigate again after
|
|
2456
|
+
addressing any errors shown in Activity.
|
|
1694
2457
|
</span>
|
|
1695
2458
|
</div>
|
|
1696
2459
|
</div>
|
|
@@ -1699,17 +2462,25 @@ function InconclusiveCard({ diagnosis }: { diagnosis: Diagnosis }) {
|
|
|
1699
2462
|
|
|
1700
2463
|
// A follow-up reply: the agent answering a question, not re-diagnosing. Plain
|
|
1701
2464
|
// neutral block — no root-cause anchor, no remediation/apply.
|
|
1702
|
-
function FollowupAnswer({
|
|
2465
|
+
function FollowupAnswer({
|
|
2466
|
+
diagnosis,
|
|
2467
|
+
animate,
|
|
2468
|
+
}: {
|
|
2469
|
+
diagnosis: Diagnosis;
|
|
2470
|
+
animate: boolean;
|
|
2471
|
+
}) {
|
|
1703
2472
|
const text = diagnosis.report || diagnosis.rootCause;
|
|
1704
2473
|
if (!text) return null;
|
|
1705
2474
|
return (
|
|
1706
|
-
<div
|
|
2475
|
+
<div
|
|
2476
|
+
className={`mt-1 rounded-lg border border-theme-border bg-theme-elevated p-3 ${animate ? "animate-result-in" : ""}`}
|
|
2477
|
+
>
|
|
1707
2478
|
<div className="mb-1.5 flex items-center justify-between gap-2">
|
|
1708
2479
|
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
1709
2480
|
<Sparkles className="h-3.5 w-3.5 text-accent" />
|
|
1710
2481
|
Answer
|
|
1711
2482
|
</div>
|
|
1712
|
-
<CopyButton text={text} />
|
|
2483
|
+
<CopyButton text={text} label="Copy answer" />
|
|
1713
2484
|
</div>
|
|
1714
2485
|
<AIMarkdown className="text-sm [overflow-wrap:anywhere] [&_code]:font-normal [&_h2:first-child]:mt-0 [&_h2]:mb-1.5 [&_h2]:mt-3 [&_h2]:text-xs [&_h2]:font-semibold [&_h2]:uppercase [&_h2]:tracking-wide [&_h2]:text-theme-text-tertiary [&_h3]:text-sm [&_li]:text-theme-text-secondary [&_p]:my-1.5 [&_p]:text-theme-text-secondary [&_p:first-child]:mt-0">
|
|
1715
2486
|
{text}
|
|
@@ -1718,88 +2489,149 @@ function FollowupAnswer({ diagnosis }: { diagnosis: Diagnosis }) {
|
|
|
1718
2489
|
);
|
|
1719
2490
|
}
|
|
1720
2491
|
|
|
1721
|
-
// A done turn that produced no renderable
|
|
2492
|
+
// A done turn that produced no renderable result at all (empty diagnosis, no
|
|
1722
2493
|
// narration). Without this the turn would render blank — which reads as "the tool
|
|
1723
2494
|
// broke." Make the dead-end explicit and point at the recovery (a follow-up).
|
|
1724
|
-
function EmptyResult() {
|
|
2495
|
+
function EmptyResult({ animate }: { animate: boolean }) {
|
|
1725
2496
|
return (
|
|
1726
|
-
<div
|
|
2497
|
+
<div
|
|
2498
|
+
className={`mt-1 flex items-start gap-2 rounded-lg border border-theme-border bg-theme-elevated p-3 text-sm text-theme-text-secondary ${animate ? "animate-result-in" : ""}`}
|
|
2499
|
+
>
|
|
1727
2500
|
<ShieldCheck className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
1728
2501
|
<span>
|
|
1729
|
-
The investigation finished without a clear
|
|
1730
|
-
question, or
|
|
2502
|
+
The investigation finished without a clear conclusion. Try a follow-up
|
|
2503
|
+
question, or investigate again.
|
|
1731
2504
|
</span>
|
|
1732
2505
|
</div>
|
|
1733
2506
|
);
|
|
1734
2507
|
}
|
|
1735
2508
|
|
|
1736
|
-
// The result of an apply turn
|
|
1737
|
-
//
|
|
2509
|
+
// The result of an apply turn. Mutation truth comes from Radar write-tool
|
|
2510
|
+
// results, not the agent's prose or process exit. Missing outcome metadata is
|
|
2511
|
+
// therefore fail-closed as unknown; only explicit confirmation renders green.
|
|
1738
2512
|
function ApplyOutcomeCard({
|
|
1739
2513
|
diagnosis,
|
|
2514
|
+
error,
|
|
2515
|
+
applyOutcome,
|
|
1740
2516
|
onCheckStatus,
|
|
2517
|
+
animate,
|
|
1741
2518
|
}: {
|
|
1742
|
-
diagnosis: Diagnosis;
|
|
2519
|
+
diagnosis: Diagnosis | null;
|
|
2520
|
+
error?: string | null;
|
|
2521
|
+
applyOutcome?: ApplyMutationOutcome;
|
|
1743
2522
|
onCheckStatus?: () => void;
|
|
2523
|
+
animate: boolean;
|
|
1744
2524
|
}) {
|
|
1745
|
-
const outcome = diagnosis
|
|
2525
|
+
const outcome = diagnosis?.report || diagnosis?.rootCause;
|
|
2526
|
+
const status = applyOutcome ?? "unknown";
|
|
2527
|
+
const confirmed = status === "confirmed";
|
|
2528
|
+
const failed = status === "failed";
|
|
2529
|
+
const heading = confirmed
|
|
2530
|
+
? "Applied"
|
|
2531
|
+
: failed
|
|
2532
|
+
? "Not applied"
|
|
2533
|
+
: "Outcome unknown";
|
|
2534
|
+
const detail = error || outcome;
|
|
2535
|
+
const fallbackDetail = confirmed
|
|
2536
|
+
? "Radar confirmed that the change was applied. Check the current state to confirm its effect."
|
|
2537
|
+
: failed
|
|
2538
|
+
? "Radar could not confirm that the change was applied. Review Activity before retrying."
|
|
2539
|
+
: "Radar could not confirm whether the change was applied. Check the current state before retrying.";
|
|
2540
|
+
const containerClass = confirmed
|
|
2541
|
+
? "border-emerald-500/30 bg-emerald-500/5"
|
|
2542
|
+
: failed
|
|
2543
|
+
? "border-red-500/30 bg-red-500/5"
|
|
2544
|
+
: "border-amber-500/40 bg-amber-500/10";
|
|
2545
|
+
const accentClass = confirmed
|
|
2546
|
+
? "text-emerald-500"
|
|
2547
|
+
: failed
|
|
2548
|
+
? "text-red-400"
|
|
2549
|
+
: "text-amber-400";
|
|
2550
|
+
const buttonClass = confirmed
|
|
2551
|
+
? "border-emerald-500/40 text-emerald-500 hover:bg-emerald-500/10"
|
|
2552
|
+
: "border-amber-500/40 text-amber-400 hover:bg-amber-500/10";
|
|
2553
|
+
const provenance = confirmed
|
|
2554
|
+
? error
|
|
2555
|
+
? "Radar confirmed the change — the agent report is incomplete"
|
|
2556
|
+
: "Radar confirmed the change was applied"
|
|
2557
|
+
: failed
|
|
2558
|
+
? "Radar did not confirm that the change was applied"
|
|
2559
|
+
: "Radar cannot confirm whether the change was applied";
|
|
1746
2560
|
return (
|
|
1747
|
-
<div className=
|
|
1748
|
-
<div className=
|
|
2561
|
+
<div className={`mt-3 space-y-2 ${animate ? "animate-result-in" : ""}`}>
|
|
2562
|
+
<div className={`rounded-lg border p-3 ${containerClass}`}>
|
|
1749
2563
|
<div className="mb-1 flex items-center justify-between gap-2">
|
|
1750
|
-
<div
|
|
1751
|
-
|
|
1752
|
-
|
|
2564
|
+
<div
|
|
2565
|
+
className={`flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide ${accentClass}`}
|
|
2566
|
+
>
|
|
2567
|
+
{confirmed ? (
|
|
2568
|
+
<CheckCircle2 className="h-3.5 w-3.5" />
|
|
2569
|
+
) : (
|
|
2570
|
+
<AlertTriangle className="h-3.5 w-3.5" />
|
|
2571
|
+
)}
|
|
2572
|
+
{heading}
|
|
1753
2573
|
</div>
|
|
1754
|
-
{
|
|
2574
|
+
{detail && <CopyButton text={detail} label="Copy apply result" />}
|
|
1755
2575
|
</div>
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
2576
|
+
<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">
|
|
2577
|
+
{detail || fallbackDetail}
|
|
2578
|
+
</AIMarkdown>
|
|
2579
|
+
{!confirmed && !failed && (
|
|
2580
|
+
<div className="mt-2 flex items-start gap-1.5 text-xs font-medium text-amber-300">
|
|
2581
|
+
<RefreshCw className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
2582
|
+
<span>
|
|
2583
|
+
Check the current state before trying to apply this change again.
|
|
2584
|
+
</span>
|
|
2585
|
+
</div>
|
|
1760
2586
|
)}
|
|
1761
|
-
{onCheckStatus && (
|
|
2587
|
+
{onCheckStatus && !failed && (
|
|
1762
2588
|
<button
|
|
2589
|
+
type="button"
|
|
1763
2590
|
onClick={onCheckStatus}
|
|
1764
|
-
className=
|
|
2591
|
+
className={`mt-3 flex w-full items-center justify-center gap-1.5 rounded-lg border py-2 text-sm font-medium ${buttonClass}`}
|
|
1765
2592
|
>
|
|
1766
2593
|
<RefreshCw className="h-4 w-4" />
|
|
1767
|
-
Check status
|
|
2594
|
+
Check current status
|
|
1768
2595
|
</button>
|
|
1769
2596
|
)}
|
|
1770
2597
|
</div>
|
|
1771
2598
|
<div className="flex items-center gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
|
|
1772
2599
|
<ShieldCheck className="h-3 w-3 shrink-0" />
|
|
1773
|
-
<span className="truncate">
|
|
1774
|
-
Applied by AI — verify the change took effect
|
|
1775
|
-
</span>
|
|
2600
|
+
<span className="truncate">{provenance}</span>
|
|
1776
2601
|
</div>
|
|
1777
2602
|
</div>
|
|
1778
2603
|
);
|
|
1779
2604
|
}
|
|
1780
2605
|
|
|
1781
|
-
function CopyButton({ text }: { text: string }) {
|
|
2606
|
+
function CopyButton({ text, label }: { text: string; label: string }) {
|
|
1782
2607
|
const [copied, setCopied] = useState(false);
|
|
1783
2608
|
return (
|
|
1784
|
-
<
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
setTimeout(() => setCopied(false), 1200);
|
|
1789
|
-
}}
|
|
1790
|
-
className="shrink-0 rounded p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
1791
|
-
aria-label="Copy"
|
|
2609
|
+
<Tooltip
|
|
2610
|
+
content={copied ? "Copied" : label}
|
|
2611
|
+
delay={100}
|
|
2612
|
+
wrapperClassName="shrink-0"
|
|
1792
2613
|
>
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2614
|
+
<button
|
|
2615
|
+
onClick={() => {
|
|
2616
|
+
navigator.clipboard?.writeText(text);
|
|
2617
|
+
setCopied(true);
|
|
2618
|
+
setTimeout(() => setCopied(false), 1200);
|
|
2619
|
+
}}
|
|
2620
|
+
className="shrink-0 rounded p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
2621
|
+
aria-label={copied ? `${label} — copied` : label}
|
|
2622
|
+
aria-live="polite"
|
|
2623
|
+
>
|
|
2624
|
+
{copied ? (
|
|
2625
|
+
<Check className="h-3.5 w-3.5 text-emerald-400" />
|
|
2626
|
+
) : (
|
|
2627
|
+
<Copy className="h-3.5 w-3.5" />
|
|
2628
|
+
)}
|
|
2629
|
+
</button>
|
|
2630
|
+
</Tooltip>
|
|
1799
2631
|
);
|
|
1800
2632
|
}
|
|
1801
2633
|
|
|
1802
|
-
function prettyTool(tool: string): string {
|
|
2634
|
+
export function prettyTool(tool: string): string {
|
|
1803
2635
|
return tool.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1804
2636
|
}
|
|
1805
2637
|
|
|
@@ -1811,42 +2643,6 @@ function confidenceLabel(c: number): string {
|
|
|
1811
2643
|
return "Low";
|
|
1812
2644
|
}
|
|
1813
2645
|
|
|
1814
|
-
// Confidence shown as a band + three discrete pips (Low=1, Med=2, High=3 filled).
|
|
1815
|
-
// Deliberately discrete, NOT a continuous bar: a filled fraction reads as a precise
|
|
1816
|
-
// percentage, which is exactly the false calibration `confidenceLabel` exists to
|
|
1817
|
-
// avoid (an LLM's two-sig-fig confidence isn't that precise). Accent-toned so it
|
|
1818
|
-
// reads as "trust in the analysis," not problem severity.
|
|
1819
|
-
function ConfidenceMeter({ value }: { value: number }) {
|
|
1820
|
-
const band = confidenceLabel(value);
|
|
1821
|
-
const filled = band === "High" ? 3 : band === "Medium" ? 2 : 1;
|
|
1822
|
-
return (
|
|
1823
|
-
<span className="flex items-center gap-1.5">
|
|
1824
|
-
<span className="text-[11px] text-theme-text-tertiary">
|
|
1825
|
-
{band} confidence
|
|
1826
|
-
</span>
|
|
1827
|
-
<span className="flex items-center gap-0.5" aria-hidden>
|
|
1828
|
-
{[0, 1, 2].map((i) => (
|
|
1829
|
-
<span
|
|
1830
|
-
key={i}
|
|
1831
|
-
className={`h-1 w-2.5 rounded-full ${i < filled ? "bg-accent" : "bg-theme-base"}`}
|
|
1832
|
-
/>
|
|
1833
|
-
))}
|
|
1834
|
-
</span>
|
|
1835
|
-
</span>
|
|
1836
|
-
);
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
// Shown when the model returned no confidence at all — so "unknown confidence"
|
|
1840
|
-
// is visible rather than silently absent (which looks identical to high confidence
|
|
1841
|
-
// minus the badge) on a trust-bearing surface.
|
|
1842
|
-
function ConfidenceUnstated() {
|
|
1843
|
-
return (
|
|
1844
|
-
<span className="text-[11px] text-theme-text-tertiary">
|
|
1845
|
-
Confidence not stated
|
|
1846
|
-
</span>
|
|
1847
|
-
);
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
2646
|
// LLMs occasionally open a ```fence mid-line ("run this: ```bash kubectl …") or
|
|
1851
2647
|
// put the command on the same line as the ```lang marker. GFM then won't parse
|
|
1852
2648
|
// it as a fence — it leaks the literal ``` and renders an empty code box. Coerce
|