@skyhook-io/radar-app 1.13.3 → 1.13.5

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 (45) hide show
  1. package/package.json +6 -6
  2. package/src/App.tsx +68 -23
  3. package/src/RadarApp.tsx +17 -1
  4. package/src/api/client.ts +28 -10
  5. package/src/api/diagnose.ts +61 -15
  6. package/src/components/ConnectionErrorView.test.tsx +30 -0
  7. package/src/components/ConnectionErrorView.tsx +31 -19
  8. package/src/components/diagnose/AgentCase.tsx +131 -0
  9. package/src/components/diagnose/DiagnoseContext.test.ts +95 -0
  10. package/src/components/diagnose/DiagnoseContext.tsx +402 -77
  11. package/src/components/diagnose/DiagnoseSurface.test.tsx +53 -54
  12. package/src/components/diagnose/DiagnoseSurface.tsx +198 -286
  13. package/src/components/diagnose/Home.test.tsx +23 -1
  14. package/src/components/diagnose/Home.tsx +49 -3
  15. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +1446 -5
  16. package/src/components/diagnose/InvestigationEvidencePane.tsx +1373 -126
  17. package/src/components/diagnose/InvestigationView.tsx +227 -5
  18. package/src/components/diagnose/LocalDiagnoseAction.tsx +2 -3
  19. package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
  20. package/src/components/diagnose/investigationCase.test.tsx +1568 -0
  21. package/src/components/diagnose/investigationCase.ts +439 -0
  22. package/src/components/diagnose/investigationEvidence.test.ts +3525 -17
  23. package/src/components/diagnose/investigationEvidence.ts +2246 -166
  24. package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
  25. package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
  26. package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
  27. package/src/components/diagnose/investigationMetrics.test.ts +712 -0
  28. package/src/components/diagnose/investigationMetrics.ts +393 -0
  29. package/src/components/diagnose/investigationSourceFocus.ts +6 -0
  30. package/src/components/diagnose/investigationState.test.ts +322 -0
  31. package/src/components/diagnose/investigationState.ts +147 -25
  32. package/src/components/diagnose/parts.test.tsx +537 -1
  33. package/src/components/diagnose/parts.tsx +486 -42
  34. package/src/components/resource/HPACharts.render.test.tsx +77 -0
  35. package/src/components/resource/HPACharts.tsx +32 -25
  36. package/src/components/resource/PrometheusChartsGrid.tsx +181 -79
  37. package/src/components/resources/PodFilePreview.test.tsx +131 -0
  38. package/src/components/resources/PodFilePreview.tsx +394 -0
  39. package/src/components/resources/PodFilesystemModal.tsx +157 -67
  40. package/src/components/resources/ResourcesView.tsx +27 -2
  41. package/src/context/DiagnoseCustomization.test.tsx +26 -0
  42. package/src/context/DiagnoseCustomization.tsx +14 -2
  43. package/src/index.ts +8 -5
  44. package/src/utils/shell-safe.test.ts +25 -1
  45. package/src/utils/shell-safe.ts +16 -0
@@ -20,15 +20,10 @@ import {
20
20
  TerminalSquare,
21
21
  Copy,
22
22
  Check,
23
- Plus,
23
+ RotateCcw,
24
24
  PanelLeftOpen,
25
- Link,
26
- Lock,
27
- Users,
28
25
  } from "lucide-react";
29
26
  import { Tooltip } from "../ui/Tooltip";
30
- import { ConfirmDialog } from "../ui/ConfirmDialog";
31
- import { Badge } from "@skyhook-io/k8s-ui/components/ui/Badge";
32
27
  import { useAnimatedUnmount } from "../../hooks/useAnimatedUnmount";
33
28
  import { TRANSITION_BACKDROP, TRANSITION_DRAWER } from "../../utils/animation";
