@skyhook-io/radar-app 1.13.1 → 1.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/App.tsx +19 -1
- package/src/RadarApp.tsx +2 -2
- package/src/api/diagnose.test.ts +268 -0
- package/src/api/diagnose.ts +74 -9
- package/src/components/diagnose/AISettings.tsx +1 -1
- package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
- package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
- package/src/components/diagnose/DiagnoseContext.tsx +12 -17
- package/src/components/diagnose/DiagnoseSurface.test.tsx +211 -16
- package/src/components/diagnose/DiagnoseSurface.tsx +464 -133
- package/src/components/diagnose/Home.test.tsx +293 -0
- package/src/components/diagnose/Home.tsx +289 -119
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +2170 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +2253 -0
- package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +257 -0
- package/src/components/diagnose/InvestigationResourceEvidence.tsx +214 -0
- package/src/components/diagnose/InvestigationView.test.ts +17 -0
- package/src/components/diagnose/InvestigationView.tsx +1913 -395
- package/src/components/diagnose/LocalDiagnoseAction.tsx +42 -25
- package/src/components/diagnose/agentCatalog.ts +1 -1
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +151 -0
- package/src/components/diagnose/investigationEvidence.test.ts +3109 -0
- package/src/components/diagnose/investigationEvidence.ts +3492 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +447 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +167 -0
- package/src/components/diagnose/investigationExplanation.test.ts +92 -0
- package/src/components/diagnose/investigationExplanation.ts +29 -0
- package/src/components/diagnose/investigationResourceEvidenceModel.ts +322 -0
- package/src/components/diagnose/investigationSourceFocus.test.ts +143 -0
- package/src/components/diagnose/investigationSourceFocus.ts +98 -0
- package/src/components/diagnose/investigationState.test.ts +695 -0
- package/src/components/diagnose/investigationState.ts +451 -0
- package/src/components/diagnose/parts.test.tsx +864 -3
- package/src/components/diagnose/parts.tsx +1337 -541
- package/src/components/diagnose/target.test.ts +39 -0
- package/src/components/diagnose/target.ts +36 -0
- package/src/components/diagnose/useDisclosureReveal.ts +117 -0
- package/src/components/home/MCPSetupDialog.tsx +2 -2
- package/src/components/home/mcpToolCatalog.test.ts +22 -0
- package/src/components/home/mcpToolCatalog.ts +3 -2
- package/src/components/issues/IssuesPane.tsx +5 -1
- package/src/components/settings/SettingsDialog.tsx +11 -13
- package/src/components/workload/WorkloadView.tsx +1 -1
- package/src/context/DiagnoseCustomization.tsx +11 -8
- package/src/index.css +63 -79
- package/src/index.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The single controller for the AI assistant surface. One instance app-wide:
|
|
2
|
-
// the per-resource "
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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 "./
|
|
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
|
|
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
|
+
});
|