@skyhook-io/radar-app 1.13.1 → 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.
Files changed (47) hide show
  1. package/package.json +2 -2
  2. package/src/App.tsx +19 -1
  3. package/src/RadarApp.tsx +2 -2
  4. package/src/api/diagnose.test.ts +268 -0
  5. package/src/api/diagnose.ts +72 -9
  6. package/src/components/diagnose/AISettings.tsx +1 -1
  7. package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
  8. package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
  9. package/src/components/diagnose/DiagnoseContext.tsx +12 -17
  10. package/src/components/diagnose/DiagnoseSurface.test.tsx +211 -16
  11. package/src/components/diagnose/DiagnoseSurface.tsx +464 -133
  12. package/src/components/diagnose/Home.test.tsx +293 -0
  13. package/src/components/diagnose/Home.tsx +289 -119
  14. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +2170 -0
  15. package/src/components/diagnose/InvestigationEvidencePane.tsx +2253 -0
  16. package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +257 -0
  17. package/src/components/diagnose/InvestigationResourceEvidence.tsx +214 -0
  18. package/src/components/diagnose/InvestigationView.test.ts +17 -0
  19. package/src/components/diagnose/InvestigationView.tsx +1900 -393
  20. package/src/components/diagnose/LocalDiagnoseAction.tsx +42 -25
  21. package/src/components/diagnose/agentCatalog.ts +1 -1
  22. package/src/components/diagnose/diagnoseEvidenceTypes.ts +151 -0
  23. package/src/components/diagnose/investigationEvidence.test.ts +3109 -0
  24. package/src/components/diagnose/investigationEvidence.ts +3492 -0
  25. package/src/components/diagnose/investigationEvidencePresentation.test.ts +447 -0
  26. package/src/components/diagnose/investigationEvidencePresentation.ts +167 -0
  27. package/src/components/diagnose/investigationExplanation.test.ts +63 -0
  28. package/src/components/diagnose/investigationExplanation.ts +22 -0
  29. package/src/components/diagnose/investigationResourceEvidenceModel.ts +322 -0
  30. package/src/components/diagnose/investigationSourceFocus.test.ts +143 -0
  31. package/src/components/diagnose/investigationSourceFocus.ts +98 -0
  32. package/src/components/diagnose/investigationState.test.ts +695 -0
  33. package/src/components/diagnose/investigationState.ts +451 -0
  34. package/src/components/diagnose/parts.test.tsx +864 -3
  35. package/src/components/diagnose/parts.tsx +1337 -541
  36. package/src/components/diagnose/target.test.ts +39 -0
  37. package/src/components/diagnose/target.ts +36 -0
  38. package/src/components/diagnose/useDisclosureReveal.ts +117 -0
  39. package/src/components/home/MCPSetupDialog.tsx +2 -2
  40. package/src/components/home/mcpToolCatalog.test.ts +22 -0
  41. package/src/components/home/mcpToolCatalog.ts +3 -2
  42. package/src/components/issues/IssuesPane.tsx +5 -1
  43. package/src/components/settings/SettingsDialog.tsx +11 -13
  44. package/src/components/workload/WorkloadView.tsx +1 -1
  45. package/src/context/DiagnoseCustomization.tsx +11 -8
  46. package/src/index.css +63 -79
  47. package/src/index.ts +1 -1
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { formatInvestigationTarget, runTargetKey } from "./target";
4
+
5
+ describe("investigation target identity", () => {
6
+ it("uses one running key for singular Kinds and plural resource names", () => {
7
+ expect(runTargetKey("Deployment", "prod", "api", "apps")).toBe(
8
+ runTargetKey("deployments", "prod", "api", "apps"),
9
+ );
10
+ });
11
+
12
+ it("keeps same-named resources in different API groups distinct", () => {
13
+ expect(runTargetKey("Service", "prod", "api", "")).not.toBe(
14
+ runTargetKey("services", "prod", "api", "serving.knative.dev"),
15
+ );
16
+ });
17
+
18
+ it("formats a non-core target with its Kubernetes-qualified Kind", () => {
19
+ expect(
20
+ formatInvestigationTarget({
21
+ kind: "Rollout",
22
+ group: "argoproj.io",
23
+ namespace: "prod",
24
+ name: "checkout",
25
+ }),
26
+ ).toBe("Rollout.argoproj.io prod/checkout");
27
+ });
28
+
29
+ it("keeps the core target label compact", () => {
30
+ expect(
31
+ formatInvestigationTarget({
32
+ kind: "Service",
33
+ group: "",
34
+ namespace: "prod",
35
+ name: "api",
36
+ }),
37
+ ).toBe("Service prod/api");
38
+ });
39
+ });
@@ -0,0 +1,36 @@
1
+ import { resourceKey } from "@skyhook-io/k8s-ui";
2
+
3
+ import { kindToPluralWithGroup } from "../../utils/navigation";
4
+
5
+ export interface InvestigationTargetIdentity {
6
+ kind: string;
7
+ /** Kubernetes API group; empty means core. */
8
+ group: string;
9
+ namespace: string;
10
+ name: string;
11
+ }
12
+
13
+ // Normalize the Kind/resource-name boundary before comparing a run returned by
14
+ // the API (singular Kind) with a UI action (often a plural resource name).
15
+ export function runTargetKey(
16
+ kind: string,
17
+ namespace: string,
18
+ name: string,
19
+ group: string,
20
+ ): string {
21
+ return resourceKey(
22
+ group,
23
+ kindToPluralWithGroup(kind, group),
24
+ namespace,
25
+ name,
26
+ );
27
+ }
28
+
29
+ // Keep the UI aligned with the CLI and the prompts: a non-core target is shown
30
+ // as Kind.api.group, which is the Kubernetes-qualified identity users can copy.
31
+ export function formatInvestigationTarget(
32
+ target: InvestigationTargetIdentity,
33
+ ): string {
34
+ const kind = target.group ? `${target.kind}.${target.group}` : target.kind;
35
+ return `${kind} ${target.namespace ? `${target.namespace}/` : ""}${target.name}`;
36
+ }
@@ -0,0 +1,117 @@
1
+ import { useCallback, useLayoutEffect, useRef } from "react";
2
+
3
+ // Shared Collapse uses a 200 ms grid-row transition. Keep a small paint margin
4
+ // before moving focus so the destination is stationary. Reduced-motion users get
5
+ // an immediate disclosure and focus hand-off because Collapse disables motion.
6
+ export const INVESTIGATION_DISCLOSURE_SETTLE_MS = 220;
7
+ export function prefersReducedMotion(): boolean {
8
+ return (
9
+ typeof window !== "undefined" &&
10
+ typeof window.matchMedia === "function" &&
11
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches
12
+ );
13
+ }
14
+
15
+ export function investigationDisclosureSettleDelay(
16
+ reducedMotion: boolean,
17
+ ): number {
18
+ return reducedMotion ? 0 : INVESTIGATION_DISCLOSURE_SETTLE_MS;
19
+ }
20
+
21
+ export function investigationDisclosureScrollTop({
22
+ scrollTop,
23
+ viewportTop,
24
+ viewportBottom,
25
+ disclosureTop,
26
+ disclosureBottom,
27
+ inset = 8,
28
+ }: {
29
+ scrollTop: number;
30
+ viewportTop: number;
31
+ viewportBottom: number;
32
+ disclosureTop: number;
33
+ disclosureBottom: number;
34
+ inset?: number;
35
+ }): number | undefined {
36
+ const visibleTop = viewportTop + inset;
37
+ const visibleBottom = viewportBottom - inset;
38
+ if (disclosureTop >= visibleTop && disclosureBottom <= visibleBottom) {
39
+ return undefined;
40
+ }
41
+ if (disclosureTop < visibleTop) {
42
+ return Math.max(0, scrollTop + disclosureTop - visibleTop);
43
+ }
44
+ const viewportHeight = visibleBottom - visibleTop;
45
+ const disclosureHeight = disclosureBottom - disclosureTop;
46
+ if (disclosureHeight <= viewportHeight) {
47
+ return Math.max(0, scrollTop + disclosureBottom - visibleBottom);
48
+ }
49
+ return Math.max(0, scrollTop + disclosureTop - visibleTop);
50
+ }
51
+
52
+ export function useDisclosureReveal<T extends HTMLElement>() {
53
+ const elementRef = useRef<T>(null);
54
+ const timerRef = useRef<number | undefined>(undefined);
55
+ useLayoutEffect(
56
+ () => () => {
57
+ if (timerRef.current !== undefined) {
58
+ window.clearTimeout(timerRef.current);
59
+ }
60
+ },
61
+ [],
62
+ );
63
+
64
+ const revealAfterToggle = useCallback((opening: boolean) => {
65
+ if (timerRef.current !== undefined) {
66
+ window.clearTimeout(timerRef.current);
67
+ timerRef.current = undefined;
68
+ }
69
+ if (!opening) return;
70
+ const settleDelay = investigationDisclosureSettleDelay(
71
+ prefersReducedMotion(),
72
+ );
73
+ const initialScroller = elementRef.current?.closest<HTMLElement>(
74
+ "[data-investigation-findings-scroll], [data-investigation-activity-scroll]",
75
+ );
76
+ const initialScrollTop = initialScroller?.scrollTop;
77
+ // Even reduced-motion disclosures need one task boundary so React can
78
+ // commit the open layout before it is measured.
79
+ timerRef.current = window.setTimeout(() => {
80
+ timerRef.current = undefined;
81
+ const element = elementRef.current;
82
+ if (!element) return;
83
+ const scroller = element.closest<HTMLElement>(
84
+ "[data-investigation-findings-scroll], [data-investigation-activity-scroll]",
85
+ );
86
+ // This component lives in a bounded Diagnose surface. Never fall back to
87
+ // scrolling the document (or an overflow-hidden ancestor), which can move
88
+ // the entire workspace and expose blank space below it.
89
+ if (!scroller) return;
90
+ // Expansion is delayed until Collapse has settled. If the reader scrolls
91
+ // during that interval, their newer intent wins over the automatic reveal.
92
+ if (
93
+ scroller === initialScroller &&
94
+ initialScrollTop !== undefined &&
95
+ Math.abs(scroller.scrollTop - initialScrollTop) > 2
96
+ ) {
97
+ return;
98
+ }
99
+ const viewport = scroller.getBoundingClientRect();
100
+ const disclosure = element.getBoundingClientRect();
101
+ const top = investigationDisclosureScrollTop({
102
+ scrollTop: scroller.scrollTop,
103
+ viewportTop: viewport.top,
104
+ viewportBottom: viewport.bottom,
105
+ disclosureTop: disclosure.top,
106
+ disclosureBottom: disclosure.bottom,
107
+ });
108
+ if (top === undefined) return;
109
+ scroller.scrollTo({
110
+ top,
111
+ behavior: settleDelay === 0 ? "auto" : "smooth",
112
+ });
113
+ }, settleDelay);
114
+ }, []);
115
+
116
+ return { elementRef, revealAfterToggle };
117
+ }
@@ -204,14 +204,14 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
204
204
  <a href="https://modelcontextprotocol.io" target="_blank" rel="noopener noreferrer" className="text-purple-400 hover:text-purple-300 underline underline-offset-2">
