@skyhook-io/radar-app 1.13.3 → 1.13.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/package.json +6 -6
  2. package/src/App.tsx +68 -23
  3. package/src/RadarApp.tsx +17 -1
  4. package/src/api/client.ts +28 -10
  5. package/src/api/diagnose.ts +61 -15
  6. package/src/components/ConnectionErrorView.test.tsx +30 -0
  7. package/src/components/ConnectionErrorView.tsx +31 -19
  8. package/src/components/diagnose/AgentCase.tsx +131 -0
  9. package/src/components/diagnose/DiagnoseContext.test.ts +95 -0
  10. package/src/components/diagnose/DiagnoseContext.tsx +402 -77
  11. package/src/components/diagnose/DiagnoseSurface.test.tsx +53 -54
  12. package/src/components/diagnose/DiagnoseSurface.tsx +198 -286
  13. package/src/components/diagnose/Home.test.tsx +23 -1
  14. package/src/components/diagnose/Home.tsx +49 -3
  15. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +1446 -5
  16. package/src/components/diagnose/InvestigationEvidencePane.tsx +1373 -126
  17. package/src/components/diagnose/InvestigationView.tsx +227 -5
  18. package/src/components/diagnose/LocalDiagnoseAction.tsx +2 -3
  19. package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
  20. package/src/components/diagnose/investigationCase.test.tsx +1568 -0
  21. package/src/components/diagnose/investigationCase.ts +439 -0
  22. package/src/components/diagnose/investigationEvidence.test.ts +3525 -17
  23. package/src/components/diagnose/investigationEvidence.ts +2246 -166
  24. package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
  25. package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
  26. package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
  27. package/src/components/diagnose/investigationMetrics.test.ts +712 -0
  28. package/src/components/diagnose/investigationMetrics.ts +393 -0
  29. package/src/components/diagnose/investigationSourceFocus.ts +6 -0
  30. package/src/components/diagnose/investigationState.test.ts +322 -0
  31. package/src/components/diagnose/investigationState.ts +147 -25
  32. package/src/components/diagnose/parts.test.tsx +537 -1
  33. package/src/components/diagnose/parts.tsx +486 -42
  34. package/src/components/resource/HPACharts.render.test.tsx +77 -0
  35. package/src/components/resource/HPACharts.tsx +32 -25
  36. package/src/components/resource/PrometheusChartsGrid.tsx +181 -79
  37. package/src/components/resources/PodFilePreview.test.tsx +131 -0
  38. package/src/components/resources/PodFilePreview.tsx +394 -0
  39. package/src/components/resources/PodFilesystemModal.tsx +157 -67
  40. package/src/components/resources/ResourcesView.tsx +27 -2
  41. package/src/context/DiagnoseCustomization.test.tsx +26 -0
  42. package/src/context/DiagnoseCustomization.tsx +14 -2
  43. package/src/index.ts +8 -5
  44. package/src/utils/shell-safe.test.ts +25 -1
  45. package/src/utils/shell-safe.ts +16 -0
