@skyhook-io/radar-app 1.13.0 → 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 +55 -9
- package/src/RadarApp.tsx +3 -3
- package/src/api/diagnose.test.ts +268 -0
- package/src/api/diagnose.ts +117 -14
- package/src/components/diagnose/AISettings.tsx +10 -5
- package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
- package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
- package/src/components/diagnose/DiagnoseContext.tsx +228 -65
- package/src/components/diagnose/DiagnoseSurface.test.tsx +317 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +621 -132
- package/src/components/diagnose/Home.test.tsx +293 -0
- package/src/components/diagnose/Home.tsx +307 -108
- 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 +1911 -338
- 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 +1340 -536
- 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/execution/BatchExecutionView.render.test.tsx +35 -0
- package/src/components/execution/BatchExecutionView.tsx +2 -3
- package/src/components/execution/execution-definition.test.ts +19 -0
- package/src/components/execution/execution-definition.ts +2 -0
- package/src/components/gitops/GitOpsView.tsx +25 -3
- 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/nav/navigation.test.ts +26 -0
- package/src/components/nav/navigation.ts +10 -0
- 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,21 +3,34 @@
|
|
|
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,
|
|
25
|
+
Link,
|
|
26
|
+
Lock,
|
|
27
|
+
Users,
|
|
19
28
|
} from "lucide-react";
|
|
20
29
|
import { Tooltip } from "../ui/Tooltip";
|
|
30
|
+
import { ConfirmDialog } from "../ui/ConfirmDialog";
|
|
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";
|
|
21
34
|
import {
|
|
22
35
|
useDiagnose,
|
|
23
36
|
useDiagnoseLayout,
|
|
@@ -27,13 +40,20 @@ import {
|
|
|
27
40
|
} from "./DiagnoseContext";
|
|
28
41
|
import { useDiagnoseCustomization } from "../../context/DiagnoseCustomization";
|
|
29
42
|
import { InvestigationView } from "./InvestigationView";
|
|
30
|
-
import { RecentList } from "./Home";
|
|
43
|
+
import { RecentList, absoluteTime, statusWord } from "./Home";
|
|
31
44
|
import { AgentSetupNotice } from "./AgentSetupNotice";
|
|
32
45
|
import { ConsentCard } from "./parts";
|
|
33
46
|
import { buildLaunchCommand, launchAgentLabel, openInTerminal } from "./launch";
|
|
34
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
updateRunVisibility,
|
|
49
|
+
type RunSummary,
|
|
50
|
+
type ExecutionProfile,
|
|
51
|
+
} from "../../api/diagnose";
|
|
35
52
|
import { routePath } from "../../api/config";
|
|
36
53
|
import { useCapabilitiesContext } from "../../contexts/CapabilitiesContext";
|
|
54
|
+
import { useContexts } from "../../api/client";
|
|
55
|
+
import { formatInvestigationTarget } from "./target";
|
|
56
|
+
import type { DiagnosisResourceRef } from "./diagnoseEvidenceTypes";
|
|
37
57
|
|
|
38
58
|
function capWord(s: string): string {
|
|
39
59
|
return s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
@@ -69,12 +89,35 @@ function buildConfigLine(cfg: {
|
|
|
69
89
|
function InvestigationMenu({ run }: { run: RunSummary }) {
|
|
70
90
|
const [open, setOpen] = useState(false);
|
|
71
91
|
const [copied, setCopied] = useState(false);
|
|
92
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
72
93
|
const { localTerminal } = useCapabilitiesContext();
|
|
73
94
|
const label = launchAgentLabel(run);
|
|
74
|
-
const command = buildLaunchCommand(
|
|
95
|
+
const command = buildLaunchCommand(
|
|
96
|
+
run,
|
|
97
|
+
`${window.location.origin}${routePath("/mcp")}`,
|
|
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
|
+
|
|
75
119
|
// No resumable session yet (or stale run) → nothing to hand off.
|
|
76
120
|
if (!command) return null;
|
|
77
|
-
|
|
78
121
|
const toggle = () => {
|
|
79
122
|
setCopied(false);
|
|
80
123
|
setOpen((v) => !v);
|
|
@@ -89,7 +132,7 @@ function InvestigationMenu({ run }: { run: RunSummary }) {
|
|
|
89
132
|
};
|
|
90
133
|
|
|
91
134
|
return (
|
|
92
|
-
<div className="relative flex items-center">
|
|
135
|
+
<div ref={menuRef} className="relative flex items-center">
|
|
93
136
|
<Tooltip content="More" position="bottom">
|
|
94
137
|
<button
|
|
95
138
|
onClick={toggle}
|
|
@@ -102,47 +145,191 @@ function InvestigationMenu({ run }: { run: RunSummary }) {
|
|
|
102
145
|
</button>
|
|
103
146
|
</Tooltip>
|
|
104
147
|
{open && (
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
|
|
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 && (
|
|
108
169
|
<button
|
|
109
|
-
onClick={
|
|
110
|
-
|
|
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"
|
|
111
175
|
>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
) : (
|
|
115
|
-
<Copy className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
116
|
-
)}
|
|
117
|
-
<span>
|
|
118
|
-
{copied ? "Copied ✓" : `Copy command to continue in ${label}`}
|
|
119
|
-
{!copied && (
|
|
120
|
-
<span className="block text-[11px] text-theme-text-tertiary">
|
|
121
|
-
Paste it wherever you run {label} — resumes this exact
|
|
122
|
-
session.
|
|
123
|
-
</span>
|
|
124
|
-
)}
|
|
125
|
-
</span>
|
|
176
|
+
<TerminalSquare className="h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
177
|
+
Run in a Radar terminal
|
|
126
178
|
</button>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
onClick={() => {
|
|
130
|
-
openInTerminal(command, "Diagnose");
|
|
131
|
-
setOpen(false);
|
|
132
|
-
}}
|
|
133
|
-
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"
|
|
134
|
-
>
|
|
135
|
-
<TerminalSquare className="h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
136
|
-
Run in a Radar terminal
|
|
137
|
-
</button>
|
|
138
|
-
)}
|
|
139
|
-
</div>
|
|
140
|
-
</>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
141
181
|
)}
|
|
142
182
|
</div>
|
|
143
183
|
);
|
|
144
184
|
}
|
|
145
185
|
|
|
186
|
+
export function canCopyRunLink(
|
|
187
|
+
run: RunSummary | null | undefined,
|
|
188
|
+
): run is RunSummary & { radarUrl: string } {
|
|
189
|
+
return typeof run?.radarUrl === "string" && run.radarUrl.length > 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
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";
|
|
203
|
+
const [copyState, setCopyState] = useState<"idle" | "copied" | "error">(
|
|
204
|
+
"idle",
|
|
205
|
+
);
|
|
206
|
+
const copy = async () => {
|
|
207
|
+
try {
|
|
208
|
+
if (!navigator.clipboard) throw new Error("clipboard unavailable");
|
|
209
|
+
await navigator.clipboard.writeText(
|
|
210
|
+
new URL(radarUrl, window.location.origin).href,
|
|
211
|
+
);
|
|
212
|
+
setCopyState("copied");
|
|
213
|
+
} catch {
|
|
214
|
+
setCopyState("error");
|
|
215
|
+
}
|
|
216
|
+
setTimeout(() => setCopyState("idle"), 1500);
|
|
217
|
+
};
|
|
218
|
+
return (
|
|
219
|
+
<Tooltip
|
|
220
|
+
content={
|
|
221
|
+
copyState === "copied"
|
|
222
|
+
? "Link copied"
|
|
223
|
+
: copyState === "error"
|
|
224
|
+
? "Couldn’t copy link"
|
|
225
|
+
: label
|
|
226
|
+
}
|
|
227
|
+
position="bottom"
|
|
228
|
+
>
|
|
229
|
+
<button
|
|
230
|
+
onClick={copy}
|
|
231
|
+
className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
232
|
+
aria-label={label}
|
|
233
|
+
>
|
|
234
|
+
{copyState === "copied" ? (
|
|
235
|
+
<Check className="h-4 w-4 text-emerald-500" />
|
|
236
|
+
) : (
|
|
237
|
+
<Link className="h-4 w-4" />
|
|
238
|
+
)}
|
|
239
|
+
</button>
|
|
240
|
+
</Tooltip>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function VisibilityControl({
|
|
245
|
+
run,
|
|
246
|
+
onChanged,
|
|
247
|
+
}: {
|
|
248
|
+
run: RunSummary;
|
|
249
|
+
onChanged: (run: RunSummary) => void;
|
|
250
|
+
}) {
|
|
251
|
+
const [busy, setBusy] = useState(false);
|
|
252
|
+
const [error, setError] = useState(false);
|
|
253
|
+
const [confirmShare, setConfirmShare] = useState(false);
|
|
254
|
+
if (!run.canManageVisibility) {
|
|
255
|
+
return run.visibility === "organization" ? (
|
|
256
|
+
<Tooltip content="Shared with your organization" position="bottom">
|
|
257
|
+
<Badge severity="neutral" size="sm" className="shrink-0">
|
|
258
|
+
<Users className="h-3 w-3" />
|
|
259
|
+
Organization
|
|
260
|
+
</Badge>
|
|
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>
|
|
272
|
+
) : null;
|
|
273
|
+
}
|
|
274
|
+
const shared = run.visibility === "organization";
|
|
275
|
+
const update = () => {
|
|
276
|
+
if (busy) return;
|
|
277
|
+
setBusy(true);
|
|
278
|
+
setError(false);
|
|
279
|
+
updateRunVisibility(run.id, shared ? "private" : "organization")
|
|
280
|
+
.then((updated) => {
|
|
281
|
+
onChanged(updated);
|
|
282
|
+
setConfirmShare(false);
|
|
283
|
+
})
|
|
284
|
+
.catch(() => setError(true))
|
|
285
|
+
.finally(() => setBusy(false));
|
|
286
|
+
};
|
|
287
|
+
const label = shared ? "Organization" : "Private";
|
|
288
|
+
return (
|
|
289
|
+
<>
|
|
290
|
+
<Tooltip
|
|
291
|
+
content={
|
|
292
|
+
error
|
|
293
|
+
? "Couldn't change sharing"
|
|
294
|
+
: shared
|
|
295
|
+
? "Shared with your organization — make private"
|
|
296
|
+
: "Private — click to let organization members access this investigation"
|
|
297
|
+
}
|
|
298
|
+
position="bottom"
|
|
299
|
+
>
|
|
300
|
+
<button
|
|
301
|
+
onClick={() => (shared ? update() : setConfirmShare(true))}
|
|
302
|
+
disabled={busy}
|
|
303
|
+
className="flex items-center gap-1 rounded-md border border-theme-border/70 px-1.5 py-1 text-[11px] font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary disabled:opacity-50"
|
|
304
|
+
aria-label={
|
|
305
|
+
shared
|
|
306
|
+
? "Shared with your organization — make private"
|
|
307
|
+
: "Private — click to let organization members access this investigation"
|
|
308
|
+
}
|
|
309
|
+
>
|
|
310
|
+
{shared ? (
|
|
311
|
+
<Users className="h-3.5 w-3.5" />
|
|
312
|
+
) : (
|
|
313
|
+
<Lock className="h-3.5 w-3.5" />
|
|
314
|
+
)}
|
|
315
|
+
{label}
|
|
316
|
+
</button>
|
|
317
|
+
</Tooltip>
|
|
318
|
+
<ConfirmDialog
|
|
319
|
+
open={confirmShare}
|
|
320
|
+
onClose={() => !busy && setConfirmShare(false)}
|
|
321
|
+
onConfirm={update}
|
|
322
|
+
title="Share this investigation?"
|
|
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."
|
|
324
|
+
confirmLabel="Share with organization"
|
|
325
|
+
showWarning={false}
|
|
326
|
+
variant="warning"
|
|
327
|
+
isLoading={busy}
|
|
328
|
+
/>
|
|
329
|
+
</>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
146
333
|
// The panel is an ABSOLUTE slot inside the app's body frame (the column under the
|
|
147
334
|
// header, right of the nav rail) — App renders it there and passes topInset (the
|
|
148
335
|
// header height; 0 in chromeless embeds). It shares that frame with the resource/
|
|
@@ -154,11 +341,11 @@ function InvestigationMenu({ run }: { run: RunSummary }) {
|
|
|
154
341
|
// the last focused run. Without this the button dispatches an
|
|
155
342
|
// agent — real tokens — from a screen showing an unrelated list.
|
|
156
343
|
// run nothing to take a resource from.
|
|
157
|
-
// running
|
|
158
|
-
//
|
|
159
|
-
// stale the
|
|
160
|
-
//
|
|
161
|
-
//
|
|
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.
|
|
162
349
|
// needsConsent the consent card owns the surface until it's answered.
|
|
163
350
|
export function canStartNewInvestigation(
|
|
164
351
|
view: DiagnoseView,
|
|
@@ -168,14 +355,165 @@ export function canStartNewInvestigation(
|
|
|
168
355
|
return (
|
|
169
356
|
view === "investigation" &&
|
|
170
357
|
!!run &&
|
|
171
|
-
run.status !== "running" &&
|
|
358
|
+
((run.status !== "running" && run.status !== "stopping") ||
|
|
359
|
+
run.trigger === "background") &&
|
|
172
360
|
run.status !== "stale" &&
|
|
173
361
|
!needsConsent
|
|
174
362
|
);
|
|
175
363
|
}
|
|
176
364
|
|
|
177
|
-
|
|
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
|
+
}) {
|
|
178
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
|
+
}, []);
|
|
179
517
|
// Injected settings action: undefined = Radar's own Settings dialog;
|
|
180
518
|
// null = hide the gear + links.
|
|
181
519
|
const { consentCopy, onOpenSettings: hostOpenSettings } =
|
|
@@ -191,6 +529,41 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
191
529
|
panelBounds: { min: minW, max: maxW },
|
|
192
530
|
panelWidthKey: widthKey,
|
|
193
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
|
+
);
|
|
194
567
|
|
|
195
568
|
const startResize = (e: React.MouseEvent) => {
|
|
196
569
|
e.preventDefault();
|
|
@@ -220,24 +593,42 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
220
593
|
d.setupState === "needs-install" || d.setupState === "needs-restart";
|
|
221
594
|
|
|
222
595
|
const activeRun = d.runs.find((r) => r.id === d.activeRunId) ?? null;
|
|
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;
|
|
223
599
|
// A focused run shows the agent it actually ran with; Home reflects the current pick.
|
|
224
|
-
const activeAgentLabel =
|
|
225
|
-
? agentLabelFor(
|
|
600
|
+
const activeAgentLabel = visibleRunDetail?.agent
|
|
601
|
+
? agentLabelFor(visibleRunDetail.agent)
|
|
226
602
|
: d.agentLabel;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
},
|
|
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,
|
|
237
612
|
);
|
|
238
|
-
const
|
|
239
|
-
?
|
|
613
|
+
const focusedTitle = visibleRunDetail
|
|
614
|
+
? formatInvestigationTarget(visibleRunDetail)
|
|
240
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;
|
|
241
632
|
|
|
242
633
|
// Absolute within the body frame: maximized fills it; docked is a right slot.
|
|
243
634
|
// topInset clears the header (the frame spans the full column incl. the header).
|
|
@@ -268,6 +659,7 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
268
659
|
run={activeRun}
|
|
269
660
|
agentLabel={activeAgentLabel}
|
|
270
661
|
maximized={maximized}
|
|
662
|
+
onOpenResource={onOpenResource ? openEvidenceResource : undefined}
|
|
271
663
|
/>
|
|
272
664
|
) : d.activeRunId && !d.runsLoaded ? (
|
|
273
665
|
// Deep-linked to a run before the list has ever loaded: show the load
|
|
@@ -280,12 +672,13 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
280
672
|
</div>
|
|
281
673
|
) : d.activeRunId && d.runsLoaded ? (
|
|
282
674
|
// A focused id that isn't in the loaded list — a deep link (?ai-run=…) to a
|
|
283
|
-
// cleared/
|
|
675
|
+
// private/revoked/cleared/unknown run. The generic "select an
|
|
284
676
|
// investigation" placeholder would read as a broken link.
|
|
285
677
|
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
|
|
286
678
|
<p className="text-sm text-theme-text-secondary">
|
|
287
|
-
This investigation is
|
|
288
|
-
|
|
679
|
+
This investigation is unavailable. It may be private, your access may
|
|
680
|
+
have changed, or its history may have been cleared. Check your account
|
|
681
|
+
and organization, or ask the creator for access.
|
|
289
682
|
</p>
|
|
290
683
|
<button
|
|
291
684
|
onClick={d.goHome}
|
|
@@ -310,20 +703,51 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
310
703
|
</div>
|
|
311
704
|
) : (
|
|
312
705
|
<div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-theme-text-tertiary">
|
|
313
|
-
Select an investigation, or open a resource and click
|
|
706
|
+
Select an investigation, or open a resource and click Investigate.
|
|
314
707
|
</div>
|
|
315
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
|
+
);
|
|
316
723
|
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
724
|
+
const showHistory = !setupPending || d.runs.length > 0;
|
|
725
|
+
const historyVisible =
|
|
726
|
+
showHistory &&
|
|
727
|
+
(wideHistory ? !historyCollapsed || d.view === "home" : historyOverlay);
|
|
728
|
+
|
|
729
|
+
useLayoutEffect(() => {
|
|
730
|
+
if (
|
|
731
|
+
!historyVisible &&
|
|
732
|
+
historyRef.current?.contains(document.activeElement)
|
|
733
|
+
) {
|
|
734
|
+
historyButtonRef.current?.focus({ preventScroll: true });
|
|
735
|
+
}
|
|
736
|
+
}, [historyVisible]);
|
|
321
737
|
|
|
322
738
|
return (
|
|
323
739
|
<div
|
|
740
|
+
ref={surfaceRef}
|
|
324
741
|
role="dialog"
|
|
325
742
|
aria-label="AI investigations"
|
|
326
|
-
|
|
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}
|
|
327
751
|
style={{
|
|
328
752
|
...positionStyle,
|
|
329
753
|
animation: "slide-in-from-right 0.22s cubic-bezier(0.32,0.72,0,1)",
|
|
@@ -341,61 +765,103 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
341
765
|
|
|
342
766
|
{/* Header */}
|
|
343
767
|
<div className="flex items-center justify-between border-b border-theme-border px-4 py-2.5">
|
|
344
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
345
|
-
{
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
>
|
|
350
779
|
<button
|
|
351
|
-
|
|
352
|
-
|
|
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"
|
|
353
793
|
>
|
|
354
|
-
<
|
|
355
|
-
Investigations
|
|
794
|
+
<PanelLeftOpen className="h-4 w-4" />
|
|
356
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
|
+
/>
|
|
357
819
|
)}
|
|
358
|
-
<div className="truncate text-sm font-medium text-theme-text-primary">
|
|
359
|
-
{detailTitle}
|
|
360
|
-
</div>
|
|
361
|
-
<div className="flex items-center gap-1 text-xs text-theme-text-tertiary">
|
|
362
|
-
<span className="truncate">{configLine}</span>
|
|
363
|
-
{openSettings && (
|
|
364
|
-
<Tooltip content="AI settings" position="bottom">
|
|
365
|
-
<button
|
|
366
|
-
onClick={openSettings}
|
|
367
|
-
className="shrink-0 rounded p-0.5 text-theme-text-tertiary hover:text-theme-text-primary"
|
|
368
|
-
aria-label="AI settings"
|
|
369
|
-
>
|
|
370
|
-
<Settings2 className="h-3 w-3" />
|
|
371
|
-
</button>
|
|
372
|
-
</Tooltip>
|
|
373
|
-
)}
|
|
374
|
-
</div>
|
|
375
820
|
</div>
|
|
376
821
|
</div>
|
|
377
822
|
<div className="flex shrink-0 items-center gap-0.5">
|
|
378
823
|
{activeRun &&
|
|
379
824
|
canStartNewInvestigation(d.view, activeRun, d.needsConsent) && (
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
d.openInvestigation({
|
|
384
|
-
kind: activeRun.kind,
|
|
385
|
-
namespace: activeRun.namespace,
|
|
386
|
-
name: activeRun.name,
|
|
387
|
-
issueId: activeRun.issueId,
|
|
388
|
-
fresh: true,
|
|
389
|
-
})
|
|
390
|
-
}
|
|
391
|
-
className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
392
|
-
aria-label="New investigation on this resource"
|
|
825
|
+
<Tooltip
|
|
826
|
+
content="Start fresh — ignore earlier findings"
|
|
827
|
+
position="bottom"
|
|
393
828
|
>
|
|
394
|
-
<
|
|
395
|
-
|
|
396
|
-
|
|
829
|
+
<button
|
|
830
|
+
onClick={() =>
|
|
831
|
+
d.openInvestigation({
|
|
832
|
+
kind: activeRun.kind,
|
|
833
|
+
group: activeRun.group,
|
|
834
|
+
namespace: activeRun.namespace,
|
|
835
|
+
name: activeRun.name,
|
|
836
|
+
issueId: activeRun.issueId,
|
|
837
|
+
fresh: true,
|
|
838
|
+
})
|
|
839
|
+
}
|
|
840
|
+
className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
841
|
+
aria-label="Start fresh — ignore earlier findings"
|
|
842
|
+
>
|
|
843
|
+
<Plus className="h-4 w-4" />
|
|
844
|
+
</button>
|
|
845
|
+
</Tooltip>
|
|
846
|
+
)}
|
|
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>
|
|
397
864
|
)}
|
|
398
|
-
{activeRun && <InvestigationMenu run={activeRun} />}
|
|
399
865
|
<Tooltip content={maximized ? "Restore" : "Expand"} position="bottom">
|
|
400
866
|
<button
|
|
401
867
|
onClick={() => setMaximized((v) => !v)}
|
|
@@ -426,38 +892,61 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
426
892
|
(which would discard its transcript and re-run the agent). The aside
|
|
427
893
|
only appears when expanded; keys keep the detail node identity-stable
|
|
428
894
|
as it comes and goes. */}
|
|
429
|
-
<div className="flex min-h-0 flex-1">
|
|
430
|
-
{
|
|
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 && (
|
|
431
904
|
<aside
|
|
432
905
|
key="recent"
|
|
433
|
-
|
|
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`}
|
|
434
913
|
>
|
|
435
914
|
<RecentList
|
|
915
|
+
currentContext={currentContext}
|
|
436
916
|
agentLabel={d.agentLabel}
|
|
437
917
|
runs={d.runs}
|
|
438
918
|
selectedId={d.activeRunId}
|
|
439
|
-
onSelect={
|
|
919
|
+
onSelect={(id) => {
|
|
920
|
+
d.openRun(id);
|
|
921
|
+
if (historyOverlay) dismissHistory();
|
|
922
|
+
}}
|
|
440
923
|
historyDegraded={d.historyDegraded}
|
|
441
924
|
/>
|
|
442
925
|
</aside>
|
|
443
926
|
)}
|
|
444
|
-
{
|
|
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
|
+
) : (
|
|
445
945
|
<div
|
|
446
946
|
key="main"
|
|
447
|
-
|
|
947
|
+
inert={historyOverlay}
|
|
948
|
+
className="flex min-h-0 min-w-0 flex-1 flex-col"
|
|
448
949
|
>
|
|
449
|
-
{setupPending && <AgentSetupNotice setupState={d.setupState} />}
|
|
450
|
-
{(!setupPending || d.runs.length > 0) && (
|
|
451
|
-
<RecentList
|
|
452
|
-
agentLabel={d.agentLabel}
|
|
453
|
-
runs={d.runs}
|
|
454
|
-
onSelect={d.openRun}
|
|
455
|
-
historyDegraded={d.historyDegraded}
|
|
456
|
-
/>
|
|
457
|
-
)}
|
|
458
|
-
</div>
|
|
459
|
-
) : (
|
|
460
|
-
<div key="main" className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
461
950
|
{detail}
|
|
462
951
|
</div>
|
|
463
952
|
)}
|