@skyhook-io/radar-app 1.13.1 → 1.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/package.json +2 -2
  2. package/src/App.tsx +19 -1
  3. package/src/RadarApp.tsx +2 -2
  4. package/src/api/diagnose.test.ts +268 -0
  5. package/src/api/diagnose.ts +72 -9
  6. package/src/components/diagnose/AISettings.tsx +1 -1
  7. package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
  8. package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
  9. package/src/components/diagnose/DiagnoseContext.tsx +12 -17
  10. package/src/components/diagnose/DiagnoseSurface.test.tsx +211 -16
  11. package/src/components/diagnose/DiagnoseSurface.tsx +464 -133
  12. package/src/components/diagnose/Home.test.tsx +293 -0
  13. package/src/components/diagnose/Home.tsx +289 -119
  14. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +2170 -0
  15. package/src/components/diagnose/InvestigationEvidencePane.tsx +2253 -0
  16. package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +257 -0
  17. package/src/components/diagnose/InvestigationResourceEvidence.tsx +214 -0
  18. package/src/components/diagnose/InvestigationView.test.ts +17 -0
  19. package/src/components/diagnose/InvestigationView.tsx +1900 -393
  20. package/src/components/diagnose/LocalDiagnoseAction.tsx +42 -25
  21. package/src/components/diagnose/agentCatalog.ts +1 -1
  22. package/src/components/diagnose/diagnoseEvidenceTypes.ts +151 -0
  23. package/src/components/diagnose/investigationEvidence.test.ts +3109 -0
  24. package/src/components/diagnose/investigationEvidence.ts +3492 -0
  25. package/src/components/diagnose/investigationEvidencePresentation.test.ts +447 -0
  26. package/src/components/diagnose/investigationEvidencePresentation.ts +167 -0
  27. package/src/components/diagnose/investigationExplanation.test.ts +63 -0
  28. package/src/components/diagnose/investigationExplanation.ts +22 -0
  29. package/src/components/diagnose/investigationResourceEvidenceModel.ts +322 -0
  30. package/src/components/diagnose/investigationSourceFocus.test.ts +143 -0
  31. package/src/components/diagnose/investigationSourceFocus.ts +98 -0
  32. package/src/components/diagnose/investigationState.test.ts +695 -0
  33. package/src/components/diagnose/investigationState.ts +451 -0
  34. package/src/components/diagnose/parts.test.tsx +864 -3
  35. package/src/components/diagnose/parts.tsx +1337 -541
  36. package/src/components/diagnose/target.test.ts +39 -0
  37. package/src/components/diagnose/target.ts +36 -0
  38. package/src/components/diagnose/useDisclosureReveal.ts +117 -0
  39. package/src/components/home/MCPSetupDialog.tsx +2 -2
  40. package/src/components/home/mcpToolCatalog.test.ts +22 -0
  41. package/src/components/home/mcpToolCatalog.ts +3 -2
  42. package/src/components/issues/IssuesPane.tsx +5 -1
  43. package/src/components/settings/SettingsDialog.tsx +11 -13
  44. package/src/components/workload/WorkloadView.tsx +1 -1
  45. package/src/context/DiagnoseCustomization.tsx +11 -8
  46. package/src/index.css +63 -79
  47. package/src/index.ts +1 -1
@@ -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 { useState } from "react";
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
- <div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
119
- <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">
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={copy}
122
- 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"
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
- {copied ? (
125
- <Check className="mt-0.5 h-4 w-4 shrink-0 text-emerald-500" />
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
- {localTerminal && (
140
- <button
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({ radarUrl, visibility }: { radarUrl: string; visibility: RunSummary["visibility"] }) {
165
- const label = visibility === "private"
166
- ? "Copy private link (only you can open it)"
167
- : "Copy investigation link";
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 a human start is handed back the live run, so the click does
299
- // nothing. An automatic run is a different, immutable session,
300
- // so it deliberately keeps the fresh human escape hatch.
301
- // stale the body already offers "Re-run on current cluster" WITH the
302
- // warning that the context changed; a bare + carries none of it,
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
- export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
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
- // Docked Home is a list, not the previously focused run. Scope every header
367
- // label/action to what is visibly on screen so Copy/Share cannot act on a run
368
- // the user has navigated away from. Expanded mode remains master-detail and
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 = headerRun?.agent
373
- ? agentLabelFor(headerRun.agent)
600
+ const activeAgentLabel = visibleRunDetail?.agent
601
+ ? agentLabelFor(visibleRunDetail.agent)
374
602
  : d.agentLabel;
375
- // Header subtitle: the config a focused run actually used (it records agent /
376
- // profile / model / effort), or the current defaults on Home. Codex shows mode
377
- // + reasoning effort; model is shown only when overridden. Clicking opens Settings.
378
- const configLine = buildConfigLine(
379
- headerRun ?? {
380
- agent: d.selectedAgent,
381
- profile: d.hosted ? undefined : d.profile,
382
- model: d.model,
383
- effort: d.effort,
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 detailTitle = headerRun
387
- ? `${headerRun.kind} ${headerRun.namespace ? `${headerRun.namespace}/` : ""}${headerRun.name}`
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 Diagnose.
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
- const showBreadcrumb = !maximized && d.view !== "home";
467
- // The maximized workspace always shows the recent-investigations list (there's
468
- // room for it in full-wide) — it's the master pane of the master-detail layout.
469
- const showHistory = maximized;
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
- className="absolute z-40 flex flex-col border-l border-theme-border bg-theme-surface shadow-drawer"
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
- {!showBreadcrumb && (
495
- <Sparkles className="h-4 w-4 shrink-0 text-accent" />
496
- )}
497
- <div className="min-w-0">
498
- {showBreadcrumb && (
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
- onClick={d.goHome}
501
- className="-ml-1 mb-0.5 flex items-center gap-0.5 rounded px-1 text-[11px] text-theme-text-tertiary hover:text-theme-text-primary"
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
- <ChevronLeft className="h-3 w-3" />
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
- {headerRun &&
528
- canStartNewInvestigation(d.view, headerRun, d.needsConsent) && (
823
+ {activeRun &&
824
+ canStartNewInvestigation(d.view, activeRun, d.needsConsent) && (
529
825
  <Tooltip
530
- content="New investigation on this resource"
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: headerRun.kind,
537
- namespace: headerRun.namespace,
538
- name: headerRun.name,
539
- issueId: headerRun.issueId,
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="New investigation on this resource"
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
- {headerRun && (
551
- <VisibilityControl run={headerRun} onChanged={d.updateRunSummary} />
552
- )}
553
- {canCopyRunLink(headerRun) && (
554
- <CopyRunLink radarUrl={headerRun.radarUrl} visibility={headerRun.visibility} />
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
- {showHistory && (!setupPending || d.runs.length > 0) && (
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
- className="w-72 shrink-0 overflow-y-auto border-r border-theme-border px-3 py-3"
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={d.openRun}
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
- {!maximized && d.view === "home" ? (
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
- className="flex-1 overflow-y-auto overflow-x-hidden px-4 py-3"
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
  )}