205
205
  Model Context Protocol
206
206
  </a>{' '}
207
- (MCP) server that lets AI agents inspect, diagnose, and operate your cluster through Radar.
207
+ (MCP) server that lets AI agents inspect, investigate, and operate your cluster through Radar.
208
208
  Unlike raw kubectl access, Radar gives your AI pre-processed, enriched data —
209
209
  topology graphs, health assessments, deduplicated events, filtered logs — so it
210
210
  can understand your cluster state quickly without burning through context on
211
211
  verbose YAML output.
212
212
  </p>
213
213
  <p className="text-sm text-theme-text-secondary leading-relaxed">
214
- Most read tools do not change cluster state. Live route diagnosis can create up
214
+ Most read tools do not change cluster state. An in-cluster route probe can create up
215
215
  to five self-deleting probe pods when explicitly requested. Write tools (restart,
216
216
  scale, sync, apply, node drain) are annotated as destructive so your AI client
217
217
  can flag them and prompt before running.
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { MCP_TOOL_CATALOG } from './mcpToolCatalog'
3
+
4
+ describe('diagnose catalog copy', () => {
5
+ const description = MCP_TOOL_CATALOG.find((tool) => tool.name === 'diagnose')?.desc
6
+
7
+ it('describes a bounded evidence bundle without promising complete sources or a verdict', () => {
8
+ expect(description).toContain('Bounded, point-in-time evidence bundle')
9
+ expect(description).toContain('not an agent run')
10
+ expect(description).toContain('not an authoritative root-cause verdict')
11
+ expect(description).toContain('attempts selected, capped current and previous logs where available')
12
+ expect(description).toContain('a capped warning-event sample')
13
+ expect(description).toContain('only when the evidence establishes one')
14
+ expect(description).not.toContain('One-call root-cause bundle')
15
+ })
16
+
17
+ it('documents group-qualified Argo Rollout evidence', () => {
18
+ const diagnose = MCP_TOOL_CATALOG.find((tool) => tool.name === 'diagnose')
19
+ expect(diagnose?.desc).toContain('Argo Rollout')
20
+ expect(diagnose?.params.find((param) => param.arg === 'group')?.desc).toContain('argoproj.io')
21
+ })
22
+ })
@@ -109,9 +109,10 @@ export const MCP_TOOL_CATALOG: MCPToolInfo[] = [
109
109
  },
