@skyhook-io/radar-app 1.13.2 → 1.13.4

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.
@@ -1,15 +1,14 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import {
3
3
  DIAGNOSE_SURFACE_FRAME_CLASS,
4
- MAXIMIZED_COMPACT_HISTORY_VISIBILITY_CLASS,
5
- MAXIMIZED_HOME_DETAIL_VISIBILITY_CLASS,
6
- MAXIMIZED_HOME_RUN_HEADER_VISIBILITY_CLASS,
7
4
  INVESTIGATION_HISTORY_MIN_WIDTH,
8
5
  MAXIMIZED_RUN_META_VISIBILITY_CLASS,
9
- canStartNewInvestigation,
6
+ canRerunInvestigation,
10
7
  canCopyRunLink,
8
+ investigationHistoryIsPersistent,
11
9
  investigationHeaderPresentation,
12
10
  openInvestigationEvidenceResource,
11
+ unavailableInvestigationMessage,
13
12
  } from "./DiagnoseSurface";
14
13
  import {
15
14
  canContinueInvestigation,
@@ -18,7 +17,7 @@ import {
18
17
  } from "./investigationState";
19
18
  import type { RunSummary } from "../../api/diagnose";
20
19
 
21
- // The "new investigation" button dispatches an agent and spends the user's own
20
+ // The re-run button dispatches an agent and spends the user's own
22
21
  // tokens, so every one of these clauses is load-bearing rather than cosmetic.
23
22
  // Each case below is a way it misfired before the gate existed.
24
23
  function run(status: RunSummary["status"]): RunSummary {
@@ -35,41 +34,40 @@ function run(status: RunSummary["status"]): RunSummary {
35
34
  };
36
35
  }
37
36
 
38
- describe("canStartNewInvestigation", () => {
37
+ describe("canRerunInvestigation", () => {
39
38
  it("offers a new investigation on a finished run", () => {
40
- expect(canStartNewInvestigation("investigation", run("done"), false)).toBe(
39
+ expect(canRerunInvestigation("investigation", run("done"), false)).toBe(
41
40
  true,
42
41
  );
43
42
  });
44
43
 
45
44
  it("stays hidden on the investigations list", () => {
46
- // goHome() leaves activeRunId set, so the header still has a run to read.
47
- // Without the view check the click starts an agent on a resource the user
48
- // navigated away from, over a list of unrelated investigations.
49
- expect(canStartNewInvestigation("home", run("done"), false)).toBe(false);
45
+ // Home is a fresh-entry surface and must never expose a token-spending
46
+ // re-run action, even if a stale caller hands the header a run.
47
+ expect(canRerunInvestigation("home", run("done"), false)).toBe(false);
50
48
  });
51
49
 
52
50
  it("stays hidden while a turn is in flight", () => {
53
51
  // A start would be handed back the live run, so the button does nothing.
54
52
  expect(
55
- canStartNewInvestigation("investigation", run("running"), false),
53
+ canRerunInvestigation("investigation", run("running"), false),
56
54
  ).toBe(false);
57
55
  });
58
56
 
59
57
  it("stays hidden on a stale run", () => {
60
58
  // A closed session must not start a new investigation against a resource
61
59
  // that may not exist in the active cluster.
62
- expect(canStartNewInvestigation("investigation", run("stale"), false)).toBe(
60
+ expect(canRerunInvestigation("investigation", run("stale"), false)).toBe(
63
61
  false,
64
62
  );
65
63
  });
66
64
 
67
65
  it("blocks fresh starts while a human turn stops, but allows a separate human run from an automatic one", () => {
68
66
  expect(
69
- canStartNewInvestigation("investigation", run("stopping"), false),
67
+ canRerunInvestigation("investigation", run("stopping"), false),
70
68
  ).toBe(false);
71
69
  expect(
72
- canStartNewInvestigation(
70
+ canRerunInvestigation(
73
71
  "investigation",
74
72
  { ...run("running"), trigger: "background" },
75
73
  false,
@@ -78,27 +76,39 @@ describe("canStartNewInvestigation", () => {
78
76
  });
79
77
 
80
78
  it("stays hidden while consent is pending", () => {
81
- expect(canStartNewInvestigation("investigation", run("done"), true)).toBe(
79
+ expect(canRerunInvestigation("investigation", run("done"), true)).toBe(
82
80
  false,
83
81
  );
84
82
  });
85
83
 
86
84
  it("stays hidden with no focused run", () => {
87
- expect(canStartNewInvestigation("investigation", null, false)).toBe(false);
85
+ expect(canRerunInvestigation("investigation", null, false)).toBe(false);
88
86
  });
89
87
 
90
88
  it("offers one on an errored or stopped run", () => {
91
89
  // Those are the runs a person most wants to start over from.
92
- expect(canStartNewInvestigation("investigation", run("error"), false)).toBe(
90
+ expect(canRerunInvestigation("investigation", run("error"), false)).toBe(
93
91
  true,
94
92
  );
95
93
  expect(
96
- canStartNewInvestigation("investigation", run("stopped"), false),
94
+ canRerunInvestigation("investigation", run("stopped"), false),
97
95
  ).toBe(true);
98
96
  });
99
97
  });
100
98
 
101
99
  describe("investigation history navigation", () => {
100
+ it("keeps hosted recovery language out of standalone Radar", () => {
101
+ expect(unavailableInvestigationMessage(false)).toBe(
102
+ "This investigation is unavailable. It may have been removed, or its saved history may have been cleared.",
103
+ );
104
+ expect(unavailableInvestigationMessage(false)).not.toMatch(
105
+ /account|organization|creator|access/,
106
+ );
107
+ expect(unavailableInvestigationMessage(true)).toMatch(
108
+ /account and organization/,
109
+ );
110
+ });
111
+
102
112
  it("restores the docked surface before opening an evidence resource", () => {
103
113
  const events: string[] = [];
104
114
  const onOpenResource = vi.fn(() => events.push("open"));
@@ -117,12 +127,13 @@ describe("investigation history navigation", () => {
117
127
  setMaximized,
118
128
  closeDiagnose,
119
129
  false,
130
+ "run-1",
120
131
  );
121
132
 
122
133
  expect(events).toEqual(["dock", "open"]);
123
134
  expect(setMaximized).toHaveBeenCalledWith(false);
124
135
  expect(closeDiagnose).not.toHaveBeenCalled();
125
- expect(onOpenResource).toHaveBeenCalledWith(ref);
136
+ expect(onOpenResource).toHaveBeenCalledWith(ref, "run-1");
126
137
  });
127
138
 
128
139
  it("closes an overlay before opening an evidence resource", () => {
@@ -147,7 +158,7 @@ describe("investigation history navigation", () => {
147
158
  expect(events).toEqual(["close", "open"]);
148
159
  expect(setMaximized).not.toHaveBeenCalled();
149
160
  expect(closeDiagnose).toHaveBeenCalledOnce();
150
- expect(onOpenResource).toHaveBeenCalledWith(ref);
161
+ expect(onOpenResource).toHaveBeenCalledWith(ref, null);
151
162
  });
152
163
 
153
164
  it("keeps document overflow out of the bounded Diagnose frame", () => {
@@ -159,20 +170,28 @@ describe("investigation history navigation", () => {
159
170
 
160
171
  it("reserves the history rail for wider investigation surfaces", () => {
161
172
  expect(INVESTIGATION_HISTORY_MIN_WIDTH).toBe(1750);
162
- expect(MAXIMIZED_COMPACT_HISTORY_VISIBILITY_CLASS).toBe(
163
- "@min-[1750px]/diagnose-surface:hidden",
164
- );
165
- expect(MAXIMIZED_HOME_DETAIL_VISIBILITY_CLASS).toBe(
166
- "hidden @min-[1750px]/diagnose-surface:flex",
167
- );
168
- expect(MAXIMIZED_HOME_RUN_HEADER_VISIBILITY_CLASS).toBe(
169
- "hidden @min-[1750px]/diagnose-surface:block",
170
- );
171
173
  expect(MAXIMIZED_RUN_META_VISIBILITY_CLASS).toBe(
172
174
  "hidden @min-[1750px]/diagnose-surface:flex",
173
175
  );
174
176
  });
175
177
 
178
+ it("keeps history visible on Home even when the workspace is compact", () => {
179
+ expect(
180
+ investigationHistoryIsPersistent({
181
+ maximized: true,
182
+ view: "home",
183
+ surfaceWidth: 700,
184
+ }),
185
+ ).toBe(true);
186
+ expect(
187
+ investigationHistoryIsPersistent({
188
+ maximized: true,
189
+ view: "investigation",
190
+ surfaceWidth: 700,
191
+ }),
192
+ ).toBe(false);
193
+ });
194
+
176
195
  it("keeps docked Home generic and removes actions for its retained run", () => {
177
196
  expect(
178
197
  investigationHeaderPresentation({
@@ -187,7 +206,7 @@ describe("investigation history navigation", () => {
187
206
  });
188
207
  });
189
208
 
190
- it("swaps generic Home identity for the labeled retained detail at the wide breakpoint", () => {
209
+ it("keeps Home generic even if a stale caller supplies retained detail", () => {
191
210
  expect(
192
211
  investigationHeaderPresentation({
193
212
  view: "home",
@@ -195,9 +214,9 @@ describe("investigation history navigation", () => {
195
214
  hasVisibleRunDetail: true,
196
215
  }),
197
216
  ).toEqual({
198
- genericIdentityClass: MAXIMIZED_COMPACT_HISTORY_VISIBILITY_CLASS,
199
- detailIdentityClass: MAXIMIZED_HOME_RUN_HEADER_VISIBILITY_CLASS,
200
- runActionsClass: MAXIMIZED_HOME_DETAIL_VISIBILITY_CLASS,
217
+ genericIdentityClass: "",
218
+ detailIdentityClass: null,
219
+ runActionsClass: null,
201
220
  });
202
221
  });
203
222
 
@@ -20,7 +20,7 @@ import {
20
20
  TerminalSquare,
21
21
  Copy,
22
22
  Check,
23
- Plus,
23
+ RotateCcw,
24
24
  PanelLeftOpen,
25
25
  Link,
26
26
  Lock,
@@ -39,8 +39,15 @@ import {
39
39
  type DiagnoseView,
40
40
  } from "./DiagnoseContext";
41
41
  import { useDiagnoseCustomization } from "../../context/DiagnoseCustomization";
42
+ import { useNavCustomization } from "../../context/NavCustomization";
42
43
  import { InvestigationView } from "./InvestigationView";
43
- import { RecentList, absoluteTime, statusWord } from "./Home";
44
+ import type { InvestigationTimelineScope } from "./InvestigationEvidencePane";
45
+ import {
46
+ InvestigationHome,
47
+ RecentList,
48
+ absoluteTime,
49
+ statusWord,
50
+ } from "./Home";
44
51
  import { AgentSetupNotice } from "./AgentSetupNotice";
45
52
  import { ConsentCard } from "./parts";
46
53
  import { buildLaunchCommand, launchAgentLabel, openInTerminal } from "./launch";
@@ -334,12 +341,10 @@ function VisibilityControl({
334
341
  // header, right of the nav rail) — App renders it there and passes topInset (the
335
342
  // header height; 0 in chromeless embeds). It shares that frame with the resource/
336
343
  // 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.
344
+ // Whether the header offers a fresh re-run of the focused investigation.
338
345
  // Every clause is a failure this button actually had:
339
346
  //
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.
347
+ // view Home is the fresh-entry surface, never a re-run action.
343
348
  // run nothing to take a resource from.
344
349
  // running/stopping human starts reuse the live run. Automatic investigations
345
350
  // are immutable, so they retain a separate fresh human start.
@@ -347,7 +352,7 @@ function VisibilityControl({
347
352
  // the resource may not exist in the active context. Do not
348
353
  // silently start a different-cluster investigation from here.
349
354
  // needsConsent the consent card owns the surface until it's answered.
350
- export function canStartNewInvestigation(
355
+ export function canRerunInvestigation(
351
356
  view: DiagnoseView,
352
357
  run: RunSummary | null,
353
358
  needsConsent: boolean,
@@ -355,6 +360,8 @@ export function canStartNewInvestigation(
355
360
  return (
356
361
  view === "investigation" &&
357
362
  !!run &&
363
+ !!run.kind &&
364
+ !!run.name &&
358
365
  ((run.status !== "running" && run.status !== "stopping") ||
359
366
  run.trigger === "background") &&
360
367
  run.status !== "stale" &&
@@ -366,12 +373,6 @@ export function canStartNewInvestigation(
366
373
  // Keep these Tailwind literals aligned with the measured rail threshold.
367
374
  // Detail uses one stable history toggle; below this width it opens an overlay.
368
375
  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
376
  export const MAXIMIZED_RUN_META_VISIBILITY_CLASS =
376
377
  "hidden @min-[1750px]/diagnose-surface:flex";
377
378
  // The panel is a bounded absolute frame whose descendants own scrolling. If
@@ -382,6 +383,24 @@ export const MAXIMIZED_RUN_META_VISIBILITY_CLASS =
382
383
  export const DIAGNOSE_SURFACE_FRAME_CLASS =
383
384
  "@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
 
386
+ export function unavailableInvestigationMessage(embedded: boolean): string {
387
+ return embedded
388
+ ? "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."
389
+ : "This investigation is unavailable. It may have been removed, or its saved history may have been cleared.";
390
+ }
391
+
392
+ export function investigationHistoryIsPersistent(input: {
393
+ maximized: boolean;
394
+ view: DiagnoseView;
395
+ surfaceWidth: number;
396
+ }): boolean {
397
+ return (
398
+ input.maximized &&
399
+ (input.view === "home" ||
400
+ input.surfaceWidth >= INVESTIGATION_HISTORY_MIN_WIDTH)
401
+ );
402
+ }
403
+
385
404
  export function investigationHeaderPresentation(input: {
386
405
  view: DiagnoseView;
387
406
  maximized: boolean;
@@ -398,17 +417,10 @@ export function investigationHeaderPresentation(input: {
398
417
  runActionsClass: input.hasVisibleRunDetail ? "" : null,
399
418
  };
400
419
  }
401
- const hasWideRetainedDetail = input.maximized && input.hasVisibleRunDetail;
402
420
  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,
421
+ genericIdentityClass: "",
422
+ detailIdentityClass: null,
423
+ runActionsClass: null,
412
424
  };
413
425
  }
414
426
 
@@ -468,32 +480,56 @@ function DiagnoseHeaderIdentity({
468
480
 
469
481
  export function openInvestigationEvidenceResource(
470
482
  ref: DiagnosisResourceRef,
471
- onOpenResource: (ref: DiagnosisResourceRef) => void,
483
+ onOpenResource: (ref: DiagnosisResourceRef, runID?: string | null) => void,
484
+ setMaximized: (maximized: boolean) => void,
485
+ dismissDiagnose: () => void,
486
+ dockedPanelWouldOverlay: boolean,
487
+ activeRunID?: string,
488
+ ) {
489
+ revealInvestigationDestination(
490
+ setMaximized,
491
+ dismissDiagnose,
492
+ dockedPanelWouldOverlay,
493
+ );
494
+ if (dockedPanelWouldOverlay) onOpenResource(ref, null);
495
+ else if (activeRunID) onOpenResource(ref, activeRunID);
496
+ else onOpenResource(ref);
497
+ }
498
+
499
+ // A destination must be visible after the handoff. On a wide canvas, restoring
500
+ // the docked panel leaves Radar and the investigation side by side. At tighter
501
+ // widths that same panel overlays the host content, so close it before
502
+ // navigating instead of making the click appear to do nothing.
503
+ function revealInvestigationDestination(
472
504
  setMaximized: (maximized: boolean) => void,
473
- closeDiagnose: () => void,
505
+ dismissDiagnose: () => void,
474
506
  dockedPanelWouldOverlay: boolean,
475
507
  ) {
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
508
  if (dockedPanelWouldOverlay) {
481
509
  // Closing already exposes the destination; retain the user's maximized
482
510
  // preference for the next time they open investigations.
483
- closeDiagnose();
511
+ dismissDiagnose();
484
512
  } else {
485
513
  setMaximized(false);
486
514
  }
487
- onOpenResource(ref);
488
515
  }
489
516
 
490
517
  export function DiagnoseSurface({
491
518
  topInset = 0,
492
519
  onOpenResource,
520
+ onOpenTimeline,
521
+ onOpenWorkspace,
522
+ onBrowseIssues,
493
523
  }: {
494
524
  topInset?: number;
495
525
  /** Resolves an evidence subject into the embedding Radar surface. */
496
- onOpenResource?: (ref: DiagnosisResourceRef) => void;
526
+ onOpenResource?: (ref: DiagnosisResourceRef, runID?: string | null) => void;
527
+ /** Opens the embedding Radar Timeline for one resource's change history. */
528
+ onOpenTimeline?: (scope: InvestigationTimelineScope) => void;
529
+ /** Lets a host whose drawer lives outside Radar's route tree cross into it. */
530
+ onOpenWorkspace?: () => void;
531
+ /** Opens the canonical Issues surface for a first focused investigation. */
532
+ onBrowseIssues?: () => void;
497
533
  }) {
498
534
  const d = useDiagnose();
499
535
  const { data: contexts } = useContexts();
@@ -518,6 +554,7 @@ export function DiagnoseSurface({
518
554
  // null = hide the gear + links.
519
555
  const { consentCopy, onOpenSettings: hostOpenSettings } =
520
556
  useDiagnoseCustomization();
557
+ const { embedded } = useNavCustomization();
521
558
  const openSettings =
522
559
  hostOpenSettings === undefined ? openDiagnoseSettings : hostOpenSettings;
523
560
  const {
@@ -529,10 +566,16 @@ export function DiagnoseSurface({
529
566
  panelBounds: { min: minW, max: maxW },
530
567
  panelWidthKey: widthKey,
531
568
  } = useDiagnoseLayout();
532
- const wideHistory =
533
- maximized && surfaceWidth >= INVESTIGATION_HISTORY_MIN_WIDTH;
569
+ // Home has no detail pane competing for width, so its history is always part
570
+ // of the workspace. Only a focused run collapses history into a drawer when
571
+ // the two-pane layout would become cramped.
572
+ const persistentHistory = investigationHistoryIsPersistent({
573
+ maximized,
574
+ view: d.view,
575
+ surfaceWidth,
576
+ });
534
577
  const historyOverlay =
535
- !wideHistory && historyOverlayOpen && d.view !== "home";
578
+ !persistentHistory && historyOverlayOpen;
536
579
  const { shouldRender: historyOverlayPresent, isOpen: historySlideOpen } =
537
580
  useAnimatedUnmount(historyOverlay);
538
581
  const dismissHistory = useCallback(() => {
@@ -541,7 +584,7 @@ export function DiagnoseSurface({
541
584
  }, []);
542
585
  useEffect(() => {
543
586
  setHistoryOverlayOpen(false);
544
- }, [wideHistory, maximized, d.view]);
587
+ }, [persistentHistory, maximized, d.view]);
545
588
  useEffect(() => {
546
589
  if (historyOverlay) {
547
590
  const target =
@@ -558,11 +601,30 @@ export function DiagnoseSurface({
558
601
  ref,
559
602
  onOpenResource,
560
603
  setMaximized,
561
- d.close,
604
+ d.dismissForNavigation,
562
605
  narrow,
606
+ d.activeRunId ?? undefined,
563
607
  );
564
608
  },
565
- [d.close, narrow, onOpenResource, setMaximized],
609
+ [
610
+ d.activeRunId,
611
+ d.dismissForNavigation,
612
+ narrow,
613
+ onOpenResource,
614
+ setMaximized,
615
+ ],
616
+ );
617
+ const openEvidenceTimeline = useCallback(
618
+ (scope: InvestigationTimelineScope) => {
619
+ if (!onOpenTimeline) return;
620
+ revealInvestigationDestination(
621
+ setMaximized,
622
+ d.dismissForNavigation,
623
+ narrow,
624
+ );
625
+ onOpenTimeline(scope);
626
+ },
627
+ [d.dismissForNavigation, narrow, onOpenTimeline, setMaximized],
566
628
  );
567
629
 
568
630
  const startResize = (e: React.MouseEvent) => {
@@ -660,6 +722,7 @@ export function DiagnoseSurface({
660
722
  agentLabel={activeAgentLabel}
661
723
  maximized={maximized}
662
724
  onOpenResource={onOpenResource ? openEvidenceResource : undefined}
725
+ onOpenTimeline={onOpenTimeline ? openEvidenceTimeline : undefined}
663
726
  />
664
727
  ) : d.activeRunId && !d.runsLoaded ? (
665
728
  // Deep-linked to a run before the list has ever loaded: show the load
@@ -676,9 +739,7 @@ export function DiagnoseSurface({
676
739
  // investigation" placeholder would read as a broken link.
677
740
  <div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
678
741
  <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.
742
+ {unavailableInvestigationMessage(embedded === true)}
682
743
  </p>
683
744
  <button
684
745
  onClick={d.goHome}
@@ -706,25 +767,38 @@ export function DiagnoseSurface({
706
767
  Select an investigation, or open a resource and click Investigate.
707
768
  </div>
708
769
  );
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
- </>
770
+ const home = d.needsConsent ? (
771
+ <div className="mx-auto w-full max-w-3xl px-4 py-8 sm:px-6">
772
+ <ConsentCard
773
+ agentName={d.agentLabel}
774
+ agent={d.selectedAgent}
775
+ profile={d.profile}
776
+ copy={consentCopy}
777
+ onOpenSettings={openSettings ?? undefined}
778
+ error={d.consentError}
779
+ onApprove={d.approveConsent}
780
+ onCancel={d.cancelConsent}
781
+ />
782
+ </div>
783
+ ) : setupPending ? (
784
+ <div className="mx-auto w-full max-w-3xl px-4 py-8 sm:px-6">
785
+ <AgentSetupNotice setupState={d.setupState} />
786
+ </div>
787
+ ) : (
788
+ <InvestigationHome
789
+ agentLabel={d.agentLabel}
790
+ onBrowseIssues={
791
+ d.runsLoaded && d.runs.length === 0 ? onBrowseIssues : undefined
792
+ }
793
+ />
722
794
  );
723
795
 
724
796
  const showHistory = !setupPending || d.runs.length > 0;
725
797
  const historyVisible =
726
798
  showHistory &&
727
- (wideHistory ? !historyCollapsed || d.view === "home" : historyOverlay);
799
+ (persistentHistory
800
+ ? d.view === "home" || !historyCollapsed
801
+ : historyOverlay);
728
802
 
729
803
  useLayoutEffect(() => {
730
804
  if (
@@ -764,9 +838,9 @@ export function DiagnoseSurface({
764
838
  )}
765
839
 
766
840
  {/* Header */}
767
- <div className="flex items-center justify-between border-b border-theme-border px-4 py-2.5">
841
+ <div className="relative z-30 flex items-center justify-between border-b border-theme-border bg-theme-surface px-4 py-2.5">
768
842
  <div className="flex min-w-0 flex-1 items-center gap-2">
769
- {d.view !== "home" && showHistory ? (
843
+ {showHistory && (d.view !== "home" || !persistentHistory) ? (
770
844
  <Tooltip
771
845
  content={
772
846
  historyVisible
@@ -783,7 +857,7 @@ export function DiagnoseSurface({
783
857
  aria-expanded={historyVisible}
784
858
  aria-controls="investigation-history"
785
859
  onClick={() =>
786
- wideHistory
860
+ persistentHistory
787
861
  ? setHistoryCollapsed((value) => !value)
788
862
  : historyOverlay
789
863
  ? dismissHistory()
@@ -821,9 +895,9 @@ export function DiagnoseSurface({
821
895
  </div>
822
896
  <div className="flex shrink-0 items-center gap-0.5">
823
897
  {activeRun &&
824
- canStartNewInvestigation(d.view, activeRun, d.needsConsent) && (
898
+ canRerunInvestigation(d.view, activeRun, d.needsConsent) && (
825
899
  <Tooltip
826
- content="Start fresh — ignore earlier findings"
900
+ content="Re-run this investigation from scratch"
827
901
  position="bottom"
828
902
  >
829
903
  <button
@@ -838,9 +912,9 @@ export function DiagnoseSurface({
838
912
  })
839
913
  }
840
914
  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"
915
+ aria-label="Re-run this investigation from scratch"
842
916
  >
843
- <Plus className="h-4 w-4" />
917
+ <RotateCcw className="h-4 w-4" />
844
918
  </button>
845
919
  </Tooltip>
846
920
  )}
@@ -862,19 +936,29 @@ export function DiagnoseSurface({
862
936
  <InvestigationMenu run={visibleRunDetail} />
863
937
  </div>
864
938
  )}
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"}
939
+ {(!maximized || d.canRestoreWorkspace) && (
940
+ <Tooltip
941
+ content={maximized ? "Restore" : "Expand"}
942
+ position="bottom"
870
943
  >
871
- {maximized ? (
872
- <Minimize2 className="h-4 w-4" />
873
- ) : (
874
- <Maximize2 className="h-4 w-4" />
875
- )}
876
- </button>
877
- </Tooltip>
944
+ <button
945
+ onClick={
946
+ maximized
947
+ ? d.restoreWorkspace
948
+ : (onOpenWorkspace ??
949
+ (() => d.openWorkspace(d.activeRunId)))
950
+ }
951
+ className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
952
+ aria-label={maximized ? "Restore" : "Expand"}
953
+ >
954
+ {maximized ? (
955
+ <Minimize2 className="h-4 w-4" />
956
+ ) : (
957
+ <Maximize2 className="h-4 w-4" />
958
+ )}
959
+ </button>
960
+ </Tooltip>
961
+ )}
878
962
  <Tooltip content="Close" position="bottom">
879
963
  <button
880
964
  onClick={d.close}
@@ -893,7 +977,7 @@ export function DiagnoseSurface({
893
977
  only appears when expanded; keys keep the detail node identity-stable
894
978
  as it comes and goes. */}
895
979
  <div className="relative flex min-h-0 flex-1">
896
- {!wideHistory && historyOverlayPresent && (
980
+ {!persistentHistory && historyOverlayPresent && (
897
981
  <div
898
982
  aria-hidden="true"
899
983
  onClick={dismissHistory}
@@ -909,7 +993,7 @@ export function DiagnoseSurface({
909
993
  aria-hidden={!historyVisible}
910
994
  inert={!historyVisible}
911
995
  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`}
996
+ 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
997
  >
914
998
  <RecentList
915
999
  currentContext={currentContext}
@@ -925,22 +1009,13 @@ export function DiagnoseSurface({
925
1009
  </aside>
926
1010
  )}
927
1011
  {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
- </>
1012
+ <div
1013
+ key="home"
1014
+ inert={historyOverlay}
1015
+ className="flex-1 overflow-y-auto overflow-x-hidden"
1016
+ >
1017
+ {home}
1018
+ </div>
944
1019
  ) : (
945
1020
  <div
946
1021
  key="main"