34
29
  import {
@@ -39,13 +34,19 @@ import {
39
34
  type DiagnoseView,
40
35
  } from "./DiagnoseContext";
41
36
  import { useDiagnoseCustomization } from "../../context/DiagnoseCustomization";
37
+ import { useNavCustomization } from "../../context/NavCustomization";
42
38
  import { InvestigationView } from "./InvestigationView";
43
- import { RecentList, absoluteTime, statusWord } from "./Home";
39
+ import type { InvestigationTimelineScope } from "./InvestigationEvidencePane";
40
+ import {
41
+ InvestigationHome,
42
+ RecentList,
43
+ absoluteTime,
44
+ statusWord,
45
+ } from "./Home";
44
46
  import { AgentSetupNotice } from "./AgentSetupNotice";
45
47
  import { ConsentCard } from "./parts";
46
48
  import { buildLaunchCommand, launchAgentLabel, openInTerminal } from "./launch";
47
49
  import {
48
- updateRunVisibility,
49
50
  type RunSummary,
50
51
  type ExecutionProfile,
51
52
  } from "../../api/diagnose";
@@ -183,163 +184,14 @@ function InvestigationMenu({ run }: { run: RunSummary }) {
183
184
  );
184
185
  }
185
186
 
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
-
333
187
  // The panel is an ABSOLUTE slot inside the app's body frame (the column under the
334
188
  // header, right of the nav rail) — App renders it there and passes topInset (the
335
189
  // header height; 0 in chromeless embeds). It shares that frame with the resource/
336
190
  // Helm drawers, so it no longer floats viewport-fixed or DOM-measures the chrome.
337
- // Whether the header offers a new investigation on the focused run's resource.
191
+ // Whether the header offers a fresh re-run of the focused investigation.
338
192
  // Every clause is a failure this button actually had:
339
193
  //
340
- // view goHome() leaves activeRunId set, so the header keeps rendering
341
- // the last focused run. Without this the button dispatches an
342
- // agent — real tokens — from a screen showing an unrelated list.
194
+ // view Home is the fresh-entry surface, never a re-run action.
343
195
  // run nothing to take a resource from.
344
196
  // running/stopping human starts reuse the live run. Automatic investigations
345
197
  // are immutable, so they retain a separate fresh human start.