110
110
  {
111
111
  name: 'diagnose',
112
- desc: 'One-call root-cause bundle. Workloads get spec + resourceContext + current AND previous logs across pods + warning events + startup blockers; GitOps reconcilers, including Flux HelmRelease, get status summary + parsed related issues; network entry kinds (Service / Ingress / HTTPRoute / GRPCRoute / Gateway) get a path-shaped trace naming the first broken hop, with an optional one-shot reachability test.',
112
+ desc: 'Bounded, point-in-time evidence bundle for one narrowed target — not an agent run and not an authoritative root-cause verdict. For workloads, including Argo Rollout, Radar attempts selected, capped current and previous logs where available, alongside resource context, a capped warning-event sample, and startup blockers; GitOps reconcilers, including Flux HelmRelease, get status summary + parsed related issues; network entry kinds (Service / Ingress / HTTPRoute / GRPCRoute / Gateway) get a coverage-honest path trace that identifies a broken hop only when the evidence establishes one, with an optional one-shot reachability test.',
113
113
  params: [
114
- { arg: 'kind', required: true, desc: 'pod, deployment, statefulset, daemonset, application, kustomization, Flux HelmRelease, service, ingress, httproute, grpcroute, or gateway' },
114
+ { arg: 'kind', required: true, desc: 'pod, deployment, statefulset, daemonset, Argo Rollout, application, kustomization, Flux HelmRelease, service, ingress, httproute, grpcroute, or gateway' },
115
+ { arg: 'group', desc: 'API group for CRDs or kind collisions (for example argoproj.io for Rollout); built-ins are inferred' },
115
116
  { arg: 'namespace', required: true, desc: 'resource namespace' },
116
117
  { arg: 'name', required: true, desc: 'resource name' },
117
118
  { arg: 'probe', desc: 'network kinds only: add active DNS/TCP/TLS/HTTP probes against the declared path (0-3s wall time)' },
@@ -1,7 +1,10 @@
1
1
  import { useMemo, useState } from "react";
2
2
  import { useNavigate } from "react-router-dom";
3
3
  import { useIssues } from "../../api/client";
4
- import { useAPIResources, karpenterCapacityAvailable } from "../../api/apiResources";
4
+ import {
5
+ useAPIResources,
6
+ karpenterCapacityAvailable,
7
+ } from "../../api/apiResources";
5
8
  import { useCapabilitiesContext } from "../../contexts/CapabilitiesContext";
6
9
  import { useConnection } from "../../context/ConnectionContext";
7
10
  import type { SelectedResource } from "../../types";
@@ -242,6 +245,7 @@ export function IssuesPane({
242
245
  )}
243
246
  <IssueDiagnoseButton
244
247
  kind={issue.kind}
248
+ group={issue.group}
245
249
  namespace={issue.namespace ?? ""}
246
250
  name={issue.name}
247
251
  />
@@ -107,7 +107,7 @@ interface SettingsDialogProps {
107
107
  // the owner-gated footer and applied after restart.
108
108
  // • Live integrations (Prometheus, cost source, Argo CD) — their own Apply/Connect endpoints
109
109
  // re-point the running server; effect immediately, NOT part of footer dirty.
110
- // • Self-saving preferences (cost currency, AI diagnose) — applied immediately.
110
+ // • Self-saving preferences (cost currency, AI investigations) — applied immediately.
111
111
  // Integration fields (prometheusUrl, argoCdUrl, argoCdInsecureTls) apply through
112
112
  // their own controls and are excluded here. Every field is normalized so
113
113
  // unset≡default doesn't read as a change.
@@ -139,7 +139,7 @@ export function SettingsDialog({
139
139
  const { data: versionInfo } = useVersionCheck()
140
140
  // Radar configuration (kubeconfig, port, integrations…) is host-level and
141
141
  // affects every user of this instance, so it's gated to owners. Personal
142
- // sections (My permissions, AI diagnose) stay usable by everyone. Non-Cloud
142
+ // sections (My permissions, AI investigations) stay usable by everyone. Non-Cloud
143
143
  // callers (OSS, OIDC, kubectl plugin) have no role and pass — single-user
144
144
  // laptops are never locked out of their own config. Backend enforces this too.
145
145
  const { canAtLeast } = useCloudRole()
@@ -163,9 +163,7 @@ export function SettingsDialog({
163
163
  open && section === 'argocd'
164
164
  )
165
165
 
166
- // AI Diagnosis prefs are client-side (localStorage) and now SELF-SAVING: the
167
- // section has its own Save that commits the draft to DiagnoseContext, so it's
168
- // independent of the owner-gated footer. The draft is snapshotted on open.
166
+ // Local AI preferences save independently of the owner-gated server settings.
169
167
  const diag = useDiagnose()
170
168
  const aiAvailable = diag.available && diag.agents.length > 0
171
169
  const [aiDraft, setAiDraft] = useState<AIDraft>({
@@ -443,7 +441,7 @@ export function SettingsDialog({
443
441
  // Flat, un-grouped nav ordered as a narrative — at-a-glance, then you, then how
444
442
  // Radar connects, then data integrations, then AI, then advanced. The
445
443
  // per-section captions carry the restart-vs-live semantics, so group labels
446
- // would only add visual weight. AI diagnose is always shown (the section
444
+ // would only add visual weight. AI investigations is always shown (the section
447
445
  // explains how to enable it when no agent CLI is installed).
448
446
  const navItems: NavItemDef[] = [
449
447
  { id: 'overview', label: 'Overview', icon: LayoutDashboard, ownerOnly: false, dirty: false },
@@ -452,7 +450,7 @@ export function SettingsDialog({
452
450
  { id: 'prometheus', label: 'Metrics', icon: Activity, ownerOnly: true, dirty: false },
453
451
  { id: 'cost', label: 'Cost', icon: Coins, ownerOnly: true, dirty: costIntegrationDirty },
454
452
  { id: 'argocd', label: 'Argo CD', icon: GitBranch, ownerOnly: true, dirty: false },
455
- { id: 'ai', label: 'AI diagnose', icon: Sparkles, ownerOnly: false, dirty: aiDirty },
453
+ { id: 'ai', label: 'AI investigations', icon: Sparkles, ownerOnly: false, dirty: aiDirty },
456
454
  { id: 'advanced', label: 'Advanced', icon: SlidersHorizontal, ownerOnly: true, dirty: advancedDirty },
457
455
  ]
458
456
 
@@ -722,16 +720,16 @@ export function SettingsDialog({
722
720
  />
723
721
  </SectionPane>
724
722
 
725
- {/* AI diagnose — self-saving, usable by everyone. Same heading block
723
+ {/* AI investigations — self-saving, usable by everyone. Same heading block
726
724
  as every other tab; the body is the agent controls (when a CLI is
727
725
  installed) or an enable explainer (when not). */}
728
726
  <div className={clsx(section !== 'ai' && 'hidden')} role="tabpanel" inert={section !== 'ai' || undefined}>
729
727
  <div className="mb-4">
730
- <h3 className="text-base font-semibold text-theme-text-primary">AI diagnose</h3>
728
+ <h3 className="text-base font-semibold text-theme-text-primary">AI investigations</h3>
731
729
  <p className="mt-0.5 text-xs text-theme-text-tertiary">
732
730
  {diag.hosted
733
- ? `Investigate incidents with ${diag.agentLabel} — reading logs, events, and topology to explain what's wrong.`
734
- : "Investigate incidents with an AI agent that runs on your own machine — reading logs, events, and topology to explain what's wrong. No Radar cloud, no API key."}
731
+ ? `Investigate incidents with ${diag.agentLabel} — reading logs, events, and topology to understand what's happening.`
732
+ : "Investigate incidents with an AI agent that runs on your own machine — reading logs, events, and topology to understand what's happening. No Radar cloud, no API key."}
735
733
  </p>
736
734
  </div>
737
735
  {aiAvailable ? (
@@ -1140,7 +1138,7 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
1140
1138
  copyable: mcpOn,
1141
1139
  },
1142
1140
  {
1143
- id: 'ai', icon: Sparkles, label: 'AI diagnose',
1141
+ id: 'ai', icon: Sparkles, label: 'AI investigations',
1144
1142
  tone: aiAvailable ? 'ok' : 'off',
1145
1143
  value: aiAvailable ? 'Ready' : 'No agent CLI',
1146
1144
  detail: aiAvailable ? agentLabel : undefined,
@@ -1220,7 +1218,7 @@ function OverviewStatus({ tone }: { tone: OverviewTone }) {
1220
1218
  return <span className={clsx('w-2 h-2 rounded-full shrink-0', cls)} />
1221
1219
  }
1222
1220
 
1223
- // AIUnavailableNotice is the body of the AI diagnose tab when no supported agent
1221
+ // AIUnavailableNotice is the body of the AI investigations tab when no supported agent
1224
1222
  // CLI is installed — the heading/description are provided by the tab itself, so
1225
1223
  // this is just the enable explainer (keeping the feature discoverable to whoever
1226
1224
  // would set it up).
@@ -1795,7 +1795,7 @@ function DiagnoseFromWorkloadHint({
1795
1795
  }) {
1796
1796
  if (services.length === 0) return null
1797
1797
  return (
1798
- <Section title="Diagnose network path">
1798
+ <Section title="Trace network path">
1799
1799
  <div className="flex items-start gap-2 text-xs text-theme-text-secondary">
1800
1800
  <Stethoscope className="w-4 h-4 mt-0.5 shrink-0 text-theme-text-tertiary" aria-hidden />
1801
1801
  <div className="flex-1 min-w-0">
@@ -1,24 +1,26 @@
1
- // Slot-based injection of a resource-level "Diagnose" action, and of the
1
+ // Slot-based injection of a resource-level "Investigate" action, and of the
2
2
  // consent card's trust copy.
3
3
  //
4
- // Lets an embedding host (e.g. Radar Hub) inject a "Diagnose with AI" button
4
+ // Lets an embedding host (e.g. Radar Hub) inject an "Investigate with AI" button
5
5
  // into every resource detail action bar — without forking WorkloadView or the
6
6
  // shared ResourceActionsBar. The host returns whatever node should render in
7
7
  // the action bar's right-aligned universal-actions area, given the resource
8
8
  // context.
9
9
  //
10
- // Default (no provider): Radar renders no Diagnose button — OSS stays
10
+ // Default (no provider): Radar renders no Investigate button — OSS stays
11
11
  // agent-free.
12
- import { createContext, useContext, useMemo } from 'react';
13
- import type { ReactNode } from 'react';
12
+ import { createContext, useContext, useMemo } from "react";
13
+ import type { ReactNode } from "react";
14
14
 
15
- /** Render prop for the resource-level Diagnose action. */
15
+ /** Render prop for the resource-level Investigate action. */
16
16
  export type RenderDiagnoseAction = (ctx: {
17
17
  kind: string;
18
+ /** Kubernetes API group; empty means core. */
19
+ group?: string;
18
20
  namespace: string;
19
21
  name: string;
20
22
  /** Coarse health of the resource (from its status badge), so the entry point can
21
- * adapt: an urgent "Diagnose" on a problem vs. a quiet "ask AI" when fine/unknown. */
23
+ * adapt: an urgent "Investigate" on a problem vs. a quiet "ask AI" when fine/unknown. */
22
24
  health?: "problem" | "healthy" | "unknown";
23
25
  }) => ReactNode;
24
26
 
@@ -62,7 +64,8 @@ const DEFAULTS: DiagnoseCustomization = {
62
64
  onOpenSettings: undefined,
63
65
  };
64
66
 
65
- const DiagnoseCustomizationContext = createContext<DiagnoseCustomization>(DEFAULTS);
67
+ const DiagnoseCustomizationContext =
68
+ createContext<DiagnoseCustomization>(DEFAULTS);
66
69
 
67
70
  export function DiagnoseCustomizationProvider({
68
71
  value,
package/src/index.css CHANGED
@@ -1,4 +1,5 @@
1
1
  @import "tailwindcss";
2
+
2
3
  /*
3
4
  * Tailwind v4 JIT scans only sources under this CSS file's package (web/).
4
5
  * k8s-ui is a workspace package whose TSX files also use Tailwind classes;
@@ -15,6 +16,25 @@
15
16
  @import "@fontsource-variable/dm-sans";
16
17
  @import "@fontsource/dm-mono";
17
18
 
19
+ @container investigation (min-width: 1000px) {
20
+ .investigation-split-enabled .investigation-evidence-jump { display: none; }
21
+ }
22
+
23
+ @layer components {
24
+ .investigation-assessment {
25
+ background: var(--color-investigation-assessment);
26
+ border-color: var(--color-investigation-assessment-border);
27
+ }
28
+ .investigation-evidence {
29
+ background: var(--color-investigation-evidence);
30
+ border-color: var(--color-investigation-evidence-border);
31
+ }
32
+ .investigation-next-steps {
33
+ background: var(--color-investigation-next-steps);
34
+ border-color: var(--color-investigation-next-steps-border);
35
+ }
36
+ }
37
+
18
38
  /*
19
39
  * Tailwind v4 theme tokens. Migrated from the legacy v3 tailwind.config.js
20
40
  * to CSS-first @theme so library consumers (e.g. radar-hub-web) don't need
@@ -22,6 +42,12 @@
22
42
  * when they consume this package from npm.
23
43
  */
24
44
  @theme {
45
+ --color-investigation-assessment: light-dark(#edf3fc, #182237);
46
+ --color-investigation-assessment-border: light-dark(#d3dff0, #2b3b56);
47
+ --color-investigation-evidence: light-dark(#f3f0fa, #211e30);
48
+ --color-investigation-evidence-border: light-dark(#e0d8ee, #39314b);
49
+ --color-investigation-next-steps: light-dark(#edf6f5, #172a2c);
50
+ --color-investigation-next-steps-border: light-dark(#cfe4e0, #2a4546);
25
51
  /* Fonts — DM Sans for UI, DM Mono for code/metric panes. */
26
52
  --font-sans: "DM Sans Variable", "DM Sans", system-ui, sans-serif;
27
53
  --font-mono: "DM Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
@@ -44,8 +70,8 @@
44
70
  --color-accent: var(--accent);
45
71
 
46
72
  /* Border radius — rounder than Tailwind defaults to match the brand. */
47
- --radius: 0.375rem; /* default (was 0.25rem) */
48
- --radius-md: 0.5rem; /* was 0.375rem */
73
+ --radius: 0.375rem; /* default (was 0.25rem) */
74
+ --radius-md: 0.5rem; /* was 0.375rem */
49
75
  --radius-lg: 0.625rem; /* was 0.5rem */
50
76
  --radius-xl: 0.875rem; /* was 0.75rem */
51
77
 
@@ -54,12 +80,26 @@
54
80
  }
55
81
 
56
82
  @keyframes fadeInOut {
57
- 0%, 100% { opacity: 0; transform: translateY(-8px); }
58
- 15%, 85% { opacity: 1; transform: translateY(0); }
83
+ 0%,
84
+ 100% {
85
+ opacity: 0;
86
+ transform: translateY(-8px);
87
+ }
88
+ 15%,
89
+ 85% {
90
+ opacity: 1;
91
+ transform: translateY(0);
92
+ }
59
93
  }
60
94
 
61
95
  /* Global: pointer cursor for all interactive elements */
62
- button, [role="button"], a, label, select, summary, [tabindex]:not([tabindex="-1"]) {
96
+ button,
97
+ [role="button"],
98
+ a,
99
+ label,
100
+ select,
101
+ summary,
102
+ [tabindex]:not([tabindex="-1"]) {
63
103
  cursor: pointer;
64
104
  }
65
105
 
@@ -69,7 +109,7 @@ button, [role="button"], a, label, select, summary, [tabindex]:not([tabindex="-1
69
109
 
70
110
  :root {
71
111
  /* Font settings */
72
- font-family: 'DM Sans Variable', 'DM Sans', system-ui, sans-serif;
112
+ font-family: "DM Sans Variable", "DM Sans", system-ui, sans-serif;
73
113
  line-height: 1.5;
74
114
  font-weight: 400;
75
115
  }
@@ -91,7 +131,6 @@ body {
91
131
  flex-direction: column;
92
132
  }
93
133
 
94
-
95
134
  /* ============================================
96
135
  REACT FLOW CUSTOMIZATIONS
97
136
  ============================================ */
@@ -218,8 +257,14 @@ body {
218
257
  }
219
258
 
220
259
  @keyframes slide-out-to-right {
221
- from { transform: translateX(0); opacity: 1; }
222
- to { transform: translateX(100%); opacity: 0; }
260
+ from {
261
+ transform: translateX(0);
262
+ opacity: 1;
263
+ }
264
+ to {
265
+ transform: translateX(100%);
266
+ opacity: 0;
267
+ }
223
268
  }
224
269
 
225
270
  .animate-out {
@@ -271,8 +316,12 @@ body {
271
316
  }
272
317
 
273
318
  @keyframes status-enter {
274
- from { opacity: 0; }
275
- to { opacity: 1; }
319
+ from {
320
+ opacity: 0;
321
+ }
322
+ to {
323
+ opacity: 1;
324
+ }
276
325
  }
277
326
  .animate-status-enter {
278
327
  animation: status-enter 140ms ease-out both;
@@ -315,24 +364,8 @@ body {
315
364
 
316
365
  /* Synthesis "thinking" dot — a calm breathing pulse (not a spinner) for the staged
317
366
  pre-verdict beats. */
318
- @keyframes synth-pulse {
319
- 0%,
320
- 100% {
321
- opacity: 0.35;
322
- transform: scale(0.85);
323
- }
324
- 50% {
325
- opacity: 1;
326
- transform: scale(1.15);
327
- }
328
- }
329
- .animate-synth-pulse {
330
- animation: synth-pulse 1.1s ease-in-out infinite;
331
- }
332
-
333
- /* Result-reveal choreography. The card eases in; the root-cause label gets an accent
334
- underline that sweeps out; the whole card pulses its accent border once to mark
335
- "diagnosis ready." Sequenced via animation-delay relative to one mount. */
367
+ /* Results enter with the same short spatial motion as new transcript/evidence rows.
368
+ Avoid pulsing glows or staged pauses: the evidence itself should carry emphasis. */
336
369
  @keyframes result-in {
337
370
  from {
338
371
  opacity: 0;
@@ -346,51 +379,6 @@ body {
346
379
  .animate-result-in {
347
380
  animation: result-in 320ms cubic-bezier(0.16, 1, 0.3, 1) both;
348
381
  }
349
- @keyframes underline-sweep {
350
- from {
351
- transform: scaleX(0);
352
- }
353
- to {
354
- transform: scaleX(1);
355
- }
356
- }
357
- .animate-underline-sweep {
358
- transform-origin: left;
359
- animation: underline-sweep 460ms cubic-bezier(0.16, 1, 0.3, 1) 120ms both;
360
- }
361
- /* Richer verdict reveal — a colored ring around the border plus an UNEVEN, breathing
362
- outer glow (two soft peaks), held longer (~2.4s). Tinted per verdict via --glow
363
- (amber root-cause, emerald healthy, accent remediation, slate inconclusive). */
364
- @keyframes verdict-reveal {
365
- 0% {
366
- box-shadow:
367
- 0 0 0 0 color-mix(in oklab, var(--glow, var(--accent)) 0%, transparent),
368
- 0 0 0 0 transparent;
369
- }
370
- 18% {
371
- box-shadow:
372
- 0 0 0 1px color-mix(in oklab, var(--glow, var(--accent)) 60%, transparent),
373
- 0 0 20px 2px color-mix(in oklab, var(--glow, var(--accent)) 42%, transparent);
374
- }
375
- 44% {
376
- box-shadow:
377
- 0 0 0 1px color-mix(in oklab, var(--glow, var(--accent)) 32%, transparent),
378
- 0 0 10px 1px color-mix(in oklab, var(--glow, var(--accent)) 20%, transparent);
379
- }
380
- 68% {
381
- box-shadow:
382
- 0 0 0 1px color-mix(in oklab, var(--glow, var(--accent)) 50%, transparent),
383
- 0 0 24px 3px color-mix(in oklab, var(--glow, var(--accent)) 38%, transparent);
384
- }
385
- 100% {
386
- box-shadow:
387
- 0 0 0 0 transparent,
388
- 0 0 0 0 transparent;
389
- }
390
- }
391
- .animate-verdict-reveal {
392
- animation: verdict-reveal 2400ms cubic-bezier(0.32, 0.72, 0, 1) 120ms both;
393
- }
394
382
  @media (prefers-reduced-motion: reduce) {
395
383
  .ai-shimmer {
396
384
  animation: none;
@@ -398,10 +386,7 @@ body {
398
386
  -webkit-text-fill-color: currentColor;
399
387
  color: var(--text-secondary);
400
388
  }
401
- .animate-synth-pulse,
402
- .animate-result-in,
403
- .animate-underline-sweep,
404
- .animate-verdict-reveal {
389
+ .animate-result-in {
405
390
  animation: none;
406
391
  }
407
392
  }
@@ -428,7 +413,6 @@ body {
428
413
  animation: slide-in-from-right 0.2s ease-out;
429
414
  }
430
415
 
431
-
432
416
  /* ============================================
433
417
  XTERM TERMINAL STYLES
434
418
  ============================================ */
package/src/index.ts CHANGED
@@ -31,7 +31,7 @@ export type {
31
31
  DiagnoseConsentCopy,
32
32
  } from './context/DiagnoseCustomization';
33
33
 
34
- // Standalone AI-diagnose surface — mount the investigation panel outside a
34
+ // Standalone AI investigation surface — mount the investigation panel outside a
35
35
  // full <RadarApp>. No router dependency, no client-side cluster state: the
36
36
  // backend set via setApiBase() picks the cluster, so hosts remount
37
37
  // <DiagnoseProvider key={cluster}> to switch. Mount order: ThemeProvider >