@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.
- package/package.json +6 -6
- package/src/App.tsx +68 -23
- package/src/RadarApp.tsx +12 -1
- package/src/api/client.ts +24 -10
- package/src/api/diagnose.ts +2 -0
- package/src/components/diagnose/DiagnoseContext.test.ts +95 -0
- package/src/components/diagnose/DiagnoseContext.tsx +402 -77
- package/src/components/diagnose/DiagnoseSurface.test.tsx +53 -34
- package/src/components/diagnose/DiagnoseSurface.tsx +167 -92
- package/src/components/diagnose/Home.test.tsx +23 -1
- package/src/components/diagnose/Home.tsx +49 -3
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +665 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +631 -25
- package/src/components/diagnose/InvestigationView.tsx +23 -7
- package/src/components/diagnose/LocalDiagnoseAction.tsx +2 -3
- package/src/components/diagnose/investigationEvidence.test.ts +2102 -9
- package/src/components/diagnose/investigationEvidence.ts +1357 -65
- package/src/components/diagnose/investigationExplanation.test.ts +30 -1
- package/src/components/diagnose/investigationExplanation.ts +7 -0
- package/src/components/diagnose/investigationSourceFocus.ts +4 -0
- package/src/components/diagnose/parts.test.tsx +55 -1
- package/src/components/diagnose/parts.tsx +68 -1
- package/src/components/resource/HPACharts.render.test.tsx +77 -0
- package/src/components/resource/HPACharts.tsx +32 -25
- package/src/components/resources/ResourcesView.tsx +27 -2
- package/src/index.ts +7 -5
|
@@ -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
|
-
|
|
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
|
|
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("
|
|
37
|
+
describe("canRerunInvestigation", () => {
|
|
39
38
|
it("offers a new investigation on a finished run", () => {
|
|
40
|
-
expect(
|
|
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
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
67
|
+
canRerunInvestigation("investigation", run("stopping"), false),
|
|
70
68
|
).toBe(false);
|
|
71
69
|
expect(
|
|
72
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
90
|
+
expect(canRerunInvestigation("investigation", run("error"), false)).toBe(
|
|
93
91
|
true,
|
|
94
92
|
);
|
|
95
93
|
expect(
|
|
96
|
-
|
|
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("
|
|
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:
|
|
199
|
-
detailIdentityClass:
|
|
200
|
-
runActionsClass:
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
533
|
-
|
|
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
|
-
!
|
|
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
|
-
}, [
|
|
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.
|
|
604
|
+
d.dismissForNavigation,
|
|
562
605
|
narrow,
|
|
606
|
+
d.activeRunId ?? undefined,
|
|
563
607
|
);
|
|
564
608
|
},
|
|
565
|
-
[
|
|
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
|
-
|
|
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
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
(
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
898
|
+
canRerunInvestigation(d.view, activeRun, d.needsConsent) && (
|
|
825
899
|
<Tooltip
|
|
826
|
-
content="
|
|
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="
|
|
915
|
+
aria-label="Re-run this investigation from scratch"
|
|
842
916
|
>
|
|
843
|
-
<
|
|
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
|
-
|
|
866
|
-
<
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
aria-label={maximized ? "Restore" : "Expand"}
|
|
939
|
+
{(!maximized || d.canRestoreWorkspace) && (
|
|
940
|
+
<Tooltip
|
|
941
|
+
content={maximized ? "Restore" : "Expand"}
|
|
942
|
+
position="bottom"
|
|
870
943
|
>
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
{!
|
|
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 || (!
|
|
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
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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"
|