@skyhook-io/radar-app 1.13.0 → 1.13.2
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 +55 -9
- package/src/RadarApp.tsx +3 -3
- package/src/api/diagnose.test.ts +268 -0
- package/src/api/diagnose.ts +117 -14
- package/src/components/diagnose/AISettings.tsx +10 -5
- package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
- package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
- package/src/components/diagnose/DiagnoseContext.tsx +228 -65
- package/src/components/diagnose/DiagnoseSurface.test.tsx +317 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +621 -132
- package/src/components/diagnose/Home.test.tsx +293 -0
- package/src/components/diagnose/Home.tsx +307 -108
- 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 +1911 -338
- 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 +63 -0
- package/src/components/diagnose/investigationExplanation.ts +22 -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 +1340 -536
- 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/execution/BatchExecutionView.render.test.tsx +35 -0
- package/src/components/execution/BatchExecutionView.tsx +2 -3
- package/src/components/execution/execution-definition.test.ts +19 -0
- package/src/components/execution/execution-definition.ts +2 -0
- package/src/components/gitops/GitOpsView.tsx +25 -3
- package/src/components/home/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/nav/navigation.test.ts +26 -0
- package/src/components/nav/navigation.ts +10 -0
- 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,7 +427,10 @@ 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;
|
|
433
|
+
actor?: string;
|
|
408
434
|
timeline: TimelineItem[];
|
|
409
435
|
diagnosis: Diagnosis | null;
|
|
410
436
|
error: string | null;
|
|
@@ -412,11 +438,21 @@ export type Turn = {
|
|
|
412
438
|
// apply turns execute the recommended fix (write tools) — they report an
|
|
413
439
|
// outcome, not a root cause, so the UI frames them differently.
|
|
414
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;
|
|
415
451
|
};
|
|
416
452
|
|
|
417
453
|
// TimelineItem is one ordered transcript entry: agent reasoning, or a tool call.
|
|
418
|
-
type TimelineItem =
|
|
419
|
-
| { kind: "thinking"; text: string }
|
|
454
|
+
export type TimelineItem =
|
|
455
|
+
| { kind: "thinking"; text: string; animate?: boolean }
|
|
420
456
|
| {
|
|
421
457
|
kind: "tool";
|
|
422
458
|
id: string;
|
|
@@ -425,25 +461,66 @@ type TimelineItem =
|
|
|
425
461
|
ms?: number;
|
|
426
462
|
summary?: string;
|
|
427
463
|
result?: string;
|
|
464
|
+
evidenceRef?: string;
|
|
465
|
+
radarEvidence?: boolean;
|
|
428
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;
|
|
429
473
|
};
|
|
430
474
|
|
|
431
475
|
export function appendThinking(
|
|
432
476
|
prev: TimelineItem[],
|
|
433
477
|
text: string,
|
|
478
|
+
animate = true,
|
|
434
479
|
): TimelineItem[] {
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
+
});
|
|
440
516
|
}
|
|
441
|
-
return
|
|
517
|
+
return next;
|
|
442
518
|
}
|
|
443
519
|
|
|
444
520
|
export function upsertTool(
|
|
445
521
|
prev: TimelineItem[],
|
|
446
522
|
step: DiagnoseStep,
|
|
523
|
+
animate = true,
|
|
447
524
|
): TimelineItem[] {
|
|
448
525
|
const i = prev.findIndex((it) => it.kind === "tool" && it.id === step.id);
|
|
449
526
|
if (i >= 0) {
|
|
@@ -456,194 +533,171 @@ export function upsertTool(
|
|
|
456
533
|
kind: "tool",
|
|
457
534
|
tool: step.tool || cur.tool,
|
|
458
535
|
summary: step.summary || cur.summary,
|
|
536
|
+
animate: cur.animate === true || animate,
|
|
459
537
|
};
|
|
460
538
|
return next;
|
|
461
539
|
}
|
|
462
|
-
return [...prev, { kind: "tool", ...step }];
|
|
540
|
+
return [...prev, { kind: "tool", ...step, animate }];
|
|
463
541
|
}
|
|
464
542
|
|
|
465
543
|
export function TurnView({
|
|
466
544
|
turn,
|
|
467
|
-
synthLabel,
|
|
468
|
-
reveal = "full",
|
|
469
545
|
onApply,
|
|
470
|
-
|
|
546
|
+
onViewExplanation,
|
|
471
547
|
onCheckStatus,
|
|
472
548
|
onRetryDiagnosis,
|
|
473
|
-
|
|
549
|
+
hideConclusion = false,
|
|
550
|
+
turnIndex,
|
|
551
|
+
evidenceStepIds,
|
|
552
|
+
onViewEvidence,
|
|
553
|
+
sourceRevealRequest,
|
|
474
554
|
}: {
|
|
475
555
|
turn: Turn;
|
|
476
|
-
synthLabel?: string | null;
|
|
477
|
-
reveal?: "rca" | "full";
|
|
478
556
|
onApply?: (fix: string) => void;
|
|
479
|
-
|
|
557
|
+
onViewExplanation?: () => void;
|
|
480
558
|
onCheckStatus?: () => void;
|
|
481
559
|
onRetryDiagnosis?: () => void;
|
|
482
|
-
// In the maximized workspace the pinned turn's
|
|
560
|
+
// In the maximized workspace the pinned turn's conclusion renders in the side rail,
|
|
483
561
|
// so the transcript suppresses its own copy (reasoning + tool calls still show).
|
|
484
|
-
|
|
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
|
+
};
|
|
485
571
|
}) {
|
|
486
572
|
// A follow-up (a turn the user asked a question on) is a conversational reply,
|
|
487
573
|
// not a fresh diagnosis — render it as a plain answer, never the root-cause
|
|
488
574
|
// anchor or a remediation card.
|
|
489
|
-
const followup = !!turn.question && !turn.apply;
|
|
575
|
+
const followup = !!turn.question && !turn.apply && !turn.verify;
|
|
490
576
|
// Whether the done turn has anything for ResultCard to render — mirrors its
|
|
491
577
|
// branch order exactly (apply → followup → structured/healthy), since a followup
|
|
492
578
|
// ONLY ever renders FollowupAnswer (report/rootCause), never the remediation list.
|
|
493
579
|
// When false, TurnView shows the narration or an explicit empty note, not a blank.
|
|
494
580
|
const dx = turn.diagnosis;
|
|
495
|
-
const
|
|
496
|
-
?
|
|
497
|
-
?
|
|
581
|
+
const hasResult = dx
|
|
582
|
+
? followup
|
|
583
|
+
? !!(dx.report?.trim() || dx.rootCause?.trim()) // FollowupAnswer
|
|
498
584
|
: dx.healthy && !dx.rootCause
|
|
499
|
-
? true // AllClearCard
|
|
585
|
+
? true // AllClearCard
|
|
500
586
|
: dx.inconclusive && !dx.rootCause
|
|
501
587
|
? true // InconclusiveCard
|
|
502
|
-
:
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
(dx.remediation?.length ?? 0) > 0 ||
|
|
506
|
-
!!dx.report?.trim()
|
|
588
|
+
: !!dx.rootCause ||
|
|
589
|
+
(dx.remediation?.length ?? 0) > 0 ||
|
|
590
|
+
!!dx.report?.trim()
|
|
507
591
|
: false;
|
|
508
592
|
return (
|
|
509
593
|
<div className="space-y-2">
|
|
510
|
-
{turn.
|
|
511
|
-
<
|
|
512
|
-
|
|
513
|
-
|
|
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>
|
|
514
619
|
</div>
|
|
515
|
-
|
|
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}
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
))
|
|
516
634
|
)}
|
|
517
635
|
<Timeline
|
|
518
636
|
items={turn.timeline}
|
|
519
637
|
running={turn.status === "running"}
|
|
520
638
|
applyMode={turn.apply}
|
|
521
639
|
followup={followup}
|
|
522
|
-
|
|
640
|
+
turnIndex={turnIndex}
|
|
641
|
+
evidenceStepIds={evidenceStepIds}
|
|
642
|
+
onViewEvidence={onViewEvidence}
|
|
643
|
+
sourceRevealRequest={sourceRevealRequest}
|
|
523
644
|
/>
|
|
524
|
-
{turn.
|
|
525
|
-
|
|
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 ? (
|
|
526
655
|
<ResultCard
|
|
527
656
|
diagnosis={turn.diagnosis!}
|
|
528
657
|
onApply={onApply}
|
|
529
|
-
onAsk={onAsk}
|
|
530
|
-
apply={turn.apply}
|
|
531
658
|
followup={followup}
|
|
532
|
-
reveal={reveal}
|
|
533
659
|
onCheckStatus={onCheckStatus}
|
|
660
|
+
animate={turn.animateResult !== false}
|
|
534
661
|
/>
|
|
535
662
|
) : (
|
|
536
|
-
<EmptyResult />
|
|
663
|
+
<EmptyResult animate={turn.animateResult !== false} />
|
|
537
664
|
))}
|
|
538
|
-
{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 ? (
|
|
539
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">
|
|
540
675
|
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-400" />
|
|
541
676
|
<div className="flex min-w-0 flex-col gap-2">
|
|
542
|
-
<span className="whitespace-pre-wrap break-words">
|
|
677
|
+
<span className="whitespace-pre-wrap break-words">
|
|
678
|
+
{turn.error}
|
|
679
|
+
</span>
|
|
543
680
|
{onRetryDiagnosis && (
|
|
544
681
|
<button
|
|
545
682
|
type="button"
|
|
546
683
|
onClick={onRetryDiagnosis}
|
|
547
684
|
className="btn-brand self-start px-3 py-1 text-xs"
|
|
548
685
|
>
|
|
549
|
-
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
|
|
550
696
|
</button>
|
|
551
697
|
)}
|
|
552
698
|
</div>
|
|
553
699
|
</div>
|
|
554
|
-
)}
|
|
555
|
-
</div>
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// RunContextCard opens every investigation with what RADAR already knows — the
|
|
560
|
-
// health frame the server captured at run start. It renders instantly (no agent
|
|
561
|
-
// round-trip), so the agent's boot time reads as "context, then deepening"
|
|
562
|
-
// instead of dead air — and it anchors the verdict against Radar's own signal.
|
|
563
|
-
function healthLineTone(severity?: string): "unhealthy" | "degraded" | "alert" {
|
|
564
|
-
if (severity === "critical") return "unhealthy";
|
|
565
|
-
if (severity === "warning") return "degraded";
|
|
566
|
-
return "alert";
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
export function RunContextCard({ run }: { run: RunSummary }) {
|
|
570
|
-
const h = run.health;
|
|
571
|
-
const issueCount = h?.issueCount ?? 0;
|
|
572
|
-
const issues = h?.issues ?? [];
|
|
573
|
-
const findings = h?.auditFindings ?? [];
|
|
574
|
-
const lines: ReactNode[] = [];
|
|
575
|
-
if (issues.length > 0) {
|
|
576
|
-
// The actual issue rows Radar's engine flagged — the reason bolded, the
|
|
577
|
-
// engine's own detail sentence after it. Concrete beats a count.
|
|
578
|
-
for (const [i, line] of issues.entries()) {
|
|
579
|
-
lines.push(
|
|
580
|
-
<div key={`issue-${i}`} className="flex items-start gap-1.5">
|
|
581
|
-
<StatusDot
|
|
582
|
-
tone={healthLineTone(line.severity)}
|
|
583
|
-
className="mt-1 shrink-0"
|
|
584
|
-
/>
|
|
585
|
-
<span className="min-w-0">
|
|
586
|
-
<span className="font-medium text-theme-text-primary">
|
|
587
|
-
{line.reason}
|
|
588
|
-
</span>
|
|
589
|
-
{line.message ? <> — {line.message}</> : null}
|
|
590
|
-
</span>
|
|
591
|
-
</div>,
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
if (issueCount > issues.length) {
|
|
595
|
-
lines.push(
|
|
596
|
-
<div key="more" className="pl-3.5 text-theme-text-tertiary">
|
|
597
|
-
+{issueCount - issues.length} more active issue
|
|
598
|
-
{issueCount - issues.length === 1 ? "" : "s"}
|
|
599
|
-
</div>,
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
} else if (h?.health === "healthy") {
|
|
603
|
-
lines.push(
|
|
604
|
-
<div key="healthy" className="flex items-center gap-1.5">
|
|
605
|
-
<StatusDot tone="healthy" className="shrink-0" />
|
|
606
|
-
Reported healthy — 0 active issues
|
|
607
|
-
</div>,
|
|
608
|
-
);
|
|
609
|
-
} else if (h) {
|
|
610
|
-
lines.push(
|
|
611
|
-
<div key="none" className="flex items-center gap-1.5">
|
|
612
|
-
<StatusDot tone="unknown" className="shrink-0" />0 active issues
|
|
613
|
-
{h.health ? ` — status ${h.health}` : ""}
|
|
614
|
-
</div>,
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
for (const [i, f] of findings.entries()) {
|
|
618
|
-
lines.push(
|
|
619
|
-
<div key={`audit-${i}`} className="pl-3.5 text-theme-text-tertiary">
|
|
620
|
-
Audit: <span className="font-medium">{f.reason}</span>
|
|
621
|
-
{f.message ? <> — {f.message}</> : null}
|
|
622
|
-
</div>,
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
if ((h?.auditCount ?? 0) > findings.length && findings.length > 0) {
|
|
626
|
-
lines.push(
|
|
627
|
-
<div key="audit-more" className="pl-3.5 text-theme-text-tertiary">
|
|
628
|
-
+{h!.auditCount! - findings.length} more audit finding
|
|
629
|
-
{h!.auditCount! - findings.length === 1 ? "" : "s"}
|
|
630
|
-
</div>,
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
if (run.managedBy) {
|
|
634
|
-
lines.push(
|
|
635
|
-
<div key="managed" className="pl-3.5 text-theme-text-tertiary">
|
|
636
|
-
Managed by {run.managedBy}
|
|
637
|
-
</div>,
|
|
638
|
-
);
|
|
639
|
-
}
|
|
640
|
-
if (lines.length === 0) return null;
|
|
641
|
-
return (
|
|
642
|
-
<div className="rounded-md border border-theme-border/60 bg-theme-base/40 px-2.5 py-2">
|
|
643
|
-
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
644
|
-
Radar's read at start
|
|
645
|
-
</div>
|
|
646
|
-
<div className="space-y-1 text-xs text-theme-text-secondary">{lines}</div>
|
|
700
|
+
) : null}
|
|
647
701
|
</div>
|
|
648
702
|
);
|
|
649
703
|
}
|
|
@@ -697,7 +751,9 @@ function ConsentCardShell({
|
|
|
697
751
|
{title}
|
|
698
752
|
</div>
|
|
699
753
|
</div>
|
|
700
|
-
<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>
|
|
701
757
|
{bullets && bullets.length > 0 && (
|
|
702
758
|
<ul className="mt-2 space-y-1 text-xs text-theme-text-tertiary">
|
|
703
759
|
{bullets.map((b, i) => (
|
|
@@ -816,16 +872,16 @@ export function ConsentCard({
|
|
|
816
872
|
? [
|
|
817
873
|
agent === "claude" ? (
|
|
818
874
|
<>
|
|
819
|
-
Radar safeguards disable Claude's built-in tools and
|
|
820
|
-
MCP access to Radar's read-only investigation
|
|
821
|
-
Claude settings, hooks, and CLAUDE.md instructions
|
|
822
|
-
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.
|
|
823
879
|
</>
|
|
824
880
|
) : agent === "codex" ? (
|
|
825
881
|
<>
|
|
826
|
-
Radar safeguards exclude your Codex configuration and other
|
|
827
|
-
servers. Codex's sandboxed shell can still read files
|
|
828
|
-
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.
|
|
829
885
|
</>
|
|
830
886
|
) : (
|
|
831
887
|
<>
|
|
@@ -837,9 +893,9 @@ export function ConsentCard({
|
|
|
837
893
|
]
|
|
838
894
|
: [
|
|
839
895
|
<>
|
|
840
|
-
Radar cannot constrain the agent's other configured tools
|
|
841
|
-
MCP servers. They may access local files or the network and
|
|
842
|
-
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.
|
|
843
899
|
</>,
|
|
844
900
|
agent === "cursor-agent" ? (
|
|
845
901
|
CURSOR_FULL_LOCAL_WARNING
|
|
@@ -868,6 +924,7 @@ export function ApplyDialog({
|
|
|
868
924
|
onConfirm,
|
|
869
925
|
agentLabel,
|
|
870
926
|
resourceLabel,
|
|
927
|
+
context,
|
|
871
928
|
fix,
|
|
872
929
|
managedBy,
|
|
873
930
|
confidence,
|
|
@@ -877,6 +934,7 @@ export function ApplyDialog({
|
|
|
877
934
|
onConfirm: () => void;
|
|
878
935
|
agentLabel: string;
|
|
879
936
|
resourceLabel: string;
|
|
937
|
+
context: string;
|
|
880
938
|
fix?: string;
|
|
881
939
|
managedBy?: string; // GitOps/Helm owner of the resource, if any
|
|
882
940
|
confidence?: number;
|
|
@@ -895,8 +953,12 @@ export function ApplyDialog({
|
|
|
895
953
|
}, [open]);
|
|
896
954
|
const applyBlocked = !!managedBy && !acked;
|
|
897
955
|
return (
|
|
898
|
-
<DialogPortal
|
|
899
|
-
|
|
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">
|
|
900
962
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-amber-500/20">
|
|
901
963
|
<AlertTriangle className="h-5 w-5 text-amber-500" />
|
|
902
964
|
</div>
|
|
@@ -911,22 +973,34 @@ export function ApplyDialog({
|
|
|
911
973
|
</span>
|
|
912
974
|
.
|
|
913
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
|
+
)}
|
|
914
987
|
</div>
|
|
915
988
|
</div>
|
|
916
989
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
<div className="
|
|
920
|
-
<
|
|
921
|
-
|
|
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>
|
|
922
1000
|
</div>
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
</div>
|
|
927
|
-
)}
|
|
928
|
-
|
|
929
|
-
<div className="space-y-2 p-4">
|
|
1001
|
+
)}
|
|
1002
|
+
</div>
|
|
1003
|
+
<div className="shrink-0 space-y-2 p-4">
|
|
930
1004
|
{/* The star warning: when we KNOW a controller owns this resource, a live
|
|
931
1005
|
change reverts on the next reconcile — say so authoritatively. */}
|
|
932
1006
|
{managedBy && (
|
|
@@ -956,7 +1030,7 @@ export function ApplyDialog({
|
|
|
956
1030
|
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
957
1031
|
<span>
|
|
958
1032
|
The agent had <span className="font-medium">low confidence</span>{" "}
|
|
959
|
-
in this
|
|
1033
|
+
in this conclusion — consider asking a follow-up to verify before
|
|
960
1034
|
applying.
|
|
961
1035
|
</span>
|
|
962
1036
|
</div>
|
|
@@ -970,8 +1044,7 @@ export function ApplyDialog({
|
|
|
970
1044
|
</span>
|
|
971
1045
|
</div>
|
|
972
1046
|
</div>
|
|
973
|
-
|
|
974
|
-
<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">
|
|
975
1048
|
<button
|
|
976
1049
|
onClick={onClose}
|
|
977
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"
|
|
@@ -996,13 +1069,23 @@ export function Timeline({
|
|
|
996
1069
|
running,
|
|
997
1070
|
applyMode,
|
|
998
1071
|
followup,
|
|
999
|
-
|
|
1072
|
+
turnIndex,
|
|
1073
|
+
evidenceStepIds,
|
|
1074
|
+
onViewEvidence,
|
|
1075
|
+
sourceRevealRequest,
|
|
1000
1076
|
}: {
|
|
1001
1077
|
items: TimelineItem[];
|
|
1002
1078
|
running: boolean;
|
|
1003
1079
|
applyMode?: boolean;
|
|
1004
1080
|
followup?: boolean;
|
|
1005
|
-
|
|
1081
|
+
turnIndex?: number;
|
|
1082
|
+
evidenceStepIds?: ReadonlySet<string>;
|
|
1083
|
+
onViewEvidence?: (sourceId: string) => void;
|
|
1084
|
+
sourceRevealRequest?: {
|
|
1085
|
+
sourceId: string;
|
|
1086
|
+
requestId: number;
|
|
1087
|
+
excerpt?: InvestigationSourceExcerpt;
|
|
1088
|
+
};
|
|
1006
1089
|
}) {
|
|
1007
1090
|
const heading = applyMode
|
|
1008
1091
|
? "Applying fix"
|
|
@@ -1014,8 +1097,7 @@ export function Timeline({
|
|
|
1014
1097
|
const activeTool = [...items]
|
|
1015
1098
|
.reverse()
|
|
1016
1099
|
.find((it) => it.kind === "tool" && it.status !== "done") as
|
|
1017
|
-
|
|
1018
|
-
| undefined;
|
|
1100
|
+
Extract<TimelineItem, { kind: "tool" }> | undefined;
|
|
1019
1101
|
const runningLabel = applyMode
|
|
1020
1102
|
? "Applying the fix…"
|
|
1021
1103
|
: activeTool
|
|
@@ -1032,26 +1114,115 @@ export function Timeline({
|
|
|
1032
1114
|
{heading}
|
|
1033
1115
|
</div>
|
|
1034
1116
|
)}
|
|
1035
|
-
{items.map((it, i) =>
|
|
1036
|
-
it.kind === "thinking"
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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}
|
|
1044
1209
|
</AIMarkdown>
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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}
|
|
1055
1226
|
</div>
|
|
1056
1227
|
);
|
|
1057
1228
|
}
|
|
@@ -1099,17 +1270,6 @@ function RunningStatus({ label }: { label: string }) {
|
|
|
1099
1270
|
);
|
|
1100
1271
|
}
|
|
1101
1272
|
|
|
1102
|
-
// A staged "thinking" beat — a calm breathing dot + shimmering label. Used both in
|
|
1103
|
-
// the timeline (pre-verdict) and between the root-cause and remediation cards.
|
|
1104
|
-
function SynthBeat({ label }: { label: string }) {
|
|
1105
|
-
return (
|
|
1106
|
-
<div className="flex items-center gap-2 pt-1 text-xs animate-transcript-enter">
|
|
1107
|
-
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-accent animate-synth-pulse" />
|
|
1108
|
-
<span className="ai-shimmer font-medium">{label}…</span>
|
|
1109
|
-
</div>
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
1273
|
// Maps a running tool to a human verb so the status line reads as activity, not
|
|
1114
1274
|
// machinery. Falls back to the prettified tool name for anything unmapped.
|
|
1115
1275
|
function toolActivity(tool: string): string {
|
|
@@ -1126,69 +1286,194 @@ function toolActivity(tool: string): string {
|
|
|
1126
1286
|
return `${prettyTool(tool)}…`;
|
|
1127
1287
|
}
|
|
1128
1288
|
|
|
1129
|
-
function ToolRow({
|
|
1130
|
-
|
|
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);
|
|
1131
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(":", "")}`;
|
|
1132
1316
|
const hasDetail = !!(step.summary || step.result);
|
|
1317
|
+
useEffect(() => {
|
|
1318
|
+
if (revealRequestId !== undefined && hasDetail) setOpen(true);
|
|
1319
|
+
}, [hasDetail, revealRequestId]);
|
|
1133
1320
|
// Offer the rich dialog when the result is structured or non-trivial in size.
|
|
1134
1321
|
const richResult =
|
|
1135
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
|
+
);
|
|
1136
1370
|
return (
|
|
1137
|
-
<div
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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>
|
|
1146
1401
|
) : (
|
|
1147
|
-
<
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
{prettyTool(step.tool)}
|
|
1151
|
-
</span>
|
|
1152
|
-
{step.summary && (
|
|
1153
|
-
<span className="min-w-0 flex-1 truncate font-mono text-[11px] text-theme-text-tertiary">
|
|
1154
|
-
{compactArgs(step.summary)}
|
|
1155
|
-
</span>
|
|
1156
|
-
)}
|
|
1157
|
-
{step.ms != null && (
|
|
1158
|
-
<span className="ml-auto shrink-0 text-[11px] text-theme-text-tertiary">
|
|
1159
|
-
{step.ms}ms
|
|
1160
|
-
</span>
|
|
1161
|
-
)}
|
|
1162
|
-
{hasDetail && (
|
|
1163
|
-
<ChevronRight
|
|
1164
|
-
className={`h-3.5 w-3.5 shrink-0 text-theme-text-tertiary transition-transform ${open ? "rotate-90" : ""}`}
|
|
1165
|
-
/>
|
|
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>
|
|
1166
1405
|
)}
|
|
1167
|
-
|
|
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>
|
|
1168
1417
|
{hasDetail && (
|
|
1169
|
-
<
|
|
1170
|
-
<
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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}>
|
|
1179
1431
|
<button
|
|
1180
|
-
|
|
1181
|
-
|
|
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"
|
|
1182
1440
|
>
|
|
1183
|
-
<
|
|
1184
|
-
|
|
1441
|
+
<CollapseChevron
|
|
1442
|
+
open={argumentsOpen}
|
|
1443
|
+
className="h-3.5 w-3.5"
|
|
1444
|
+
/>
|
|
1445
|
+
{argumentsOpen ? "Hide arguments" : "Show arguments"}
|
|
1185
1446
|
</button>
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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>
|
|
1192
1477
|
)}
|
|
1193
1478
|
{step.result && (
|
|
1194
1479
|
<ToolResultDialog
|
|
@@ -1227,13 +1512,36 @@ function PayloadBlock({
|
|
|
1227
1512
|
text,
|
|
1228
1513
|
truncated,
|
|
1229
1514
|
action,
|
|
1515
|
+
sourceExcerpt,
|
|
1516
|
+
revealRequestId,
|
|
1230
1517
|
}: {
|
|
1231
1518
|
label: string;
|
|
1232
1519
|
text: string;
|
|
1233
1520
|
truncated?: boolean;
|
|
1234
1521
|
action?: ReactNode;
|
|
1522
|
+
sourceExcerpt?: InvestigationSourceExcerpt;
|
|
1523
|
+
revealRequestId?: number;
|
|
1235
1524
|
}) {
|
|
1236
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]);
|
|
1237
1545
|
return (
|
|
1238
1546
|
<div>
|
|
1239
1547
|
<div className="mb-0.5 flex items-center justify-between gap-2">
|
|
@@ -1242,13 +1550,30 @@ function PayloadBlock({
|
|
|
1242
1550
|
</span>
|
|
1243
1551
|
<div className="flex items-center gap-2">
|
|
1244
1552
|
{action}
|
|
1245
|
-
<CopyButton
|
|
1553
|
+
<CopyButton
|
|
1554
|
+
text={json ?? text}
|
|
1555
|
+
label={`Copy ${label.toLowerCase()}`}
|
|
1556
|
+
/>
|
|
1246
1557
|
</div>
|
|
1247
1558
|
</div>
|
|
1248
1559
|
<pre
|
|
1249
|
-
|
|
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]"}`}
|
|
1250
1562
|
>
|
|
1251
|
-
{
|
|
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
|
+
)}
|
|
1252
1577
|
</pre>
|
|
1253
1578
|
{truncated && (
|
|
1254
1579
|
<div className="mt-0.5 text-[10px] text-amber-500">
|
|
@@ -1337,7 +1662,7 @@ function ToolResultDialog({
|
|
|
1337
1662
|
/>
|
|
1338
1663
|
</div>
|
|
1339
1664
|
)}
|
|
1340
|
-
<CopyButton text={display} />
|
|
1665
|
+
<CopyButton text={display} label="Copy tool result" />
|
|
1341
1666
|
</div>
|
|
1342
1667
|
</div>
|
|
1343
1668
|
{html ? (
|
|
@@ -1362,18 +1687,6 @@ function safeYaml(value: unknown): string {
|
|
|
1362
1687
|
}
|
|
1363
1688
|
}
|
|
1364
1689
|
|
|
1365
|
-
// Collapse — the Radar-standard expand/collapse motion (grid-template-rows
|
|
1366
|
-
// 0fr↔1fr) used across issue rows. Children stay mounted so close animates too.
|
|
1367
|
-
function Collapse({ open, children }: { open: boolean; children: ReactNode }) {
|
|
1368
|
-
return (
|
|
1369
|
-
<div
|
|
1370
|
-
className={`issue-details-motion ${open ? "issue-details-motion-open" : ""}`}
|
|
1371
|
-
>
|
|
1372
|
-
<div className="overflow-hidden">{children}</div>
|
|
1373
|
-
</div>
|
|
1374
|
-
);
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
1690
|
function compactArgs(raw: string): string {
|
|
1378
1691
|
try {
|
|
1379
1692
|
const o = JSON.parse(raw);
|
|
@@ -1388,71 +1701,271 @@ function compactArgs(raw: string): string {
|
|
|
1388
1701
|
export function ResultCard({
|
|
1389
1702
|
diagnosis,
|
|
1390
1703
|
onApply,
|
|
1391
|
-
|
|
1704
|
+
explanation,
|
|
1392
1705
|
apply,
|
|
1706
|
+
applyOutcome,
|
|
1393
1707
|
followup,
|
|
1394
|
-
|
|
1708
|
+
section = "full",
|
|
1395
1709
|
onCheckStatus,
|
|
1710
|
+
animate = true,
|
|
1711
|
+
showDisclaimer = true,
|
|
1712
|
+
coverageLimited = false,
|
|
1713
|
+
evidenceConflict = false,
|
|
1714
|
+
compactActions = false,
|
|
1715
|
+
assessmentAction,
|
|
1716
|
+
assessmentSources,
|
|
1717
|
+
actionNotice,
|
|
1396
1718
|
}: {
|
|
1397
1719
|
diagnosis: Diagnosis;
|
|
1398
1720
|
onApply?: (fix: string) => void;
|
|
1399
|
-
|
|
1721
|
+
explanation?: AssessmentExplanation;
|
|
1400
1722
|
apply?: boolean;
|
|
1723
|
+
applyOutcome?: ApplyMutationOutcome;
|
|
1401
1724
|
followup?: boolean;
|
|
1402
|
-
|
|
1725
|
+
section?: "full" | "conclusion" | "actions";
|
|
1403
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;
|
|
1404
1740
|
}) {
|
|
1405
|
-
// Apply turns report
|
|
1406
|
-
//
|
|
1741
|
+
// Apply turns report mutation truth, not a diagnosis. The outcome-specific
|
|
1742
|
+
// card decides whether that truth is confirmed, failed, or still unknown.
|
|
1407
1743
|
if (apply)
|
|
1408
1744
|
return (
|
|
1409
|
-
<ApplyOutcomeCard
|
|
1745
|
+
<ApplyOutcomeCard
|
|
1746
|
+
diagnosis={diagnosis}
|
|
1747
|
+
applyOutcome={applyOutcome}
|
|
1748
|
+
onCheckStatus={onCheckStatus}
|
|
1749
|
+
animate={animate}
|
|
1750
|
+
/>
|
|
1410
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} />;
|
|
1411
1757
|
if (diagnosis.healthy && !diagnosis.rootCause)
|
|
1412
|
-
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
|
+
);
|
|
1413
1769
|
// Couldn't-determine is its own honest state — never a confident all-clear, never
|
|
1414
1770
|
// the alarming root-cause anchor.
|
|
1415
1771
|
if (diagnosis.inconclusive && !diagnosis.rootCause)
|
|
1416
|
-
return
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
+
);
|
|
1420
1783
|
// A turn with no structured root cause and no remediation (e.g. "looks healthy",
|
|
1421
1784
|
// or a clarifying question) is not a diagnosis — render it neutrally rather than
|
|
1422
1785
|
// forcing the alarming root-cause anchor onto a non-problem.
|
|
1423
1786
|
const structured =
|
|
1424
1787
|
!!diagnosis.rootCause || (diagnosis.remediation?.length ?? 0) > 0;
|
|
1425
|
-
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
|
+
);
|
|
1426
1800
|
|
|
1427
1801
|
return (
|
|
1428
1802
|
<DiagnosisResult
|
|
1429
1803
|
diagnosis={diagnosis}
|
|
1430
1804
|
onApply={onApply}
|
|
1431
|
-
|
|
1432
|
-
|
|
1805
|
+
explanation={explanation}
|
|
1806
|
+
section={section}
|
|
1807
|
+
animate={animate}
|
|
1808
|
+
showDisclaimer={showDisclaimer}
|
|
1809
|
+
compactActions={compactActions}
|
|
1810
|
+
assessmentAction={assessmentAction}
|
|
1811
|
+
assessmentSources={assessmentSources}
|
|
1812
|
+
actionNotice={actionNotice}
|
|
1433
1813
|
/>
|
|
1434
1814
|
);
|
|
1435
1815
|
}
|
|
1436
1816
|
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
+
}
|
|
1854
|
+
|
|
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
|
+
};
|
|
1439
1910
|
|
|
1440
|
-
// The diagnosis result:
|
|
1911
|
+
// The diagnosis result: likely cause + remediation + the
|
|
1441
1912
|
// agent's full analysis on demand.
|
|
1442
1913
|
function DiagnosisResult({
|
|
1443
1914
|
diagnosis,
|
|
1444
1915
|
onApply,
|
|
1445
|
-
|
|
1446
|
-
|
|
1916
|
+
explanation,
|
|
1917
|
+
section = "full",
|
|
1918
|
+
animate,
|
|
1919
|
+
showDisclaimer,
|
|
1920
|
+
compactActions,
|
|
1921
|
+
assessmentAction,
|
|
1922
|
+
assessmentSources,
|
|
1923
|
+
actionNotice,
|
|
1447
1924
|
}: {
|
|
1448
1925
|
diagnosis: Diagnosis;
|
|
1449
1926
|
onApply?: (fix: string) => void;
|
|
1450
|
-
|
|
1451
|
-
|
|
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;
|
|
1452
1935
|
}) {
|
|
1453
|
-
const [
|
|
1454
|
-
|
|
1455
|
-
|
|
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).
|
|
1456
1969
|
const rootCause = diagnosis.rootCause;
|
|
1457
1970
|
const remediation = diagnosis.remediation || [];
|
|
1458
1971
|
const hasRemediation = remediation.length > 0;
|
|
@@ -1463,161 +1976,321 @@ function DiagnosisResult({
|
|
|
1463
1976
|
// When it returns 0 / none ("needs human judgement"), we honor that and don't
|
|
1464
1977
|
// offer one-click apply — the steps stay copy-only with a note.
|
|
1465
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
|
+
};
|
|
1466
2054
|
return (
|
|
1467
|
-
<div className=
|
|
1468
|
-
{/*
|
|
1469
|
-
{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 && (
|
|
1470
2058
|
<div
|
|
1471
|
-
className=
|
|
1472
|
-
|
|
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
|
+
}
|
|
1473
2064
|
>
|
|
1474
|
-
<div
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
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
|
+
)}
|
|
1480
2077
|
<div className="flex items-center gap-2">
|
|
1481
|
-
{
|
|
1482
|
-
<ConfidenceMeter value={diagnosis.confidence} />
|
|
1483
|
-
) : (
|
|
1484
|
-
<ConfidenceUnstated />
|
|
1485
|
-
)}
|
|
1486
|
-
<CopyButton text={rootCause} />
|
|
2078
|
+
<CopyButton text={rootCause} label="Copy likely cause" />
|
|
1487
2079
|
</div>
|
|
1488
2080
|
</div>
|
|
1489
|
-
<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
|
+
>
|
|
1490
2084
|
{rootCause}
|
|
1491
2085
|
</AIMarkdown>
|
|
1492
|
-
{onAsk && reveal === "full" && (
|
|
1493
|
-
<button
|
|
1494
|
-
onClick={() => onAsk(EXPLAIN_SIMPLY_PROMPT)}
|
|
1495
|
-
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"
|
|
1496
|
-
>
|
|
1497
|
-
<HelpCircle className="h-3 w-3" />
|
|
1498
|
-
Explain simply
|
|
1499
|
-
</button>
|
|
1500
|
-
)}
|
|
1501
2086
|
</div>
|
|
1502
2087
|
)}
|
|
1503
2088
|
|
|
1504
|
-
{/*
|
|
1505
|
-
|
|
1506
|
-
{
|
|
1507
|
-
<SynthBeat label="Weighing remediation options" />
|
|
1508
|
-
)}
|
|
1509
|
-
|
|
1510
|
-
{/* Remediation — copyable steps; the recommended one is highlighted as the
|
|
1511
|
-
default, and any step can be applied (Apply binds to that step's text). */}
|
|
1512
|
-
{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 && (
|
|
1513
2092
|
<div
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
+
}
|
|
1516
2099
|
>
|
|
1517
|
-
|
|
1518
|
-
<
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
}`}
|
|
1541
|
-
>
|
|
1542
|
-
{i + 1}
|
|
1543
|
-
</span>
|
|
1544
|
-
<div className="min-w-0 flex-1">
|
|
1545
|
-
{isRec && (
|
|
1546
|
-
<div className="mb-1">
|
|
1547
|
-
<div className="flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wide text-accent">
|
|
1548
|
-
<Sparkles className="h-3 w-3" />
|
|
1549
|
-
Recommended
|
|
1550
|
-
</div>
|
|
1551
|
-
{diagnosis.recommendedReason && (
|
|
1552
|
-
<div className="mt-0.5 text-[11px] leading-snug text-theme-text-tertiary">
|
|
1553
|
-
{diagnosis.recommendedReason}
|
|
1554
|
-
</div>
|
|
1555
|
-
)}
|
|
1556
|
-
</div>
|
|
1557
|
-
)}
|
|
1558
|
-
<AIMarkdown className="text-sm [overflow-wrap:anywhere] [&_p]:my-0 [&_pre]:my-1.5">
|
|
1559
|
-
{r}
|
|
1560
|
-
</AIMarkdown>
|
|
1561
|
-
</div>
|
|
1562
|
-
{/* Action cluster: compact Apply (recommended = subtly
|
|
1563
|
-
filled, others = ghost) sits next to Copy so each row's
|
|
1564
|
-
actions stay together. The ellipsis signals a confirm
|
|
1565
|
-
dialog follows — it doesn't apply immediately. */}
|
|
1566
|
-
<div className="flex shrink-0 items-center gap-0.5">
|
|
1567
|
-
{canApply && (
|
|
1568
|
-
<button
|
|
1569
|
-
onClick={() => onApply!(r)}
|
|
1570
|
-
className={`inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-accent transition-colors ${
|
|
1571
|
-
isRec
|
|
1572
|
-
? "border border-accent/40 bg-accent/10 hover:bg-accent/20"
|
|
1573
|
-
: "hover:bg-accent/10"
|
|
1574
|
-
}`}
|
|
1575
|
-
>
|
|
1576
|
-
<Wrench className="h-3 w-3" />
|
|
1577
|
-
Apply…
|
|
1578
|
-
</button>
|
|
1579
|
-
)}
|
|
1580
|
-
<CopyButton text={r} />
|
|
1581
|
-
</div>
|
|
1582
|
-
</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>
|
|
1583
2123
|
</li>
|
|
1584
|
-
)
|
|
1585
|
-
|
|
1586
|
-
</
|
|
1587
|
-
{
|
|
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 && (
|
|
1588
2147
|
<p className="mt-2 flex items-start gap-1.5 text-[11px] leading-snug text-theme-text-tertiary">
|
|
1589
2148
|
<ShieldCheck className="mt-0.5 h-3 w-3 shrink-0" />
|
|
1590
|
-
No
|
|
1591
|
-
|
|
1592
|
-
interactively.
|
|
2149
|
+
No one-click fix is available. Review these steps and apply them
|
|
2150
|
+
manually, or ask the agent to continue.
|
|
1593
2151
|
</p>
|
|
1594
2152
|
)}
|
|
1595
2153
|
</div>
|
|
1596
2154
|
)}
|
|
1597
2155
|
|
|
1598
2156
|
{/* Full analysis — the agent's detailed evidence, on demand. */}
|
|
1599
|
-
{
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
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
|
+
)}
|
|
1615
2226
|
</div>
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
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
|
+
)}
|
|
1619
2292
|
|
|
1620
|
-
{
|
|
2293
|
+
{showConclusion && showDisclaimer && (
|
|
1621
2294
|
<div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
|
|
1622
2295
|
<ShieldCheck className="mt-0.5 h-3 w-3 shrink-0" />
|
|
1623
2296
|
<span>AI-generated — review before applying</span>
|
|
@@ -1627,30 +2300,124 @@ function DiagnosisResult({
|
|
|
1627
2300
|
);
|
|
1628
2301
|
}
|
|
1629
2302
|
|
|
1630
|
-
function AllClearCard({
|
|
1631
|
-
|
|
1632
|
-
|
|
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;
|
|
1633
2330
|
return (
|
|
1634
|
-
<div className=
|
|
2331
|
+
<div className={`mt-3 space-y-2 ${animate ? "animate-result-in" : ""}`}>
|
|
1635
2332
|
<div
|
|
1636
|
-
className=
|
|
1637
|
-
|
|
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
|
+
}`}
|
|
1638
2340
|
>
|
|
1639
2341
|
<div className="mb-1 flex items-center justify-between gap-2">
|
|
1640
|
-
<div
|
|
1641
|
-
|
|
1642
|
-
|
|
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"}
|
|
1643
2359
|
</div>
|
|
1644
|
-
<CopyButton text={
|
|
2360
|
+
<CopyButton text={report} label="Copy assessment" />
|
|
1645
2361
|
</div>
|
|
1646
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">
|
|
1647
|
-
{
|
|
2363
|
+
{summary}
|
|
1648
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}
|
|
1649
2377
|
</div>
|
|
1650
|
-
|
|
1651
|
-
<
|
|
1652
|
-
|
|
1653
|
-
|
|
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}
|
|
1654
2421
|
</div>
|
|
1655
2422
|
);
|
|
1656
2423
|
}
|
|
@@ -1658,22 +2425,25 @@ function AllClearCard({ diagnosis }: { diagnosis: Diagnosis }) {
|
|
|
1658
2425
|
// The agent investigated but couldn't determine an answer. A distinct, honest
|
|
1659
2426
|
// state — neutral (not the alarming amber root cause, not the reassuring emerald
|
|
1660
2427
|
// all-clear) — so "I couldn't tell" never reads as "you're fine."
|
|
1661
|
-
function InconclusiveCard({
|
|
2428
|
+
function InconclusiveCard({
|
|
2429
|
+
diagnosis,
|
|
2430
|
+
animate,
|
|
2431
|
+
}: {
|
|
2432
|
+
diagnosis: Diagnosis;
|
|
2433
|
+
animate: boolean;
|
|
2434
|
+
}) {
|
|
1662
2435
|
const text =
|
|
1663
2436
|
diagnosis.report ||
|
|
1664
|
-
"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.";
|
|
1665
2438
|
return (
|
|
1666
|
-
<div className=
|
|
1667
|
-
<div
|
|
1668
|
-
className="rounded-lg border border-theme-border bg-theme-elevated p-3 animate-verdict-reveal"
|
|
1669
|
-
style={{ "--glow": "rgb(100 116 139)" } as React.CSSProperties}
|
|
1670
|
-
>
|
|
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">
|
|
1671
2441
|
<div className="mb-1 flex items-center justify-between gap-2">
|
|
1672
2442
|
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-theme-text-secondary">
|
|
1673
2443
|
<HelpCircle className="h-3.5 w-3.5" />
|
|
1674
2444
|
Couldn't determine
|
|
1675
2445
|
</div>
|
|
1676
|
-
<CopyButton text={text} />
|
|
2446
|
+
<CopyButton text={text} label="Copy assessment" />
|
|
1677
2447
|
</div>
|
|
1678
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">
|
|
1679
2449
|
{text}
|
|
@@ -1682,7 +2452,8 @@ function InconclusiveCard({ diagnosis }: { diagnosis: Diagnosis }) {
|
|
|
1682
2452
|
<div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
|
|
1683
2453
|
<ShieldCheck className="mt-0.5 h-3 w-3 shrink-0" />
|
|
1684
2454
|
<span>
|
|
1685
|
-
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.
|
|
1686
2457
|
</span>
|
|
1687
2458
|
</div>
|
|
1688
2459
|
</div>
|
|
@@ -1691,17 +2462,25 @@ function InconclusiveCard({ diagnosis }: { diagnosis: Diagnosis }) {
|
|
|
1691
2462
|
|
|
1692
2463
|
// A follow-up reply: the agent answering a question, not re-diagnosing. Plain
|
|
1693
2464
|
// neutral block — no root-cause anchor, no remediation/apply.
|
|
1694
|
-
function FollowupAnswer({
|
|
2465
|
+
function FollowupAnswer({
|
|
2466
|
+
diagnosis,
|
|
2467
|
+
animate,
|
|
2468
|
+
}: {
|
|
2469
|
+
diagnosis: Diagnosis;
|
|
2470
|
+
animate: boolean;
|
|
2471
|
+
}) {
|
|
1695
2472
|
const text = diagnosis.report || diagnosis.rootCause;
|
|
1696
2473
|
if (!text) return null;
|
|
1697
2474
|
return (
|
|
1698
|
-
<div
|
|
2475
|
+
<div
|
|
2476
|
+
className={`mt-1 rounded-lg border border-theme-border bg-theme-elevated p-3 ${animate ? "animate-result-in" : ""}`}
|
|
2477
|
+
>
|
|
1699
2478
|
<div className="mb-1.5 flex items-center justify-between gap-2">
|
|
1700
2479
|
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
1701
2480
|
<Sparkles className="h-3.5 w-3.5 text-accent" />
|
|
1702
2481
|
Answer
|
|
1703
2482
|
</div>
|
|
1704
|
-
<CopyButton text={text} />
|
|
2483
|
+
<CopyButton text={text} label="Copy answer" />
|
|
1705
2484
|
</div>
|
|
1706
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">
|
|
1707
2486
|
{text}
|
|
@@ -1710,88 +2489,149 @@ function FollowupAnswer({ diagnosis }: { diagnosis: Diagnosis }) {
|
|
|
1710
2489
|
);
|
|
1711
2490
|
}
|
|
1712
2491
|
|
|
1713
|
-
// A done turn that produced no renderable
|
|
2492
|
+
// A done turn that produced no renderable result at all (empty diagnosis, no
|
|
1714
2493
|
// narration). Without this the turn would render blank — which reads as "the tool
|
|
1715
2494
|
// broke." Make the dead-end explicit and point at the recovery (a follow-up).
|
|
1716
|
-
function EmptyResult() {
|
|
2495
|
+
function EmptyResult({ animate }: { animate: boolean }) {
|
|
1717
2496
|
return (
|
|
1718
|
-
<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
|
+
>
|
|
1719
2500
|
<ShieldCheck className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
1720
2501
|
<span>
|
|
1721
|
-
The investigation finished without a clear
|
|
1722
|
-
question, or
|
|
2502
|
+
The investigation finished without a clear conclusion. Try a follow-up
|
|
2503
|
+
question, or investigate again.
|
|
1723
2504
|
</span>
|
|
1724
2505
|
</div>
|
|
1725
2506
|
);
|
|
1726
2507
|
}
|
|
1727
2508
|
|
|
1728
|
-
// The result of an apply turn
|
|
1729
|
-
//
|
|
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.
|
|
1730
2512
|
function ApplyOutcomeCard({
|
|
1731
2513
|
diagnosis,
|
|
2514
|
+
error,
|
|
2515
|
+
applyOutcome,
|
|
1732
2516
|
onCheckStatus,
|
|
2517
|
+
animate,
|
|
1733
2518
|
}: {
|
|
1734
|
-
diagnosis: Diagnosis;
|
|
2519
|
+
diagnosis: Diagnosis | null;
|
|
2520
|
+
error?: string | null;
|
|
2521
|
+
applyOutcome?: ApplyMutationOutcome;
|
|
1735
2522
|
onCheckStatus?: () => void;
|
|
2523
|
+
animate: boolean;
|
|
1736
2524
|
}) {
|
|
1737
|
-
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";
|
|
1738
2560
|
return (
|
|
1739
|
-
<div className=
|
|
1740
|
-
<div className=
|
|
2561
|
+
<div className={`mt-3 space-y-2 ${animate ? "animate-result-in" : ""}`}>
|
|
2562
|
+
<div className={`rounded-lg border p-3 ${containerClass}`}>
|
|
1741
2563
|
<div className="mb-1 flex items-center justify-between gap-2">
|
|
1742
|
-
<div
|
|
1743
|
-
|
|
1744
|
-
|
|
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}
|
|
1745
2573
|
</div>
|
|
1746
|
-
{
|
|
2574
|
+
{detail && <CopyButton text={detail} label="Copy apply result" />}
|
|
1747
2575
|
</div>
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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>
|
|
1752
2586
|
)}
|
|
1753
|
-
{onCheckStatus && (
|
|
2587
|
+
{onCheckStatus && !failed && (
|
|
1754
2588
|
<button
|
|
2589
|
+
type="button"
|
|
1755
2590
|
onClick={onCheckStatus}
|
|
1756
|
-
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}`}
|
|
1757
2592
|
>
|
|
1758
2593
|
<RefreshCw className="h-4 w-4" />
|
|
1759
|
-
Check status
|
|
2594
|
+
Check current status
|
|
1760
2595
|
</button>
|
|
1761
2596
|
)}
|
|
1762
2597
|
</div>
|
|
1763
2598
|
<div className="flex items-center gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
|
|
1764
2599
|
<ShieldCheck className="h-3 w-3 shrink-0" />
|
|
1765
|
-
<span className="truncate">
|
|
1766
|
-
Applied by AI — verify the change took effect
|
|
1767
|
-
</span>
|
|
2600
|
+
<span className="truncate">{provenance}</span>
|
|
1768
2601
|
</div>
|
|
1769
2602
|
</div>
|
|
1770
2603
|
);
|
|
1771
2604
|
}
|
|
1772
2605
|
|
|
1773
|
-
function CopyButton({ text }: { text: string }) {
|
|
2606
|
+
function CopyButton({ text, label }: { text: string; label: string }) {
|
|
1774
2607
|
const [copied, setCopied] = useState(false);
|
|
1775
2608
|
return (
|
|
1776
|
-
<
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
setTimeout(() => setCopied(false), 1200);
|
|
1781
|
-
}}
|
|
1782
|
-
className="shrink-0 rounded p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
1783
|
-
aria-label="Copy"
|
|
2609
|
+
<Tooltip
|
|
2610
|
+
content={copied ? "Copied" : label}
|
|
2611
|
+
delay={100}
|
|
2612
|
+
wrapperClassName="shrink-0"
|
|
1784
2613
|
>
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
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>
|
|
1791
2631
|
);
|
|
1792
2632
|
}
|
|
1793
2633
|
|
|
1794
|
-
function prettyTool(tool: string): string {
|
|
2634
|
+
export function prettyTool(tool: string): string {
|
|
1795
2635
|
return tool.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1796
2636
|
}
|
|
1797
2637
|
|
|
@@ -1803,42 +2643,6 @@ function confidenceLabel(c: number): string {
|
|
|
1803
2643
|
return "Low";
|
|
1804
2644
|
}
|
|
1805
2645
|
|
|
1806
|
-
// Confidence shown as a band + three discrete pips (Low=1, Med=2, High=3 filled).
|
|
1807
|
-
// Deliberately discrete, NOT a continuous bar: a filled fraction reads as a precise
|
|
1808
|
-
// percentage, which is exactly the false calibration `confidenceLabel` exists to
|
|
1809
|
-
// avoid (an LLM's two-sig-fig confidence isn't that precise). Accent-toned so it
|
|
1810
|
-
// reads as "trust in the analysis," not problem severity.
|
|
1811
|
-
function ConfidenceMeter({ value }: { value: number }) {
|
|
1812
|
-
const band = confidenceLabel(value);
|
|
1813
|
-
const filled = band === "High" ? 3 : band === "Medium" ? 2 : 1;
|
|
1814
|
-
return (
|
|
1815
|
-
<span className="flex items-center gap-1.5">
|
|
1816
|
-
<span className="text-[11px] text-theme-text-tertiary">
|
|
1817
|
-
{band} confidence
|
|
1818
|
-
</span>
|
|
1819
|
-
<span className="flex items-center gap-0.5" aria-hidden>
|
|
1820
|
-
{[0, 1, 2].map((i) => (
|
|
1821
|
-
<span
|
|
1822
|
-
key={i}
|
|
1823
|
-
className={`h-1 w-2.5 rounded-full ${i < filled ? "bg-accent" : "bg-theme-base"}`}
|
|
1824
|
-
/>
|
|
1825
|
-
))}
|
|
1826
|
-
</span>
|
|
1827
|
-
</span>
|
|
1828
|
-
);
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
// Shown when the model returned no confidence at all — so "unknown confidence"
|
|
1832
|
-
// is visible rather than silently absent (which looks identical to high confidence
|
|
1833
|
-
// minus the badge) on a trust-bearing surface.
|
|
1834
|
-
function ConfidenceUnstated() {
|
|
1835
|
-
return (
|
|
1836
|
-
<span className="text-[11px] text-theme-text-tertiary">
|
|
1837
|
-
Confidence not stated
|
|
1838
|
-
</span>
|
|
1839
|
-
);
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
2646
|
// LLMs occasionally open a ```fence mid-line ("run this: ```bash kubectl …") or
|
|
1843
2647
|
// put the command on the same line as the ```lang marker. GFM then won't parse
|
|
1844
2648
|
// it as a fence — it leaks the literal ``` and renders an empty code box. Coerce
|