@@ -0,0 +1,131 @@
1
+ import { clsx } from "clsx";
2
+ import { Sparkles } from "lucide-react";
3
+ import { Badge } from "@skyhook-io/k8s-ui";
4
+
5
+ import type { DiagnosisEvidenceRole } from "../../api/diagnose";
6
+ import { Tooltip } from "../ui/Tooltip";
7
+
8
+ export const AGENT_ROLE_LABELS: Readonly<
9
+ Record<DiagnosisEvidenceRole, string>
10
+ > = {
11
+ cause: "Cause",
12
+ symptom: "Symptom",
13
+ context: "Context",
14
+ benign: "Not a problem",
15
+ demoted: "Less relevant",
16
+ rules_out: "Rules out",
17
+ };
18
+
19
+ /**
20
+ * The agent's framing of a Radar fact. Always the agent tone, never a
21
+ * severity tone: a chip must not read as a Radar finding.
22
+ */
23
+ export function AgentRoleChip({ role }: { role: DiagnosisEvidenceRole }) {
24
+ return (
25
+ <Tooltip
26
+ content={`The agent labelled this evidence "${AGENT_ROLE_LABELS[role]}". Radar recorded the fact; the label is the agent's reading of it.`}
27
+ // The label has to share the sentence's baseline. Centring the badge box
28
+ // drops it ~1px (padding and border make the box taller than the line),
29
+ // and the badge's own baseline comes from the sparkle, landing 1px high.
30
+ wrapperClassName="shrink-0 align-[-1px]"
31
+ >
32
+ <Badge tone="agent" size="sm">
33
+ <Sparkles className="h-2.5 w-2.5 shrink-0" aria-hidden />
34
+ {AGENT_ROLE_LABELS[role]}
35
+ </Badge>
36
+ </Tooltip>
37
+ );
38
+ }
39
+
40
+ /**
41
+ * One sentence from the agent about a Radar fact. Everything the agent
42
+ * contributed to a card lives in this one row — the sparkle, the role, and the
43
+ * sentence — so the register is unmistakable: the fact above is Radar's, this
44
+ * line is the agent's reading of it. The role travelled with the card title
45
+ * once, where it sat in the same slot as Radar's own badges and could be read
46
+ * as Radar having established it.
47
+ *
48
+ * A `rules_out` note carries the hypothesis it excludes, as text beside the
49
+ * chip rather than inside it: "Rules out" alone forces the reader to find the
50
+ * hypothesis in a block further down and match it back, and a hypothesis long
51
+ * enough to be useful makes a badge that swallows the row.
52
+ */
53
+ export function AgentClaimNote({
54
+ claim,
55
+ role,
56
+ excludes,
57
+ subject,
58
+ className,
59
+ }: {
60
+ claim: string;
61
+ role?: DiagnosisEvidenceRole;
62
+ /**
63
+ * The hypothesis this item excludes. Rendered only for `rules_out`, where
64
+ * the chip frames it as rejected: a hypothesis is a claim the agent
65
+ * DISPROVED, so on any other card it would read as a statement of fact.
66
+ */
67
+ excludes?: string;
68
+ /** The observation the note was bound to, when it is not on that card. */
69
+ subject?: string;
70
+ className?: string;
71
+ }) {
72
+ if (!claim && !role) return null;
73
+ return (
74
+ <p
75
+ data-agent-claim
76
+ className={clsx(
77
+ "flex min-w-0 items-start gap-2 border-t border-dashed border-theme-border/70 text-xs leading-relaxed text-theme-text-secondary",
78
+ className,
79
+ )}
80
+ >
81
+ <span className="min-w-0 flex-1 [overflow-wrap:anywhere]">
82
+ {/* The chip and its sparkle carry this visually; the word is what
83
+ survives when colour and iconography do not. "Agent" rather than
84
+ "Agent's note" because a role can arrive with no sentence. */}
85
+ <span className="sr-only">Agent: </span>
86
+ {role ? (
87
+ <>
88
+ <AgentRoleChip role={role} />{" "}
89
+ </>
90
+ ) : null}
91
+ {excludes && role === "rules_out" ? (
92
+ <>
93
+ <span className="italic text-theme-text-tertiary">{excludes}</span>
94
+ {" — "}
95
+ </>
96
+ ) : null}
97
+ {claim ? (
98
+ <>
99
+ {subject ? (
100
+ <span className="text-theme-text-tertiary">{subject} · </span>
101
+ ) : null}
102
+ {renderClaim(claim)}
103
+ </>
104
+ ) : null}
105
+ </span>
106
+ </p>
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Claims are one sentence of prose in which the agent marks identifiers with
112
+ * backticks. Only that inline-code span is honoured: a full Markdown render
113
+ * would wrap the sentence in a block and break the shared baseline with the
114
+ * role chip and the lead-in.
115
+ */
116
+ function renderClaim(claim: string) {
117
+ const parts = claim.split("`");
118
+ if (parts.length < 3) return claim;
119
+ return parts.map((part, i) =>
120
+ i % 2 === 1 && i < parts.length - 1 ? (
121
+ <code
122
+ key={i}
123
+ className="inline-code rounded border border-theme-border/60 bg-theme-base px-1 py-px font-mono text-[11px] font-normal text-theme-text-primary"
124
+ >
125
+ {part}
126
+ </code>
127
+ ) : (
128
+ part
129
+ ),
130
+ );
131
+ }
@@ -0,0 +1,95 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ investigationWorkspaceNavigationState,
4
+ investigationWorkspacePath,
5
+ investigationWorkspaceRestorePath,
6
+ investigationWorkspaceSearch,
7
+ isInvestigationWorkspacePath,
8
+ shouldExitUnavailableWorkspace,
9
+ workspaceRunIDFromPath,
10
+ } from "./DiagnoseContext";
11
+
12
+ describe("investigation workspace routes", () => {
13
+ it("recognizes the base workspace and selected-run routes", () => {
14
+ expect(isInvestigationWorkspacePath("/investigations")).toBe(true);
15
+ expect(isInvestigationWorkspacePath("/investigations/run_123")).toBe(true);
16
+ expect(workspaceRunIDFromPath("/investigations/run_123")).toBe("run_123");
17
+ expect(workspaceRunIDFromPath("/investigations/run%20one")).toBe("run one");
18
+ });
19
+
20
+ it("keeps only cross-view scope in workspace URLs", () => {
21
+ expect(
22
+ investigationWorkspaceSearch(
23
+ "?org=o1&namespaces=prod%2Cstage&ai-run=r1&resource=pod",
24
+ ),
25
+ ).toBe("?org=o1&namespaces=prod%2Cstage");
26
+ });
27
+
28
+ it("uses Home for global entry and an explicit route for drawer expansion", () => {
29
+ expect(investigationWorkspacePath(null)).toBe("/investigations");
30
+ expect(investigationWorkspacePath("run 123")).toBe(
31
+ "/investigations/run%20123",
32
+ );
33
+ });
34
+
35
+ it("builds opaque navigation state for a synthetic drawer return", () => {
36
+ expect(
37
+ investigationWorkspaceNavigationState({
38
+ returnPath: "/?ai-run=r1",
39
+ drawerOrigin: true,
40
+ closeHistorySteps: 2,
41
+ }),
42
+ ).toEqual({
43
+ investigationWorkspaceReturn: "/?ai-run=r1",
44
+ investigationReturnHistorySteps: "2",
45
+ });
46
+ });
47
+
48
+ it("restores the currently focused run into the original dock location", () => {
49
+ expect(
50
+ investigationWorkspaceRestorePath(
51
+ { investigationWorkspaceReturn: "/resources?kind=pods&ai-run=old#row" },
52
+ "new run",
53
+ "https://radar.local",
54
+ ),
55
+ ).toBe("/resources?kind=pods&ai-run=new+run#row");
56
+ });
57
+
58
+ it("falls back safely when a malformed return path normalizes cross-origin", () => {
59
+ expect(
60
+ investigationWorkspaceRestorePath(
61
+ { investigationWorkspaceReturn: "/\\\\evil.example" },
62
+ "run-1",
63
+ "https://radar.local",
64
+ ),
65
+ ).toBeNull();
66
+ });
67
+
68
+ it("exits only resolved AI-off workspace routes", () => {
69
+ expect(
70
+ shouldExitUnavailableWorkspace("/investigations/run-1", true, false),
71
+ ).toBe(true);
72
+ expect(
73
+ shouldExitUnavailableWorkspace("/investigations/run-1", false, false),
74
+ ).toBe(false);
75
+ expect(shouldExitUnavailableWorkspace("/?ai-run=run-1", true, false)).toBe(
76
+ false,
77
+ );
78
+ });
79
+
80
+ it("builds history-only return state for cross-tree workspace entry", () => {
81
+ expect(
82
+ investigationWorkspaceNavigationState({ closeHistorySteps: 1 }),
83
+ ).toEqual({ investigationReturnHistorySteps: "1" });
84
+ });
85
+
86
+ it("does not claim unrelated paths and rejects malformed run ids", () => {
87
+ expect(isInvestigationWorkspacePath("/resources/pods")).toBe(false);
88
+ expect(isInvestigationWorkspacePath("/investigations/run/extra")).toBe(
89
+ true,
90
+ );
91
+ expect(workspaceRunIDFromPath("/investigations")).toBeNull();
92
+ expect(workspaceRunIDFromPath("/investigations/%2Fbad")).toBeNull();
93
+ expect(workspaceRunIDFromPath("/investigations/%ZZ")).toBeNull();
94
+ });
95
+ });