@@ -347,7 +199,7 @@ function VisibilityControl({
347
199
  // the resource may not exist in the active context. Do not
348
200
  // silently start a different-cluster investigation from here.
349
201
  // needsConsent the consent card owns the surface until it's answered.
350
- export function canStartNewInvestigation(
202
+ export function canRerunInvestigation(
351
203
  view: DiagnoseView,
352
204
  run: RunSummary | null,
353
205
  needsConsent: boolean,
@@ -355,6 +207,8 @@ export function canStartNewInvestigation(
355
207
  return (
356
208
  view === "investigation" &&
357
209
  !!run &&
210
+ !!run.kind &&
211
+ !!run.name &&
358
212
  ((run.status !== "running" && run.status !== "stopping") ||
359
213
  run.trigger === "background") &&
360
214
  run.status !== "stale" &&
@@ -366,12 +220,6 @@ export function canStartNewInvestigation(
366
220
  // Keep these Tailwind literals aligned with the measured rail threshold.
367
221
  // Detail uses one stable history toggle; below this width it opens an overlay.
368
222
  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
223
  export const MAXIMIZED_RUN_META_VISIBILITY_CLASS =
376
224
  "hidden @min-[1750px]/diagnose-surface:flex";
377
225
  // The panel is a bounded absolute frame whose descendants own scrolling. If
@@ -382,6 +230,24 @@ export const MAXIMIZED_RUN_META_VISIBILITY_CLASS =
382
230
  export const DIAGNOSE_SURFACE_FRAME_CLASS =
383
231
  "@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
232
 
233
+ export function unavailableInvestigationMessage(embedded: boolean): string {
234
+ return embedded
235
+ ? "This investigation is unavailable. It may be private, your access may have changed, or its history may have been cleared. Check your account and organization, or ask the creator for access."
236
+ : "This investigation is unavailable. It may have been removed, or its saved history may have been cleared.";
237
+ }
238
+
239
+ export function investigationHistoryIsPersistent(input: {
240
+ maximized: boolean;
241
+ view: DiagnoseView;
242
+ surfaceWidth: number;
243
+ }): boolean {
244
+ return (
245
+ input.maximized &&
246
+ (input.view === "home" ||
247
+ input.surfaceWidth >= INVESTIGATION_HISTORY_MIN_WIDTH)
248
+ );
249
+ }
250
+
385
251
  export function investigationHeaderPresentation(input: {
386
252
  view: DiagnoseView;
387
253
  maximized: boolean;
@@ -398,17 +264,10 @@ export function investigationHeaderPresentation(input: {
398
264
  runActionsClass: input.hasVisibleRunDetail ? "" : null,
399
265
  };
400
266
  }
401
- const hasWideRetainedDetail = input.maximized && input.hasVisibleRunDetail;
402
267
  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,
268
+ genericIdentityClass: "",
269
+ detailIdentityClass: null,
270
+ runActionsClass: null,
412
271
  };
413
272
  }
414
273
 
@@ -431,69 +290,93 @@ function DiagnoseHeaderIdentity({
431
290
  onOpenSettings: (() => void) | null;
432
291
  }) {
433
292
  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">
293
+ <div className={`flex min-w-0 items-center gap-3 ${className}`}>
294
+ <div className="min-w-0 flex-1">
295
+ <div className="min-w-0 truncate text-sm font-medium text-theme-text-primary">
437
296
  {title}
438
297
  </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
- )}
298
+ <div className="flex items-center gap-1 text-xs text-theme-text-tertiary">
299
+ <span className="truncate">{configLine}</span>
300
+ {onOpenSettings && (
301
+ <Tooltip content="AI settings" position="bottom">
302
+ <button
303
+ onClick={onOpenSettings}
304
+ className="shrink-0 rounded p-0.5 text-theme-text-tertiary hover:text-theme-text-primary"
305
+ aria-label="AI settings"
306
+ >
307
+ <Settings2 className="h-3 w-3" />
308
+ </button>
309
+ </Tooltip>
310
+ )}
311
+ </div>
464
312
  </div>
313
+ {runMeta ? (
314
+ <div
315
+ className={`${MAXIMIZED_RUN_META_VISIBILITY_CLASS} shrink-0 items-center gap-1 whitespace-nowrap text-[11px] tabular-nums text-theme-text-tertiary`}
316
+ >
317
+ <span className={`font-medium ${runMeta.labelClass}`}>
318
+ {runMeta.label}
319
+ </span>
320
+ <span aria-hidden>·</span>
321
+ <time dateTime={runMeta.dateTime}>{runMeta.time}</time>
322
+ </div>
323
+ ) : null}
465
324
  </div>
466
325
  );
467
326
  }
468
327
 
469
328
  export function openInvestigationEvidenceResource(
470
329
  ref: DiagnosisResourceRef,
471
- onOpenResource: (ref: DiagnosisResourceRef) => void,
330
+ onOpenResource: (ref: DiagnosisResourceRef, runID?: string | null) => void,
472
331
  setMaximized: (maximized: boolean) => void,
473
- closeDiagnose: () => void,
332
+ dismissDiagnose: () => void,
333
+ dockedPanelWouldOverlay: boolean,
334
+ activeRunID?: string,
335
+ ) {
336
+ revealInvestigationDestination(
337
+ setMaximized,
338
+ dismissDiagnose,
339
+ dockedPanelWouldOverlay,
340
+ );
341
+ if (dockedPanelWouldOverlay) onOpenResource(ref, null);
342
+ else if (activeRunID) onOpenResource(ref, activeRunID);
343
+ else onOpenResource(ref);
344
+ }
345
+
346
+ // A destination must be visible after the handoff. On a wide canvas, restoring
347
+ // the docked panel leaves Radar and the investigation side by side. At tighter
348
+ // widths that same panel overlays the host content, so close it before
349
+ // navigating instead of making the click appear to do nothing.
350
+ function revealInvestigationDestination(
351
+ setMaximized: (maximized: boolean) => void,
352
+ dismissDiagnose: () => void,
474
353
  dockedPanelWouldOverlay: boolean,
475
354
  ) {
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
355
  if (dockedPanelWouldOverlay) {
481
356
  // Closing already exposes the destination; retain the user's maximized
482
357
  // preference for the next time they open investigations.
483
- closeDiagnose();
358
+ dismissDiagnose();
484
359
  } else {
485
360
  setMaximized(false);
486
361
  }
487
- onOpenResource(ref);
488
362
  }
489
363
 
490
364
  export function DiagnoseSurface({
491
365
  topInset = 0,
492
366
  onOpenResource,
367
+ onOpenTimeline,
368
+ onOpenWorkspace,
369
+ onBrowseIssues,
493
370
  }: {
494
371
  topInset?: number;
495
372
  /** Resolves an evidence subject into the embedding Radar surface. */
496
- onOpenResource?: (ref: DiagnosisResourceRef) => void;
373
+ onOpenResource?: (ref: DiagnosisResourceRef, runID?: string | null) => void;
374
+ /** Opens the embedding Radar Timeline for one resource's change history. */
375
+ onOpenTimeline?: (scope: InvestigationTimelineScope) => void;
376
+ /** Lets a host whose drawer lives outside Radar's route tree cross into it. */
377
+ onOpenWorkspace?: () => void;
378
+ /** Opens the canonical Issues surface for a first focused investigation. */
379
+ onBrowseIssues?: () => void;
497
380
  }) {
498
381
  const d = useDiagnose();
499
382
  const { data: contexts } = useContexts();
@@ -516,8 +399,9 @@ export function DiagnoseSurface({
516
399
  }, []);
517
400
  // Injected settings action: undefined = Radar's own Settings dialog;
518
401
  // null = hide the gear + links.
519
- const { consentCopy, onOpenSettings: hostOpenSettings } =
402
+ const { consentCopy, onOpenSettings: hostOpenSettings, renderRunActions } =
520
403
  useDiagnoseCustomization();
404
+ const { embedded } = useNavCustomization();
521
405
  const openSettings =
522
406
  hostOpenSettings === undefined ? openDiagnoseSettings : hostOpenSettings;
523
407
  const {
@@ -529,10 +413,16 @@ export function DiagnoseSurface({
529
413
  panelBounds: { min: minW, max: maxW },
530
414
  panelWidthKey: widthKey,
531
415
  } = useDiagnoseLayout();
532
- const wideHistory =
533
- maximized && surfaceWidth >= INVESTIGATION_HISTORY_MIN_WIDTH;
416
+ // Home has no detail pane competing for width, so its history is always part
417
+ // of the workspace. Only a focused run collapses history into a drawer when
418
+ // the two-pane layout would become cramped.
419
+ const persistentHistory = investigationHistoryIsPersistent({
420
+ maximized,
421
+ view: d.view,
422
+ surfaceWidth,
423
+ });
534
424
  const historyOverlay =
535
- !wideHistory && historyOverlayOpen && d.view !== "home";
425
+ !persistentHistory && historyOverlayOpen;
536
426
  const { shouldRender: historyOverlayPresent, isOpen: historySlideOpen } =
537
427
  useAnimatedUnmount(historyOverlay);
538
428
  const dismissHistory = useCallback(() => {
@@ -541,7 +431,7 @@ export function DiagnoseSurface({
541
431
  }, []);
542
432
  useEffect(() => {
543
433
  setHistoryOverlayOpen(false);
544
- }, [wideHistory, maximized, d.view]);
434
+ }, [persistentHistory, maximized, d.view]);
545
435
  useEffect(() => {
546
436
  if (historyOverlay) {
547
437
  const target =
@@ -558,11 +448,30 @@ export function DiagnoseSurface({
558
448
  ref,
559
449
  onOpenResource,
560
450
  setMaximized,
561
- d.close,
451
+ d.dismissForNavigation,
452
+ narrow,
453
+ d.activeRunId ?? undefined,
454
+ );
455
+ },
456
+ [
457
+ d.activeRunId,
458
+ d.dismissForNavigation,
459
+ narrow,
460
+ onOpenResource,
461
+ setMaximized,
462
+ ],
463
+ );
464
+ const openEvidenceTimeline = useCallback(
465
+ (scope: InvestigationTimelineScope) => {
466
+ if (!onOpenTimeline) return;
467
+ revealInvestigationDestination(
468
+ setMaximized,
469
+ d.dismissForNavigation,
562
470
  narrow,
563
471
  );
472
+ onOpenTimeline(scope);
564
473
  },
565
- [d.close, narrow, onOpenResource, setMaximized],
474
+ [d.dismissForNavigation, narrow, onOpenTimeline, setMaximized],
566
475
  );
567
476
 
568
477
  const startResize = (e: React.MouseEvent) => {
@@ -660,6 +569,7 @@ export function DiagnoseSurface({
660
569
  agentLabel={activeAgentLabel}
661
570
  maximized={maximized}
662
571
  onOpenResource={onOpenResource ? openEvidenceResource : undefined}
572
+ onOpenTimeline={onOpenTimeline ? openEvidenceTimeline : undefined}
663
573
  />
664
574
  ) : d.activeRunId && !d.runsLoaded ? (
665
575
  // Deep-linked to a run before the list has ever loaded: show the load
@@ -676,9 +586,7 @@ export function DiagnoseSurface({
676
586
  // investigation" placeholder would read as a broken link.
677
587
  <div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
678
588
  <p className="text-sm text-theme-text-secondary">
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.
589
+ {unavailableInvestigationMessage(embedded === true)}
682
590
  </p>
683
591
  <button
684
592
  onClick={d.goHome}
@@ -706,25 +614,38 @@ export function DiagnoseSurface({
706
614
  Select an investigation, or open a resource and click Investigate.
707
615
  </div>
708
616
  );
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
- </>
617
+ const home = d.needsConsent ? (
618
+ <div className="mx-auto w-full max-w-3xl px-4 py-8 sm:px-6">
619
+ <ConsentCard
620
+ agentName={d.agentLabel}
621
+ agent={d.selectedAgent}
622
+ profile={d.profile}
623
+ copy={consentCopy}
624
+ onOpenSettings={openSettings ?? undefined}
625
+ error={d.consentError}
626
+ onApprove={d.approveConsent}
627
+ onCancel={d.cancelConsent}
628
+ />
629
+ </div>
630
+ ) : setupPending ? (
631
+ <div className="mx-auto w-full max-w-3xl px-4 py-8 sm:px-6">
632
+ <AgentSetupNotice setupState={d.setupState} />
633
+ </div>
634
+ ) : (
635
+ <InvestigationHome
636
+ agentLabel={d.agentLabel}
637
+ onBrowseIssues={
638
+ d.runsLoaded && d.runs.length === 0 ? onBrowseIssues : undefined
639
+ }
640
+ />
722
641
  );
723
642
 
724
643
  const showHistory = !setupPending || d.runs.length > 0;
725
644
  const historyVisible =
726
645
  showHistory &&
727
- (wideHistory ? !historyCollapsed || d.view === "home" : historyOverlay);
646
+ (persistentHistory
647
+ ? d.view === "home" || !historyCollapsed
648
+ : historyOverlay);
728
649
 
729
650
  useLayoutEffect(() => {
730
651
  if (
@@ -764,9 +685,9 @@ export function DiagnoseSurface({
764
685
  )}
765
686
 
766
687
  {/* Header */}
767
- <div className="flex items-center justify-between border-b border-theme-border px-4 py-2.5">
688
+ <div className="relative z-30 flex items-center justify-between gap-3 border-b border-theme-border bg-theme-surface px-4 py-2.5">
768
689
  <div className="flex min-w-0 flex-1 items-center gap-2">
769
- {d.view !== "home" && showHistory ? (
690
+ {showHistory && (d.view !== "home" || !persistentHistory) ? (
770
691
  <Tooltip
771
692
  content={
772
693
  historyVisible
@@ -783,7 +704,7 @@ export function DiagnoseSurface({
783
704
  aria-expanded={historyVisible}
784
705
  aria-controls="investigation-history"
785
706
  onClick={() =>
786
- wideHistory
707
+ persistentHistory
787
708
  ? setHistoryCollapsed((value) => !value)
788
709
  : historyOverlay
789
710
  ? dismissHistory()
@@ -819,11 +740,11 @@ export function DiagnoseSurface({
819
740
  )}
820
741
  </div>
821
742
  </div>
822
- <div className="flex shrink-0 items-center gap-0.5">
743
+ <div className="flex shrink-0 items-center gap-1">
823
744
  {activeRun &&
824
- canStartNewInvestigation(d.view, activeRun, d.needsConsent) && (
745
+ canRerunInvestigation(d.view, activeRun, d.needsConsent) && (
825
746
  <Tooltip
826
- content="Start fresh — ignore earlier findings"
747
+ content="Re-run this investigation from scratch"
827
748
  position="bottom"
828
749
  >
829
750
  <button
@@ -838,9 +759,9 @@ export function DiagnoseSurface({
838
759
  })
839
760
  }
840
761
  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"
762
+ aria-label="Re-run this investigation from scratch"
842
763
  >
843
- <Plus className="h-4 w-4" />
764
+ <RotateCcw className="h-4 w-4" />
844
765
  </button>
845
766
  </Tooltip>
846
767
  )}
@@ -848,33 +769,33 @@ export function DiagnoseSurface({
848
769
  <div
849
770
  className={`items-center gap-1 ${headerPresentation.runActionsClass || "flex"}`}
850
771
  >
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
- )}
772
+ {renderRunActions?.({ run: visibleRunDetail, onRunUpdated: d.updateRunSummary })}
862
773
  <InvestigationMenu run={visibleRunDetail} />
863
774
  </div>
864
775
  )}
865
- <Tooltip content={maximized ? "Restore" : "Expand"} position="bottom">
866
- <button
867
- onClick={() => setMaximized((v) => !v)}
868
- className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
869
- aria-label={maximized ? "Restore" : "Expand"}
776
+ {(!maximized || d.canRestoreWorkspace) && (
777
+ <Tooltip
778
+ content={maximized ? "Restore" : "Expand"}
779
+ position="bottom"
870
780
  >
871
- {maximized ? (
872
- <Minimize2 className="h-4 w-4" />
873
- ) : (
874
- <Maximize2 className="h-4 w-4" />
875
- )}
876
- </button>
877
- </Tooltip>
781
+ <button
782
+ onClick={
783
+ maximized
784
+ ? d.restoreWorkspace
785
+ : (onOpenWorkspace ??
786
+ (() => d.openWorkspace(d.activeRunId)))
787
+ }
788
+ className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
789
+ aria-label={maximized ? "Restore" : "Expand"}
790
+ >
791
+ {maximized ? (
792
+ <Minimize2 className="h-4 w-4" />
793
+ ) : (
794
+ <Maximize2 className="h-4 w-4" />
795
+ )}
796
+ </button>
797
+ </Tooltip>
798
+ )}
878
799
  <Tooltip content="Close" position="bottom">
879
800
  <button
880
801
  onClick={d.close}
@@ -893,7 +814,7 @@ export function DiagnoseSurface({
893
814
  only appears when expanded; keys keep the detail node identity-stable
894
815
  as it comes and goes. */}
895
816
  <div className="relative flex min-h-0 flex-1">
896
- {!wideHistory && historyOverlayPresent && (
817
+ {!persistentHistory && historyOverlayPresent && (
897
818
  <div
898
819
  aria-hidden="true"
899
820
  onClick={dismissHistory}
@@ -909,7 +830,7 @@ export function DiagnoseSurface({
909
830
  aria-hidden={!historyVisible}
910
831
  inert={!historyVisible}
911
832
  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`}
833
+ className={`${historyVisible || (!persistentHistory && historyOverlayPresent) ? "block" : "hidden"} ${persistentHistory ? "" : `absolute inset-y-0 left-0 z-20 max-w-[calc(100%-2rem)] ${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`}
913
834
  >
914
835
  <RecentList
915
836
  currentContext={currentContext}
@@ -925,22 +846,13 @@ export function DiagnoseSurface({
925
846
  </aside>
926
847
  )}
927
848
  {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
- </>
849
+ <div
850
+ key="home"
851
+ inert={historyOverlay}
852
+ className="flex-1 overflow-y-auto overflow-x-hidden"
853
+ >
854
+ {home}
855
+ </div>
944
856
  ) : (
945
857
  <div
946
858
  key="main"