@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.
- 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 +72 -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 +1900 -393
- 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 +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
|
@@ -3,19 +3,25 @@
|
|
|
3
3
|
// - expanded: a master-detail workspace that fills ONLY the content area (does
|
|
4
4
|
// not cover the left nav rail or top bar) — recent list on the left, the
|
|
5
5
|
// selected investigation/report on the right.
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useLayoutEffect,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from "react";
|
|
7
13
|
import {
|
|
8
14
|
Sparkles,
|
|
9
15
|
X,
|
|
10
16
|
Maximize2,
|
|
11
17
|
Minimize2,
|
|
12
|
-
ChevronLeft,
|
|
13
18
|
Settings2,
|
|
14
19
|
MoreVertical,
|
|
15
20
|
TerminalSquare,
|
|
16
21
|
Copy,
|
|
17
22
|
Check,
|
|
18
23
|
Plus,
|
|
24
|
+
PanelLeftOpen,
|
|
19
25
|
Link,
|
|
20
26
|
Lock,
|
|
21
27
|
Users,
|
|
@@ -23,6 +29,8 @@ import {
|
|
|
23
29
|
import { Tooltip } from "../ui/Tooltip";
|
|
24
30
|
import { ConfirmDialog } from "../ui/ConfirmDialog";
|
|
25
31
|
import { Badge } from "@skyhook-io/k8s-ui/components/ui/Badge";
|
|
32
|
+
import { useAnimatedUnmount } from "../../hooks/useAnimatedUnmount";
|
|
33
|
+
import { TRANSITION_BACKDROP, TRANSITION_DRAWER } from "../../utils/animation";
|
|
26
34
|
import {
|
|
27
35
|
useDiagnose,
|
|
28
36
|
useDiagnoseLayout,
|
|
@@ -32,7 +40,7 @@ import {
|
|
|
32
40
|
} from "./DiagnoseContext";
|
|
33
41
|
import { useDiagnoseCustomization } from "../../context/DiagnoseCustomization";
|
|
34
42
|
import { InvestigationView } from "./InvestigationView";
|
|
35
|
-
import { RecentList } from "./Home";
|
|
43
|
+
import { RecentList, absoluteTime, statusWord } from "./Home";
|
|
36
44
|
import { AgentSetupNotice } from "./AgentSetupNotice";
|
|
37
45
|
import { ConsentCard } from "./parts";
|
|
38
46
|
import { buildLaunchCommand, launchAgentLabel, openInTerminal } from "./launch";
|
|
@@ -43,6 +51,9 @@ import {
|
|
|
43
51
|
} from "../../api/diagnose";
|
|
44
52
|
import { routePath } from "../../api/config";
|
|
45
53
|
import { useCapabilitiesContext } from "../../contexts/CapabilitiesContext";
|
|
54
|
+
import { useContexts } from "../../api/client";
|
|
55
|
+
import { formatInvestigationTarget } from "./target";
|
|
56
|
+
import type { DiagnosisResourceRef } from "./diagnoseEvidenceTypes";
|
|
46
57
|
|
|
47
58
|
function capWord(s: string): string {
|
|
48
59
|
return s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
@@ -78,15 +89,35 @@ function buildConfigLine(cfg: {
|
|
|
78
89
|
function InvestigationMenu({ run }: { run: RunSummary }) {
|
|
79
90
|
const [open, setOpen] = useState(false);
|
|
80
91
|
const [copied, setCopied] = useState(false);
|
|
92
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
81
93
|
const { localTerminal } = useCapabilitiesContext();
|
|
82
94
|
const label = launchAgentLabel(run);
|
|
83
95
|
const command = buildLaunchCommand(
|
|
84
96
|
run,
|
|
85
97
|
`${window.location.origin}${routePath("/mcp")}`,
|
|
86
98
|
);
|
|
99
|
+
|
|
100
|
+
// The diagnose root is a CSS container, so a position:fixed click-away layer
|
|
101
|
+
// inside it is panel-bound rather than viewport-bound. Dismiss from the
|
|
102
|
+
// document instead; the anchored menu itself can stay next to its trigger.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!open) return;
|
|
105
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
106
|
+
if (!menuRef.current?.contains(event.target as Node)) setOpen(false);
|
|
107
|
+
};
|
|
108
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
109
|
+
if (event.key === "Escape") setOpen(false);
|
|
110
|
+
};
|
|
111
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
112
|
+
document.addEventListener("keydown", onKeyDown);
|
|
113
|
+
return () => {
|
|
114
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
115
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
116
|
+
};
|
|
117
|
+
}, [open]);
|
|
118
|
+
|
|
87
119
|
// No resumable session yet (or stale run) → nothing to hand off.
|
|
88
120
|
if (!command) return null;
|
|
89
|
-
|
|
90
121
|
const toggle = () => {
|
|
91
122
|
setCopied(false);
|
|
92
123
|
setOpen((v) => !v);
|
|
@@ -101,7 +132,7 @@ function InvestigationMenu({ run }: { run: RunSummary }) {
|
|
|
101
132
|
};
|
|
102
133
|
|
|
103
134
|
return (
|
|
104
|
-
<div className="relative flex items-center">
|
|
135
|
+
<div ref={menuRef} className="relative flex items-center">
|
|
105
136
|
<Tooltip content="More" position="bottom">
|
|
106
137
|
<button
|
|
107
138
|
onClick={toggle}
|
|
@@ -114,42 +145,39 @@ function InvestigationMenu({ run }: { run: RunSummary }) {
|
|
|
114
145
|
</button>
|
|
115
146
|
</Tooltip>
|
|
116
147
|
{open && (
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
|
|
148
|
+
<div className="absolute right-0 top-full z-20 mt-1 w-64 rounded-lg border border-theme-border bg-theme-surface py-1 shadow-theme-lg">
|
|
149
|
+
<button
|
|
150
|
+
onClick={copy}
|
|
151
|
+
className="flex w-full items-start gap-2 px-3 py-1.5 text-left text-sm text-theme-text-primary hover:bg-theme-hover"
|
|
152
|
+
>
|
|
153
|
+
{copied ? (
|
|
154
|
+
<Check className="mt-0.5 h-4 w-4 shrink-0 text-emerald-500" />
|
|
155
|
+
) : (
|
|
156
|
+
<Copy className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
157
|
+
)}
|
|
158
|
+
<span>
|
|
159
|
+
{copied ? "Copied ✓" : `Copy command to continue in ${label}`}
|
|
160
|
+
{!copied && (
|
|
161
|
+
<span className="block text-[11px] text-theme-text-tertiary">
|
|
162
|
+
Paste it wherever you run {label} — resumes this exact
|
|
163
|
+
session.
|
|
164
|
+
</span>
|
|
165
|
+
)}
|
|
166
|
+
</span>
|
|
167
|
+
</button>
|
|
168
|
+
{localTerminal && (
|
|
120
169
|
<button
|
|
121
|
-
onClick={
|
|
122
|
-
|
|
170
|
+
onClick={() => {
|
|
171
|
+
openInTerminal(command, "Radar Investigation");
|
|
172
|
+
setOpen(false);
|
|
173
|
+
}}
|
|
174
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm text-theme-text-primary hover:bg-theme-hover"
|
|
123
175
|
>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
) : (
|
|
127
|
-
<Copy className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
128
|
-
)}
|
|
129
|
-
<span>
|
|
130
|
-
{copied ? "Copied ✓" : `Copy command to continue in ${label}`}
|
|
131
|
-
{!copied && (
|
|
132
|
-
<span className="block text-[11px] text-theme-text-tertiary">
|
|
133
|
-
Paste it wherever you run {label} — resumes this exact
|
|
134
|
-
session.
|
|
135
|
-
</span>
|
|
136
|
-
)}
|
|
137
|
-
</span>
|
|
176
|
+
<TerminalSquare className="h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
177
|
+
Run in a Radar terminal
|
|
138
178
|
</button>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
onClick={() => {
|
|
142
|
-
openInTerminal(command, "Diagnose");
|
|
143
|
-
setOpen(false);
|
|
144
|
-
}}
|
|
145
|
-
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm text-theme-text-primary hover:bg-theme-hover"
|
|
146
|
-
>
|
|
147
|
-
<TerminalSquare className="h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
148
|
-
Run in a Radar terminal
|
|
149
|
-
</button>
|
|
150
|
-
)}
|
|
151
|
-
</div>
|
|
152
|
-
</>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
153
181
|
)}
|
|
154
182
|
</div>
|
|
155
183
|
);
|
|
@@ -161,10 +189,17 @@ export function canCopyRunLink(
|
|
|
161
189
|
return typeof run?.radarUrl === "string" && run.radarUrl.length > 0;
|
|
162
190
|
}
|
|
163
191
|
|
|
164
|
-
function CopyRunLink({
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
192
|
+
function CopyRunLink({
|
|
193
|
+
radarUrl,
|
|
194
|
+
visibility,
|
|
195
|
+
}: {
|
|
196
|
+
radarUrl: string;
|
|
197
|
+
visibility: RunSummary["visibility"];
|
|
198
|
+
}) {
|
|
199
|
+
const label =
|
|
200
|
+
visibility === "private"
|
|
201
|
+
? "Copy private link (only you can open it)"
|
|
202
|
+
: "Copy investigation link";
|
|
168
203
|
const [copyState, setCopyState] = useState<"idle" | "copied" | "error">(
|
|
169
204
|
"idle",
|
|
170
205
|
);
|
|
@@ -224,6 +259,16 @@ function VisibilityControl({
|
|
|
224
259
|
Organization
|
|
225
260
|
</Badge>
|
|
226
261
|
</Tooltip>
|
|
262
|
+
) : run.visibility === "private" ? (
|
|
263
|
+
<Tooltip
|
|
264
|
+
content="Only you can view this investigation. Other organization members don’t have access."
|
|
265
|
+
position="bottom"
|
|
266
|
+
>
|
|
267
|
+
<Badge severity="neutral" size="sm" className="shrink-0">
|
|
268
|
+
<Lock className="h-3 w-3" />
|
|
269
|
+
Private
|
|
270
|
+
</Badge>
|
|
271
|
+
</Tooltip>
|
|
227
272
|
) : null;
|
|
228
273
|
}
|
|
229
274
|
const shared = run.visibility === "organization";
|
|
@@ -277,6 +322,7 @@ function VisibilityControl({
|
|
|
277
322
|
title="Share this investigation?"
|
|
278
323
|
message="Everyone in your organization can read this entire investigation—including your questions and the logs and manifests Radar read—and can continue or stop it."
|
|
279
324
|
confirmLabel="Share with organization"
|
|
325
|
+
showWarning={false}
|
|
280
326
|
variant="warning"
|
|
281
327
|
isLoading={busy}
|
|
282
328
|
/>
|
|
@@ -295,12 +341,11 @@ function VisibilityControl({
|
|
|
295
341
|
// the last focused run. Without this the button dispatches an
|
|
296
342
|
// agent — real tokens — from a screen showing an unrelated list.
|
|
297
343
|
// run nothing to take a resource from.
|
|
298
|
-
// running/stopping
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
// and the resource may not exist in the context it'd run against.
|
|
344
|
+
// running/stopping human starts reuse the live run. Automatic investigations
|
|
345
|
+
// are immutable, so they retain a separate fresh human start.
|
|
346
|
+
// stale the original session is closed after a cluster switch;
|
|
347
|
+
// the resource may not exist in the active context. Do not
|
|
348
|
+
// silently start a different-cluster investigation from here.
|
|
304
349
|
// needsConsent the consent card owns the surface until it's answered.
|
|
305
350
|
export function canStartNewInvestigation(
|
|
306
351
|
view: DiagnoseView,
|
|
@@ -317,8 +362,158 @@ export function canStartNewInvestigation(
|
|
|
317
362
|
);
|
|
318
363
|
}
|
|
319
364
|
|
|
320
|
-
|
|
365
|
+
// Home shows a list plus retained detail only when the surface has room.
|
|
366
|
+
// Keep these Tailwind literals aligned with the measured rail threshold.
|
|
367
|
+
// Detail uses one stable history toggle; below this width it opens an overlay.
|
|
368
|
+
export const INVESTIGATION_HISTORY_MIN_WIDTH = 1750;
|
|
369
|
+
export const MAXIMIZED_COMPACT_HISTORY_VISIBILITY_CLASS =
|
|
370
|
+
"@min-[1750px]/diagnose-surface:hidden";
|
|
371
|
+
export const MAXIMIZED_HOME_DETAIL_VISIBILITY_CLASS =
|
|
372
|
+
"hidden @min-[1750px]/diagnose-surface:flex";
|
|
373
|
+
export const MAXIMIZED_HOME_RUN_HEADER_VISIBILITY_CLASS =
|
|
374
|
+
"hidden @min-[1750px]/diagnose-surface:block";
|
|
375
|
+
export const MAXIMIZED_RUN_META_VISIBILITY_CLASS =
|
|
376
|
+
"hidden @min-[1750px]/diagnose-surface:flex";
|
|
377
|
+
// The panel is a bounded absolute frame whose descendants own scrolling. If
|
|
378
|
+
// overflow remains visible here, a tall Activity/Findings tree contributes its
|
|
379
|
+
// full scroll height to the document even though the frame itself is viewport-
|
|
380
|
+
// sized, producing a large blank page tail below Radar. This applies equally to
|
|
381
|
+
// docked and maximized modes; their intended scroll roots are inside the frame.
|
|
382
|
+
export const DIAGNOSE_SURFACE_FRAME_CLASS =
|
|
383
|
+
"@container/diagnose-surface absolute z-40 flex min-h-0 flex-col overflow-hidden border-l border-theme-border bg-theme-surface shadow-drawer";
|
|
384
|
+
|
|
385
|
+
export function investigationHeaderPresentation(input: {
|
|
386
|
+
view: DiagnoseView;
|
|
387
|
+
maximized: boolean;
|
|
388
|
+
hasVisibleRunDetail: boolean;
|
|
389
|
+
}): {
|
|
390
|
+
genericIdentityClass: string | null;
|
|
391
|
+
detailIdentityClass: string | null;
|
|
392
|
+
runActionsClass: string | null;
|
|
393
|
+
} {
|
|
394
|
+
if (input.view !== "home") {
|
|
395
|
+
return {
|
|
396
|
+
genericIdentityClass: null,
|
|
397
|
+
detailIdentityClass: "",
|
|
398
|
+
runActionsClass: input.hasVisibleRunDetail ? "" : null,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const hasWideRetainedDetail = input.maximized && input.hasVisibleRunDetail;
|
|
402
|
+
return {
|
|
403
|
+
genericIdentityClass: hasWideRetainedDetail
|
|
404
|
+
? MAXIMIZED_COMPACT_HISTORY_VISIBILITY_CLASS
|
|
405
|
+
: "",
|
|
406
|
+
detailIdentityClass: hasWideRetainedDetail
|
|
407
|
+
? MAXIMIZED_HOME_RUN_HEADER_VISIBILITY_CLASS
|
|
408
|
+
: null,
|
|
409
|
+
runActionsClass: hasWideRetainedDetail
|
|
410
|
+
? MAXIMIZED_HOME_DETAIL_VISIBILITY_CLASS
|
|
411
|
+
: null,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function DiagnoseHeaderIdentity({
|
|
416
|
+
className,
|
|
417
|
+
title,
|
|
418
|
+
configLine,
|
|
419
|
+
runMeta,
|
|
420
|
+
onOpenSettings,
|
|
421
|
+
}: {
|
|
422
|
+
className: string;
|
|
423
|
+
title: string;
|
|
424
|
+
configLine: string;
|
|
425
|
+
runMeta?: {
|
|
426
|
+
label: string;
|
|
427
|
+
labelClass: string;
|
|
428
|
+
dateTime: string;
|
|
429
|
+
time: string;
|
|
430
|
+
};
|
|
431
|
+
onOpenSettings: (() => void) | null;
|
|
432
|
+
}) {
|
|
433
|
+
return (
|
|
434
|
+
<div className={`min-w-0 ${className}`}>
|
|
435
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
436
|
+
<div className="min-w-0 flex-1 truncate text-sm font-medium text-theme-text-primary">
|
|
437
|
+
{title}
|
|
438
|
+
</div>
|
|
439
|
+
{runMeta ? (
|
|
440
|
+
<div
|
|
441
|
+
className={`${MAXIMIZED_RUN_META_VISIBILITY_CLASS} shrink-0 items-center gap-1 text-[11px] tabular-nums text-theme-text-tertiary`}
|
|
442
|
+
>
|
|
443
|
+
<span className={`font-medium ${runMeta.labelClass}`}>
|
|
444
|
+
{runMeta.label}
|
|
445
|
+
</span>
|
|
446
|
+
<span aria-hidden>·</span>
|
|
447
|
+
<time dateTime={runMeta.dateTime}>{runMeta.time}</time>
|
|
448
|
+
</div>
|
|
449
|
+
) : null}
|
|
450
|
+
</div>
|
|
451
|
+
<div className="flex items-center gap-1 text-xs text-theme-text-tertiary">
|
|
452
|
+
<span className="truncate">{configLine}</span>
|
|
453
|
+
{onOpenSettings && (
|
|
454
|
+
<Tooltip content="AI settings" position="bottom">
|
|
455
|
+
<button
|
|
456
|
+
onClick={onOpenSettings}
|
|
457
|
+
className="shrink-0 rounded p-0.5 text-theme-text-tertiary hover:text-theme-text-primary"
|
|
458
|
+
aria-label="AI settings"
|
|
459
|
+
>
|
|
460
|
+
<Settings2 className="h-3 w-3" />
|
|
461
|
+
</button>
|
|
462
|
+
</Tooltip>
|
|
463
|
+
)}
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function openInvestigationEvidenceResource(
|
|
470
|
+
ref: DiagnosisResourceRef,
|
|
471
|
+
onOpenResource: (ref: DiagnosisResourceRef) => void,
|
|
472
|
+
setMaximized: (maximized: boolean) => void,
|
|
473
|
+
closeDiagnose: () => void,
|
|
474
|
+
dockedPanelWouldOverlay: boolean,
|
|
475
|
+
) {
|
|
476
|
+
// A resource destination must be visible after the handoff. On a wide canvas,
|
|
477
|
+
// restoring the docked panel leaves Radar and the investigation side by side.
|
|
478
|
+
// At tighter widths that same panel overlays the host content, so close it
|
|
479
|
+
// before navigating instead of making the click appear to do nothing.
|
|
480
|
+
if (dockedPanelWouldOverlay) {
|
|
481
|
+
// Closing already exposes the destination; retain the user's maximized
|
|
482
|
+
// preference for the next time they open investigations.
|
|
483
|
+
closeDiagnose();
|
|
484
|
+
} else {
|
|
485
|
+
setMaximized(false);
|
|
486
|
+
}
|
|
487
|
+
onOpenResource(ref);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function DiagnoseSurface({
|
|
491
|
+
topInset = 0,
|
|
492
|
+
onOpenResource,
|
|
493
|
+
}: {
|
|
494
|
+
topInset?: number;
|
|
495
|
+
/** Resolves an evidence subject into the embedding Radar surface. */
|
|
496
|
+
onOpenResource?: (ref: DiagnosisResourceRef) => void;
|
|
497
|
+
}) {
|
|
321
498
|
const d = useDiagnose();
|
|
499
|
+
const { data: contexts } = useContexts();
|
|
500
|
+
const currentContext = contexts?.find((context) => context.isCurrent)?.name;
|
|
501
|
+
const [historyCollapsed, setHistoryCollapsed] = useState(false);
|
|
502
|
+
const [historyOverlayOpen, setHistoryOverlayOpen] = useState(false);
|
|
503
|
+
const [surfaceWidth, setSurfaceWidth] = useState(0);
|
|
504
|
+
const surfaceRef = useRef<HTMLDivElement>(null);
|
|
505
|
+
const historyRef = useRef<HTMLElement>(null);
|
|
506
|
+
const historyButtonRef = useRef<HTMLButtonElement>(null);
|
|
507
|
+
useLayoutEffect(() => {
|
|
508
|
+
const element = surfaceRef.current;
|
|
509
|
+
if (!element) return;
|
|
510
|
+
const observer = new ResizeObserver(() =>
|
|
511
|
+
setSurfaceWidth(element.clientWidth),
|
|
512
|
+
);
|
|
513
|
+
setSurfaceWidth(element.clientWidth);
|
|
514
|
+
observer.observe(element);
|
|
515
|
+
return () => observer.disconnect();
|
|
516
|
+
}, []);
|
|
322
517
|
// Injected settings action: undefined = Radar's own Settings dialog;
|
|
323
518
|
// null = hide the gear + links.
|
|
324
519
|
const { consentCopy, onOpenSettings: hostOpenSettings } =
|
|
@@ -334,6 +529,41 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
334
529
|
panelBounds: { min: minW, max: maxW },
|
|
335
530
|
panelWidthKey: widthKey,
|
|
336
531
|
} = useDiagnoseLayout();
|
|
532
|
+
const wideHistory =
|
|
533
|
+
maximized && surfaceWidth >= INVESTIGATION_HISTORY_MIN_WIDTH;
|
|
534
|
+
const historyOverlay =
|
|
535
|
+
!wideHistory && historyOverlayOpen && d.view !== "home";
|
|
536
|
+
const { shouldRender: historyOverlayPresent, isOpen: historySlideOpen } =
|
|
537
|
+
useAnimatedUnmount(historyOverlay);
|
|
538
|
+
const dismissHistory = useCallback(() => {
|
|
539
|
+
setHistoryOverlayOpen(false);
|
|
540
|
+
historyButtonRef.current?.focus({ preventScroll: true });
|
|
541
|
+
}, []);
|
|
542
|
+
useEffect(() => {
|
|
543
|
+
setHistoryOverlayOpen(false);
|
|
544
|
+
}, [wideHistory, maximized, d.view]);
|
|
545
|
+
useEffect(() => {
|
|
546
|
+
if (historyOverlay) {
|
|
547
|
+
const target =
|
|
548
|
+
historyRef.current?.querySelector<HTMLElement>(
|
|
549
|
+
'[aria-current="true"]',
|
|
550
|
+
) ?? historyRef.current?.querySelector<HTMLElement>("button");
|
|
551
|
+
(target ?? historyRef.current)?.focus({ preventScroll: true });
|
|
552
|
+
}
|
|
553
|
+
}, [historyOverlay]);
|
|
554
|
+
const openEvidenceResource = useCallback(
|
|
555
|
+
(ref: DiagnosisResourceRef) => {
|
|
556
|
+
if (!onOpenResource) return;
|
|
557
|
+
openInvestigationEvidenceResource(
|
|
558
|
+
ref,
|
|
559
|
+
onOpenResource,
|
|
560
|
+
setMaximized,
|
|
561
|
+
d.close,
|
|
562
|
+
narrow,
|
|
563
|
+
);
|
|
564
|
+
},
|
|
565
|
+
[d.close, narrow, onOpenResource, setMaximized],
|
|
566
|
+
);
|
|
337
567
|
|
|
338
568
|
const startResize = (e: React.MouseEvent) => {
|
|
339
569
|
e.preventDefault();
|
|
@@ -363,29 +593,42 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
363
593
|
d.setupState === "needs-install" || d.setupState === "needs-restart";
|
|
364
594
|
|
|
365
595
|
const activeRun = d.runs.find((r) => r.id === d.activeRunId) ?? null;
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
// therefore keeps the selected run active beside its history list.
|
|
370
|
-
const headerRun = !maximized && d.view === "home" ? null : activeRun;
|
|
596
|
+
// Consent replaces the detail pane even if goHome retained an older run id.
|
|
597
|
+
// Header identity and actions must describe what is actually visible.
|
|
598
|
+
const visibleRunDetail = d.needsConsent ? null : activeRun;
|
|
371
599
|
// A focused run shows the agent it actually ran with; Home reflects the current pick.
|
|
372
|
-
const activeAgentLabel =
|
|
373
|
-
? agentLabelFor(
|
|
600
|
+
const activeAgentLabel = visibleRunDetail?.agent
|
|
601
|
+
? agentLabelFor(visibleRunDetail.agent)
|
|
374
602
|
: d.agentLabel;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
},
|
|
603
|
+
const defaultHeaderConfig = {
|
|
604
|
+
agent: d.selectedAgent,
|
|
605
|
+
profile: d.hosted ? undefined : d.profile,
|
|
606
|
+
model: d.model,
|
|
607
|
+
effort: d.effort,
|
|
608
|
+
};
|
|
609
|
+
const genericConfigLine = buildConfigLine(defaultHeaderConfig);
|
|
610
|
+
const focusedConfigLine = buildConfigLine(
|
|
611
|
+
visibleRunDetail ?? defaultHeaderConfig,
|
|
385
612
|
);
|
|
386
|
-
const
|
|
387
|
-
?
|
|
613
|
+
const focusedTitle = visibleRunDetail
|
|
614
|
+
? formatInvestigationTarget(visibleRunDetail)
|
|
388
615
|
: "AI investigations";
|
|
616
|
+
const headerPresentation = investigationHeaderPresentation({
|
|
617
|
+
view: d.view,
|
|
618
|
+
maximized,
|
|
619
|
+
hasVisibleRunDetail: !!visibleRunDetail,
|
|
620
|
+
});
|
|
621
|
+
const focusedRunMeta = visibleRunDetail
|
|
622
|
+
? {
|
|
623
|
+
label: statusWord(visibleRunDetail.status).text,
|
|
624
|
+
labelClass: statusWord(visibleRunDetail.status).cls,
|
|
625
|
+
dateTime: visibleRunDetail.updatedAt,
|
|
626
|
+
time: absoluteTime(
|
|
627
|
+
new Date(visibleRunDetail.updatedAt).getTime(),
|
|
628
|
+
Date.now(),
|
|
629
|
+
),
|
|
630
|
+
}
|
|
631
|
+
: undefined;
|
|
389
632
|
|
|
390
633
|
// Absolute within the body frame: maximized fills it; docked is a right slot.
|
|
391
634
|
// topInset clears the header (the frame spans the full column incl. the header).
|
|
@@ -416,6 +659,7 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
416
659
|
run={activeRun}
|
|
417
660
|
agentLabel={activeAgentLabel}
|
|
418
661
|
maximized={maximized}
|
|
662
|
+
onOpenResource={onOpenResource ? openEvidenceResource : undefined}
|
|
419
663
|
/>
|
|
420
664
|
) : d.activeRunId && !d.runsLoaded ? (
|
|
421
665
|
// Deep-linked to a run before the list has ever loaded: show the load
|
|
@@ -459,20 +703,51 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
459
703
|
</div>
|
|
460
704
|
) : (
|
|
461
705
|
<div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-theme-text-tertiary">
|
|
462
|
-
Select an investigation, or open a resource and click
|
|
706
|
+
Select an investigation, or open a resource and click Investigate.
|
|
463
707
|
</div>
|
|
464
708
|
);
|
|
709
|
+
const compactHistory = (
|
|
710
|
+
<>
|
|
711
|
+
{setupPending && <AgentSetupNotice setupState={d.setupState} />}
|
|
712
|
+
{(!setupPending || d.runs.length > 0) && (
|
|
713
|
+
<RecentList
|
|
714
|
+
currentContext={currentContext}
|
|
715
|
+
agentLabel={d.agentLabel}
|
|
716
|
+
runs={d.runs}
|
|
717
|
+
onSelect={d.openRun}
|
|
718
|
+
historyDegraded={d.historyDegraded}
|
|
719
|
+
/>
|
|
720
|
+
)}
|
|
721
|
+
</>
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
const showHistory = !setupPending || d.runs.length > 0;
|
|
725
|
+
const historyVisible =
|
|
726
|
+
showHistory &&
|
|
727
|
+
(wideHistory ? !historyCollapsed || d.view === "home" : historyOverlay);
|
|
465
728
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
729
|
+
useLayoutEffect(() => {
|
|
730
|
+
if (
|
|
731
|
+
!historyVisible &&
|
|
732
|
+
historyRef.current?.contains(document.activeElement)
|
|
733
|
+
) {
|
|
734
|
+
historyButtonRef.current?.focus({ preventScroll: true });
|
|
735
|
+
}
|
|
736
|
+
}, [historyVisible]);
|
|
470
737
|
|
|
471
738
|
return (
|
|
472
739
|
<div
|
|
740
|
+
ref={surfaceRef}
|
|
473
741
|
role="dialog"
|
|
474
742
|
aria-label="AI investigations"
|
|
475
|
-
|
|
743
|
+
onKeyDownCapture={(event) => {
|
|
744
|
+
if (historyOverlay && event.key === "Escape") {
|
|
745
|
+
event.preventDefault();
|
|
746
|
+
event.stopPropagation();
|
|
747
|
+
dismissHistory();
|
|
748
|
+
}
|
|
749
|
+
}}
|
|
750
|
+
className={DIAGNOSE_SURFACE_FRAME_CLASS}
|
|
476
751
|
style={{
|
|
477
752
|
...positionStyle,
|
|
478
753
|
animation: "slide-in-from-right 0.22s cubic-bezier(0.32,0.72,0,1)",
|
|
@@ -490,70 +765,103 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
490
765
|
|
|
491
766
|
{/* Header */}
|
|
492
767
|
<div className="flex items-center justify-between border-b border-theme-border px-4 py-2.5">
|
|
493
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
494
|
-
{
|
|
495
|
-
<
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
768
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
769
|
+
{d.view !== "home" && showHistory ? (
|
|
770
|
+
<Tooltip
|
|
771
|
+
content={
|
|
772
|
+
historyVisible
|
|
773
|
+
? "Hide investigation history"
|
|
774
|
+
: "Show investigation history"
|
|
775
|
+
}
|
|
776
|
+
position="bottom"
|
|
777
|
+
className="pointer-events-none"
|
|
778
|
+
>
|
|
499
779
|
<button
|
|
500
|
-
|
|
501
|
-
|
|
780
|
+
ref={historyButtonRef}
|
|
781
|
+
type="button"
|
|
782
|
+
aria-label="Investigations"
|
|
783
|
+
aria-expanded={historyVisible}
|
|
784
|
+
aria-controls="investigation-history"
|
|
785
|
+
onClick={() =>
|
|
786
|
+
wideHistory
|
|
787
|
+
? setHistoryCollapsed((value) => !value)
|
|
788
|
+
: historyOverlay
|
|
789
|
+
? dismissHistory()
|
|
790
|
+
: setHistoryOverlayOpen(true)
|
|
791
|
+
}
|
|
792
|
+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-theme-text-secondary hover:bg-theme-hover focus-visible:ring-2 focus-visible:ring-accent"
|
|
502
793
|
>
|
|
503
|
-
<
|
|
504
|
-
Investigations
|
|
794
|
+
<PanelLeftOpen className="h-4 w-4" />
|
|
505
795
|
</button>
|
|
796
|
+
</Tooltip>
|
|
797
|
+
) : (
|
|
798
|
+
<span className="flex h-8 w-8 shrink-0 items-center justify-center">
|
|
799
|
+
<Sparkles className="h-4 w-4 text-accent" />
|
|
800
|
+
</span>
|
|
801
|
+
)}
|
|
802
|
+
<div className="min-w-0 flex-1">
|
|
803
|
+
{headerPresentation.genericIdentityClass !== null && (
|
|
804
|
+
<DiagnoseHeaderIdentity
|
|
805
|
+
className={headerPresentation.genericIdentityClass}
|
|
806
|
+
title="AI investigations"
|
|
807
|
+
configLine={genericConfigLine}
|
|
808
|
+
onOpenSettings={openSettings}
|
|
809
|
+
/>
|
|
810
|
+
)}
|
|
811
|
+
{headerPresentation.detailIdentityClass !== null && (
|
|
812
|
+
<DiagnoseHeaderIdentity
|
|
813
|
+
className={headerPresentation.detailIdentityClass}
|
|
814
|
+
title={focusedTitle}
|
|
815
|
+
configLine={focusedConfigLine}
|
|
816
|
+
runMeta={focusedRunMeta}
|
|
817
|
+
onOpenSettings={openSettings}
|
|
818
|
+
/>
|
|
506
819
|
)}
|
|
507
|
-
<div className="truncate text-sm font-medium text-theme-text-primary">
|
|
508
|
-
{detailTitle}
|
|
509
|
-
</div>
|
|
510
|
-
<div className="flex items-center gap-1 text-xs text-theme-text-tertiary">
|
|
511
|
-
<span className="truncate">{configLine}</span>
|
|
512
|
-
{openSettings && (
|
|
513
|
-
<Tooltip content="AI settings" position="bottom">
|
|
514
|
-
<button
|
|
515
|
-
onClick={openSettings}
|
|
516
|
-
className="shrink-0 rounded p-0.5 text-theme-text-tertiary hover:text-theme-text-primary"
|
|
517
|
-
aria-label="AI settings"
|
|
518
|
-
>
|
|
519
|
-
<Settings2 className="h-3 w-3" />
|
|
520
|
-
</button>
|
|
521
|
-
</Tooltip>
|
|
522
|
-
)}
|
|
523
|
-
</div>
|
|
524
820
|
</div>
|
|
525
821
|
</div>
|
|
526
822
|
<div className="flex shrink-0 items-center gap-0.5">
|
|
527
|
-
{
|
|
528
|
-
canStartNewInvestigation(d.view,
|
|
823
|
+
{activeRun &&
|
|
824
|
+
canStartNewInvestigation(d.view, activeRun, d.needsConsent) && (
|
|
529
825
|
<Tooltip
|
|
530
|
-
content="
|
|
826
|
+
content="Start fresh — ignore earlier findings"
|
|
531
827
|
position="bottom"
|
|
532
828
|
>
|
|
533
829
|
<button
|
|
534
830
|
onClick={() =>
|
|
535
831
|
d.openInvestigation({
|
|
536
|
-
kind:
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
832
|
+
kind: activeRun.kind,
|
|
833
|
+
group: activeRun.group,
|
|
834
|
+
namespace: activeRun.namespace,
|
|
835
|
+
name: activeRun.name,
|
|
836
|
+
issueId: activeRun.issueId,
|
|
540
837
|
fresh: true,
|
|
541
838
|
})
|
|
542
839
|
}
|
|
543
840
|
className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
544
|
-
aria-label="
|
|
841
|
+
aria-label="Start fresh — ignore earlier findings"
|
|
545
842
|
>
|
|
546
843
|
<Plus className="h-4 w-4" />
|
|
547
844
|
</button>
|
|
548
845
|
</Tooltip>
|
|
549
846
|
)}
|
|
550
|
-
{
|
|
551
|
-
<
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
847
|
+
{visibleRunDetail && headerPresentation.runActionsClass !== null && (
|
|
848
|
+
<div
|
|
849
|
+
className={`items-center gap-1 ${headerPresentation.runActionsClass || "flex"}`}
|
|
850
|
+
>
|
|
851
|
+
<VisibilityControl
|
|
852
|
+
key={visibleRunDetail.id}
|
|
853
|
+
run={visibleRunDetail}
|
|
854
|
+
onChanged={d.updateRunSummary}
|
|
855
|
+
/>
|
|
856
|
+
{canCopyRunLink(visibleRunDetail) && (
|
|
857
|
+
<CopyRunLink
|
|
858
|
+
radarUrl={visibleRunDetail.radarUrl}
|
|
859
|
+
visibility={visibleRunDetail.visibility}
|
|
860
|
+
/>
|
|
861
|
+
)}
|
|
862
|
+
<InvestigationMenu run={visibleRunDetail} />
|
|
863
|
+
</div>
|
|
555
864
|
)}
|
|
556
|
-
{headerRun && <InvestigationMenu run={headerRun} />}
|
|
557
865
|
<Tooltip content={maximized ? "Restore" : "Expand"} position="bottom">
|
|
558
866
|
<button
|
|
559
867
|
onClick={() => setMaximized((v) => !v)}
|
|
@@ -584,38 +892,61 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
584
892
|
(which would discard its transcript and re-run the agent). The aside
|
|
585
893
|
only appears when expanded; keys keep the detail node identity-stable
|
|
586
894
|
as it comes and goes. */}
|
|
587
|
-
<div className="flex min-h-0 flex-1">
|
|
588
|
-
{
|
|
895
|
+
<div className="relative flex min-h-0 flex-1">
|
|
896
|
+
{!wideHistory && historyOverlayPresent && (
|
|
897
|
+
<div
|
|
898
|
+
aria-hidden="true"
|
|
899
|
+
onClick={dismissHistory}
|
|
900
|
+
className={`absolute inset-0 z-10 bg-black/20 ${TRANSITION_BACKDROP} motion-reduce:transition-none ${historySlideOpen ? "opacity-100" : "opacity-0"} ${historyOverlay ? "" : "pointer-events-none"}`}
|
|
901
|
+
/>
|
|
902
|
+
)}
|
|
903
|
+
{showHistory && (
|
|
589
904
|
<aside
|
|
590
905
|
key="recent"
|
|
591
|
-
|
|
906
|
+
ref={historyRef}
|
|
907
|
+
tabIndex={-1}
|
|
908
|
+
aria-label="Investigation history"
|
|
909
|
+
aria-hidden={!historyVisible}
|
|
910
|
+
inert={!historyVisible}
|
|
911
|
+
id="investigation-history"
|
|
912
|
+
className={`${historyVisible || (!wideHistory && historyOverlayPresent) ? "block" : "hidden"} ${wideHistory ? "" : `absolute inset-y-0 left-0 z-20 max-w-[calc(100%-2rem)] shadow-drawer ${TRANSITION_DRAWER} motion-reduce:transition-none ${historySlideOpen ? "translate-x-0 opacity-100" : "-translate-x-full opacity-0"}`} w-72 shrink-0 overflow-y-auto border-r border-theme-border bg-theme-surface px-3 py-3 outline-none`}
|
|
592
913
|
>
|
|
593
914
|
<RecentList
|
|
915
|
+
currentContext={currentContext}
|
|
594
916
|
agentLabel={d.agentLabel}
|
|
595
917
|
runs={d.runs}
|
|
596
918
|
selectedId={d.activeRunId}
|
|
597
|
-
onSelect={
|
|
919
|
+
onSelect={(id) => {
|
|
920
|
+
d.openRun(id);
|
|
921
|
+
if (historyOverlay) dismissHistory();
|
|
922
|
+
}}
|
|
598
923
|
historyDegraded={d.historyDegraded}
|
|
599
924
|
/>
|
|
600
925
|
</aside>
|
|
601
926
|
)}
|
|
602
|
-
{
|
|
927
|
+
{d.view === "home" ? (
|
|
928
|
+
<>
|
|
929
|
+
<div
|
|
930
|
+
key="history"
|
|
931
|
+
className={`flex-1 overflow-y-auto overflow-x-hidden px-4 py-3 ${maximized ? MAXIMIZED_COMPACT_HISTORY_VISIBILITY_CLASS : ""}`}
|
|
932
|
+
>
|
|
933
|
+
{compactHistory}
|
|
934
|
+
</div>
|
|
935
|
+
{maximized && (
|
|
936
|
+
<div
|
|
937
|
+
key="main"
|
|
938
|
+
className={`${MAXIMIZED_HOME_DETAIL_VISIBILITY_CLASS} min-h-0 min-w-0 flex-1 flex-col`}
|
|
939
|
+
>
|
|
940
|
+
{detail}
|
|
941
|
+
</div>
|
|
942
|
+
)}
|
|
943
|
+
</>
|
|
944
|
+
) : (
|
|
603
945
|
<div
|
|
604
946
|
key="main"
|
|
605
|
-
|
|
947
|
+
inert={historyOverlay}
|
|
948
|
+
className="flex min-h-0 min-w-0 flex-1 flex-col"
|
|
606
949
|
>
|
|
607
|
-
{setupPending && <AgentSetupNotice setupState={d.setupState} />}
|
|
608
|
-
{(!setupPending || d.runs.length > 0) && (
|
|
609
|
-
<RecentList
|
|
610
|
-
agentLabel={d.agentLabel}
|
|
611
|
-
runs={d.runs}
|
|
612
|
-
onSelect={d.openRun}
|
|
613
|
-
historyDegraded={d.historyDegraded}
|
|
614
|
-
/>
|
|
615
|
-
)}
|
|
616
|
-
</div>
|
|
617
|
-
) : (
|
|
618
|
-
<div key="main" className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
619
950
|
{detail}
|
|
620
951
|
</div>
|
|
621
952
|
)}
|