@skyhook-io/radar-app 1.13.1 → 1.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +74 -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 +1913 -395
  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 +92 -0
  28. package/src/components/diagnose/investigationExplanation.ts +29 -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
@@ -1,5 +1,5 @@
1
1
  // The single controller for the AI assistant surface. One instance app-wide:
2
- // the per-resource "Diagnose" button and the global top-bar entry both dispatch
2
+ // the per-resource "Investigate" button and the global top-bar entry both dispatch
3
3
  // here. Investigations are durable, server-side jobs (see internal/ai RunManager);
4
4
  // this provider lists them, tracks which one is focused, and owns the push-content
5
5
  // layout. The run lifetime is the server's, so closing/navigating never kills one.
@@ -29,9 +29,12 @@ import {
29
29
  type AgentInfo,
30
30
  type ExecutionProfile,
31
31
  } from "../../api/diagnose";
32
+ import { runTargetKey } from "./target";
32
33
 
33
34
  export interface Target {
34
35
  kind: string;
36
+ /** Kubernetes API group; empty means core. */
37
+ group: string;
35
38
  namespace: string;
36
39
  name: string;
37
40
  /** The issue this investigation is for, when it came from an issue. Hosts
@@ -45,7 +48,7 @@ export interface Target {
45
48
  }
46
49
  export type DiagnoseView = "home" | "investigation";
47
50
 
48
- // Setup readiness of the local AI-diagnosis feature, derived from the agents API:
51
+ // Setup readiness of local AI investigations, derived from the agents API:
49
52
  // - "ready": an agent is installed and the engine is running (available)
50
53
  // - "needs-install": the feature is supported here but no agent CLI is installed
51
54
  // - "needs-restart": a supported agent is now on PATH but Radar booted before it
@@ -110,16 +113,6 @@ interface DiagnoseLayoutCtx {
110
113
  runningKeys: ReadonlySet<string>; // resources with a live investigation (see runTargetKey)
111
114
  }
112
115
 
113
- // Stable key for "is THIS resource being investigated right now" — built the same way
114
- // from a run summary and from a button's target so the two always match.
115
- export function runTargetKey(
116
- kind: string,
117
- namespace: string,
118
- name: string,
119
- ): string {
120
- return `${kind} ${namespace} ${name}`;
121
- }
122
-
123
116
  const Ctx = createContext<DiagnoseCtx | null>(null);
124
117
  const LayoutCtx = createContext<DiagnoseLayoutCtx | null>(null);
125
118
 
@@ -168,7 +161,7 @@ export function agentLabelFor(name: string, fallbackLabel?: string): string {
168
161
  }
169
162
 
170
163
  // openDiagnoseSettings opens the Settings dialog (App.tsx listens for this DOM
171
- // event) — the canonical home for AI-diagnosis config.
164
+ // event) — the canonical home for AI investigation config.
172
165
  export function openDiagnoseSettings() {
173
166
  window.dispatchEvent(
174
167
  new CustomEvent("radar:open-settings", { detail: { section: "ai" } }),
@@ -476,15 +469,17 @@ export function DiagnoseProvider({
476
469
  }, []);
477
470
 
478
471
  // A content-stable signature of the resources with a live investigation,
479
- // so the per-resource Diagnose buttons can show a "running" indicator even with the
472
+ // so the per-resource Investigate buttons can show a "running" indicator even with the
480
473
  // panel closed — and only re-render when the set actually changes, not every poll.
481
474
  const runningSig = runs
482
475
  .filter((r) => r.status === "running" || r.status === "stopping")
483
- .map((r) => runTargetKey(r.kind, r.namespace, r.name))
476
+ .map((r) => runTargetKey(r.kind, r.namespace, r.name, r.group))
484
477
  .sort()
485
- .join("|");
478
+ // resourceKey itself is pipe-delimited; newlines cannot occur in a
479
+ // Kubernetes group, Kind, namespace, or name.
480
+ .join("\n");
486
481
  const runningKeys = useMemo(
487
- () => new Set(runningSig ? runningSig.split("|") : []),
482
+ () => new Set(runningSig ? runningSig.split("\n") : []),
488
483
  [runningSig],
489
484
  );
490
485
  const hasRunning = runningSig.length > 0;
@@ -1,9 +1,21 @@
1
- import { describe, expect, it } from "vitest";
2
- import { canCopyRunLink, canStartNewInvestigation } from "./DiagnoseSurface";
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ DIAGNOSE_SURFACE_FRAME_CLASS,
4
+ MAXIMIZED_COMPACT_HISTORY_VISIBILITY_CLASS,
5
+ MAXIMIZED_HOME_DETAIL_VISIBILITY_CLASS,
6
+ MAXIMIZED_HOME_RUN_HEADER_VISIBILITY_CLASS,
7
+ INVESTIGATION_HISTORY_MIN_WIDTH,
8
+ MAXIMIZED_RUN_META_VISIBILITY_CLASS,
9
+ canStartNewInvestigation,
10
+ canCopyRunLink,
11
+ investigationHeaderPresentation,
12
+ openInvestigationEvidenceResource,
13
+ } from "./DiagnoseSurface";
3
14
  import {
4
15
  canContinueInvestigation,
16
+ canInvestigateFurther,
5
17
  canStopInvestigation,
6
- } from "./InvestigationView";
18
+ } from "./investigationState";
7
19
  import type { RunSummary } from "../../api/diagnose";
8
20
 
9
21
  // The "new investigation" button dispatches an agent and spends the user's own
@@ -13,6 +25,7 @@ function run(status: RunSummary["status"]): RunSummary {
13
25
  return {
14
26
  id: "r1",
15
27
  kind: "Deployment",
28
+ group: "apps",
16
29
  namespace: "prod",
17
30
  name: "payments",
18
31
  context: "prod-cluster",
@@ -43,13 +56,18 @@ describe("canStartNewInvestigation", () => {
43
56
  ).toBe(false);
44
57
  });
45
58
 
46
- it("stays hidden while a stopped turn is draining", () => {
59
+ it("stays hidden on a stale run", () => {
60
+ // A closed session must not start a new investigation against a resource
61
+ // that may not exist in the active cluster.
62
+ expect(canStartNewInvestigation("investigation", run("stale"), false)).toBe(
63
+ false,
64
+ );
65
+ });
66
+
67
+ it("blocks fresh starts while a human turn stops, but allows a separate human run from an automatic one", () => {
47
68
  expect(
48
69
  canStartNewInvestigation("investigation", run("stopping"), false),
49
70
  ).toBe(false);
50
- });
51
-
52
- it("offers a separate human investigation while an automatic run is in flight", () => {
53
71
  expect(
54
72
  canStartNewInvestigation(
55
73
  "investigation",
@@ -59,15 +77,6 @@ describe("canStartNewInvestigation", () => {
59
77
  ).toBe(true);
60
78
  });
61
79
 
62
- it("stays hidden on a stale run", () => {
63
- // The body offers "Re-run on current cluster" WITH the context-changed
64
- // warning. A bare + carries none of it, at a resource that may not exist in
65
- // the context it would now run against.
66
- expect(canStartNewInvestigation("investigation", run("stale"), false)).toBe(
67
- false,
68
- );
69
- });
70
-
71
80
  it("stays hidden while consent is pending", () => {
72
81
  expect(canStartNewInvestigation("investigation", run("done"), true)).toBe(
73
82
  false,
@@ -89,6 +98,152 @@ describe("canStartNewInvestigation", () => {
89
98
  });
90
99
  });
91
100
 
101
+ describe("investigation history navigation", () => {
102
+ it("restores the docked surface before opening an evidence resource", () => {
103
+ const events: string[] = [];
104
+ const onOpenResource = vi.fn(() => events.push("open"));
105
+ const setMaximized = vi.fn(() => events.push("dock"));
106
+ const closeDiagnose = vi.fn(() => events.push("close"));
107
+ const ref = {
108
+ kind: "Deployment",
109
+ group: "apps",
110
+ namespace: "shop",
111
+ name: "api",
112
+ };
113
+
114
+ openInvestigationEvidenceResource(
115
+ ref,
116
+ onOpenResource,
117
+ setMaximized,
118
+ closeDiagnose,
119
+ false,
120
+ );
121
+
122
+ expect(events).toEqual(["dock", "open"]);
123
+ expect(setMaximized).toHaveBeenCalledWith(false);
124
+ expect(closeDiagnose).not.toHaveBeenCalled();
125
+ expect(onOpenResource).toHaveBeenCalledWith(ref);
126
+ });
127
+
128
+ it("closes an overlay before opening an evidence resource", () => {
129
+ const events: string[] = [];
130
+ const onOpenResource = vi.fn(() => events.push("open"));
131
+ const setMaximized = vi.fn(() => events.push("dock"));
132
+ const closeDiagnose = vi.fn(() => events.push("close"));
133
+ const ref = {
134
+ kind: "Pod",
135
+ namespace: "shop",
136
+ name: "api-7d9f",
137
+ };
138
+
139
+ openInvestigationEvidenceResource(
140
+ ref,
141
+ onOpenResource,
142
+ setMaximized,
143
+ closeDiagnose,
144
+ true,
145
+ );
146
+
147
+ expect(events).toEqual(["close", "open"]);
148
+ expect(setMaximized).not.toHaveBeenCalled();
149
+ expect(closeDiagnose).toHaveBeenCalledOnce();
150
+ expect(onOpenResource).toHaveBeenCalledWith(ref);
151
+ });
152
+
153
+ it("keeps document overflow out of the bounded Diagnose frame", () => {
154
+ expect(DIAGNOSE_SURFACE_FRAME_CLASS).toContain("absolute");
155
+ expect(DIAGNOSE_SURFACE_FRAME_CLASS).toContain("min-h-0");
156
+ expect(DIAGNOSE_SURFACE_FRAME_CLASS).toContain("overflow-hidden");
157
+ expect(DIAGNOSE_SURFACE_FRAME_CLASS).not.toContain("overflow-y-auto");
158
+ });
159
+
160
+ it("reserves the history rail for wider investigation surfaces", () => {
161
+ expect(INVESTIGATION_HISTORY_MIN_WIDTH).toBe(1750);
162
+ expect(MAXIMIZED_COMPACT_HISTORY_VISIBILITY_CLASS).toBe(
163
+ "@min-[1750px]/diagnose-surface:hidden",
164
+ );
165
+ expect(MAXIMIZED_HOME_DETAIL_VISIBILITY_CLASS).toBe(
166
+ "hidden @min-[1750px]/diagnose-surface:flex",
167
+ );
168
+ expect(MAXIMIZED_HOME_RUN_HEADER_VISIBILITY_CLASS).toBe(
169
+ "hidden @min-[1750px]/diagnose-surface:block",
170
+ );
171
+ expect(MAXIMIZED_RUN_META_VISIBILITY_CLASS).toBe(
172
+ "hidden @min-[1750px]/diagnose-surface:flex",
173
+ );
174
+ });
175
+
176
+ it("keeps docked Home generic and removes actions for its retained run", () => {
177
+ expect(
178
+ investigationHeaderPresentation({
179
+ view: "home",
180
+ maximized: false,
181
+ hasVisibleRunDetail: true,
182
+ }),
183
+ ).toEqual({
184
+ genericIdentityClass: "",
185
+ detailIdentityClass: null,
186
+ runActionsClass: null,
187
+ });
188
+ });
189
+
190
+ it("swaps generic Home identity for the labeled retained detail at the wide breakpoint", () => {
191
+ expect(
192
+ investigationHeaderPresentation({
193
+ view: "home",
194
+ maximized: true,
195
+ hasVisibleRunDetail: true,
196
+ }),
197
+ ).toEqual({
198
+ genericIdentityClass: MAXIMIZED_COMPACT_HISTORY_VISIBILITY_CLASS,
199
+ detailIdentityClass: MAXIMIZED_HOME_RUN_HEADER_VISIBILITY_CLASS,
200
+ runActionsClass: MAXIMIZED_HOME_DETAIL_VISIBILITY_CLASS,
201
+ });
202
+ });
203
+
204
+ it("labels a direct detail at every size and keeps its run actions with it", () => {
205
+ expect(
206
+ investigationHeaderPresentation({
207
+ view: "investigation",
208
+ maximized: false,
209
+ hasVisibleRunDetail: true,
210
+ }),
211
+ ).toEqual({
212
+ genericIdentityClass: null,
213
+ detailIdentityClass: "",
214
+ runActionsClass: "",
215
+ });
216
+ });
217
+
218
+ it("does not invent a retained-detail header or actions without a run", () => {
219
+ expect(
220
+ investigationHeaderPresentation({
221
+ view: "home",
222
+ maximized: true,
223
+ hasVisibleRunDetail: false,
224
+ }),
225
+ ).toEqual({
226
+ genericIdentityClass: "",
227
+ detailIdentityClass: null,
228
+ runActionsClass: null,
229
+ });
230
+ });
231
+
232
+ it("removes run actions when another surface replaces the direct detail", () => {
233
+ expect(
234
+ investigationHeaderPresentation({
235
+ view: "investigation",
236
+ maximized: true,
237
+ hasVisibleRunDetail: false,
238
+ }),
239
+ ).toEqual({
240
+ genericIdentityClass: null,
241
+ detailIdentityClass: "",
242
+ runActionsClass: null,
243
+ });
244
+ });
245
+ });
246
+
92
247
  describe("canStopInvestigation", () => {
93
248
  it("lets the terminal transcript outrank a lagging running summary", () => {
94
249
  expect(canStopInvestigation(run("running"), false, false, "done")).toBe(
@@ -181,3 +336,43 @@ describe("canCopyRunLink", () => {
181
336
  expect(canCopyRunLink({ ...run("done"), radarUrl: "" })).toBe(false);
182
337
  });
183
338
  });
339
+
340
+ describe("canInvestigateFurther", () => {
341
+ it("offers a context-preserving next step on a completed automatic investigation", () => {
342
+ expect(
343
+ canInvestigateFurther({
344
+ ...run("done"),
345
+ trigger: "background",
346
+ issueId: "issue-1",
347
+ }),
348
+ ).toBe(true);
349
+ });
350
+
351
+ it("does not promise findings for missing, unfinished, stale or human investigations", () => {
352
+ expect(canInvestigateFurther(run("done"))).toBe(false);
353
+ expect(
354
+ canInvestigateFurther({ ...run("done"), trigger: "background" }),
355
+ ).toBe(false);
356
+ for (const status of [
357
+ "running",
358
+ "stopping",
359
+ "error",
360
+ "stopped",
361
+ "stale",
362
+ ] as const) {
363
+ expect(
364
+ canInvestigateFurther({
365
+ ...run(status),
366
+ trigger: "background",
367
+ issueId: "issue-1",
368
+ }),
369
+ ).toBe(false);
370
+ }
371
+ expect(
372
+ canInvestigateFurther(
373
+ { ...run("done"), trigger: "background", issueId: "issue-1" },
374
+ true,
375
+ ),
376
+ ).toBe(false);
377
+ });
378
+ });