@skyhook-io/radar-app 1.16.1 → 1.16.3
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 +5 -5
- package/src/App.tsx +51 -9
- package/src/RadarApp.tsx +10 -2
- package/src/api/client.ts +61 -49
- package/src/api/client.updateResource.test.tsx +58 -0
- package/src/api/cnpg.ts +95 -0
- package/src/api/diagnose.ts +26 -0
- package/src/api/httpErrors.ts +12 -0
- package/src/api/timelineSource.ts +1 -1
- package/src/components/ConnectionErrorView.test.tsx +2 -0
- package/src/components/ConnectionErrorView.tsx +2 -1
- package/src/components/NoClusterAccessBanner.tsx +68 -0
- package/src/components/cnpg/CNPGClusterActivity.tsx +49 -0
- package/src/components/cnpg/CNPGClusterLogs.tsx +62 -0
- package/src/components/cnpg/CNPGDeclarations.tsx +311 -0
- package/src/components/cnpg/CNPGDetailPage.tsx +228 -0
- package/src/components/cnpg/CNPGDrawerTrail.tsx +37 -0
- package/src/components/cnpg/CNPGOperator.tsx +234 -0
- package/src/components/cnpg/CNPGOverview.tsx +307 -0
- package/src/components/cnpg/CNPGPooling.tsx +116 -0
- package/src/components/cnpg/CNPGProtection.tsx +320 -0
- package/src/components/cnpg/CNPGSummaryHost.tsx +143 -0
- package/src/components/cnpg/CNPGView.tsx +156 -0
- package/src/components/cnpg/paths.ts +13 -0
- package/src/components/cnpg/routes.test.ts +60 -0
- package/src/components/cnpg/routes.ts +112 -0
- package/src/components/cnpg/shared.tsx +290 -0
- package/src/components/cnpg/useCNPGSidebarWorkspace.ts +88 -0
- package/src/components/diagnose/AgentControls.tsx +1 -1
- package/src/components/diagnose/AssessmentCard.tsx +11 -1
- package/src/components/diagnose/DiagnoseContext.tsx +35 -22
- package/src/components/diagnose/DiagnoseSurface.tsx +12 -8
- package/src/components/diagnose/Home.tsx +1 -1
- package/src/components/diagnose/InvestigationEvidencePane.tsx +1 -1
- package/src/components/diagnose/InvestigationRefusalAction.test.tsx +156 -0
- package/src/components/diagnose/InvestigationRefusalAction.tsx +14 -0
- package/src/components/diagnose/InvestigationView.tsx +199 -65
- package/src/components/diagnose/investigationSourceFocus.ts +6 -6
- package/src/components/diagnose/investigationState.test.ts +83 -1
- package/src/components/diagnose/investigationState.ts +43 -0
- package/src/components/diagnose/launch.ts +2 -2
- package/src/components/diagnose/useDisclosureReveal.ts +1 -1
- package/src/components/dock/BottomDock.tsx +1 -1
- package/src/components/dock/DockContext.tsx +27 -2
- package/src/components/dock/LocalTerminalContext.test.tsx +210 -0
- package/src/components/dock/LocalTerminalTab.test.tsx +122 -0
- package/src/components/dock/LocalTerminalTab.tsx +81 -6
- package/src/components/helm/ChartBrowser.tsx +6 -7
- package/src/components/helm/HelmCompareRoute.tsx +108 -115
- package/src/components/helm/HelmReleaseDrawer.tsx +26 -44
- package/src/components/helm/InstallWizard.tsx +5 -2
- package/src/components/helm/TrackChartSourceDialog.tsx +10 -2
- package/src/components/helm/ValuesViewer.tsx +1 -1
- package/src/components/logs/LogsViewer.tsx +3 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +3 -0
- package/src/components/resources/ResourceDetailDrawer.tsx +6 -0
- package/src/components/resources/ResourcesView.tsx +6 -38
- package/src/components/settings/LocalConfigurationDetails.tsx +21 -15
- package/src/components/settings/LocalConnectionSettings.tsx +41 -10
- package/src/components/timeline/TimelineList.test.ts +47 -29
- package/src/components/timeline/TimelineList.tsx +57 -11
- package/src/components/timeline/TimelineView.tsx +9 -2
- package/src/components/timeline/TimelineView.urlparams.test.ts +6 -0
- package/src/components/whats-new/WhatsNew.test.ts +107 -28
- package/src/components/whats-new/WhatsNew.tsx +146 -86
- package/src/components/whats-new/WhatsNewDialog.test.tsx +132 -10
- package/src/components/whats-new/releaseNotes.ts +156 -25
- package/src/components/workload/WorkloadView.tsx +30 -4
- package/src/context/DiagnoseCustomization.tsx +13 -3
- package/src/contexts/CapabilitiesContext.tsx +9 -1
- package/src/hooks/useResourceCounts.ts +49 -0
- package/src/index.ts +4 -2
- package/src/components/helm/RoleGatedPanel.tsx +0 -47
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
investigationApplyAttemptVerified,
|
|
21
21
|
investigationAssessmentNeedsCurrentStateVerification,
|
|
22
22
|
investigationApplyRejectionIsDefinitive,
|
|
23
|
+
investigationFailureShown,
|
|
23
24
|
investigationApplyCompletionEffects,
|
|
24
25
|
investigationTurnWithTerminalEvent,
|
|
25
26
|
investigationApplyTerminalNeedsClusterRefresh,
|
|
@@ -74,10 +75,13 @@ import {
|
|
|
74
75
|
addTurn,
|
|
75
76
|
stopRun,
|
|
76
77
|
DiagnoseError,
|
|
78
|
+
investigationRefusal,
|
|
77
79
|
type DiagnoseStreamEvent,
|
|
80
|
+
type InvestigationRefusal,
|
|
78
81
|
type RunSummary,
|
|
79
82
|
} from "../../api/diagnose";
|
|
80
83
|
import { useDiagnose } from "./DiagnoseContext";
|
|
84
|
+
import { InvestigationRefusalAction } from "./InvestigationRefusalAction";
|
|
81
85
|
import {
|
|
82
86
|
TurnView,
|
|
83
87
|
ResultCard,
|
|
@@ -114,9 +118,11 @@ const RECHECK_QUESTION =
|
|
|
114
118
|
|
|
115
119
|
export function InvestigationStartErrorAlert({
|
|
116
120
|
error,
|
|
121
|
+
refusal = null,
|
|
117
122
|
onDismiss,
|
|
118
123
|
}: {
|
|
119
124
|
error: string;
|
|
125
|
+
refusal?: InvestigationRefusal | null;
|
|
120
126
|
onDismiss: () => void;
|
|
121
127
|
}) {
|
|
122
128
|
return (
|
|
@@ -130,6 +136,11 @@ export function InvestigationStartErrorAlert({
|
|
|
130
136
|
Couldn't start a new investigation
|
|
131
137
|
</div>
|
|
132
138
|
<div className="text-theme-text-secondary">{error}</div>
|
|
139
|
+
{refusal ? (
|
|
140
|
+
<div className="mt-1.5">
|
|
141
|
+
<InvestigationRefusalAction refusal={refusal} />
|
|
142
|
+
</div>
|
|
143
|
+
) : null}
|
|
133
144
|
</div>
|
|
134
145
|
<button
|
|
135
146
|
type="button"
|
|
@@ -142,6 +153,99 @@ export function InvestigationStartErrorAlert({
|
|
|
142
153
|
);
|
|
143
154
|
}
|
|
144
155
|
|
|
156
|
+
// The activity pane's one failure notice: a status warning (with its retry) or
|
|
157
|
+
// a failed request, the host's action beside a refusal, and a newer failed
|
|
158
|
+
// follow-up shown under a status warning it can't replace.
|
|
159
|
+
export function InvestigationFailureNotice({
|
|
160
|
+
message,
|
|
161
|
+
refusal,
|
|
162
|
+
statusCheck,
|
|
163
|
+
followUp,
|
|
164
|
+
}: {
|
|
165
|
+
message: string;
|
|
166
|
+
refusal: InvestigationRefusal | null;
|
|
167
|
+
statusCheck: { label: string; onCheck: () => void; disabled: boolean } | null;
|
|
168
|
+
followUp: { message: string; refusal: InvestigationRefusal | null } | null;
|
|
169
|
+
}) {
|
|
170
|
+
return (
|
|
171
|
+
<div className="flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-theme-text-primary">
|
|
172
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-400" />
|
|
173
|
+
<div className="min-w-0 flex-1">
|
|
174
|
+
<span>{message}</span>
|
|
175
|
+
{refusal ? (
|
|
176
|
+
<div className="mt-2">
|
|
177
|
+
<InvestigationRefusalAction refusal={refusal} />
|
|
178
|
+
</div>
|
|
179
|
+
) : null}
|
|
180
|
+
{statusCheck ? (
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
onClick={statusCheck.onCheck}
|
|
184
|
+
disabled={statusCheck.disabled}
|
|
185
|
+
className="mt-2 block rounded-md border border-red-500/30 px-2 py-1 text-xs font-medium text-theme-text-primary hover:bg-red-500/10 disabled:opacity-50"
|
|
186
|
+
>
|
|
187
|
+
{statusCheck.label}
|
|
188
|
+
</button>
|
|
189
|
+
) : null}
|
|
190
|
+
{followUp ? (
|
|
191
|
+
<div className="mt-3 border-t border-red-500/20 pt-2">
|
|
192
|
+
<span>{followUp.message}</span>
|
|
193
|
+
{followUp.refusal ? (
|
|
194
|
+
<div className="mt-2">
|
|
195
|
+
<InvestigationRefusalAction refusal={followUp.refusal} />
|
|
196
|
+
</div>
|
|
197
|
+
) : null}
|
|
198
|
+
</div>
|
|
199
|
+
) : null}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// The Findings pane's status line. A refused status check never ran, so it
|
|
206
|
+
// says it couldn't check, and offers the host's action like Activity does.
|
|
207
|
+
export function InvestigationStatusCheckNotice({
|
|
208
|
+
message,
|
|
209
|
+
uncertainOnly,
|
|
210
|
+
refusal,
|
|
211
|
+
onCheck,
|
|
212
|
+
disabled,
|
|
213
|
+
}: {
|
|
214
|
+
message: string;
|
|
215
|
+
uncertainOnly: boolean;
|
|
216
|
+
refusal: InvestigationRefusal | null;
|
|
217
|
+
onCheck: () => void;
|
|
218
|
+
disabled: boolean;
|
|
219
|
+
}) {
|
|
220
|
+
return (
|
|
221
|
+
<div className="mt-2 flex items-center gap-2 rounded-lg border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-theme-text-secondary">
|
|
222
|
+
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-red-400" />
|
|
223
|
+
<div className="min-w-0 flex-1">
|
|
224
|
+
<span>
|
|
225
|
+
{refusal
|
|
226
|
+
? `Couldn't check status: ${message}`
|
|
227
|
+
: uncertainOnly
|
|
228
|
+
? message
|
|
229
|
+
: `Verification did not complete: ${message}`}
|
|
230
|
+
</span>
|
|
231
|
+
{refusal ? (
|
|
232
|
+
<div className="mt-1.5">
|
|
233
|
+
<InvestigationRefusalAction refusal={refusal} />
|
|
234
|
+
</div>
|
|
235
|
+
) : null}
|
|
236
|
+
</div>
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
onClick={onCheck}
|
|
240
|
+
disabled={disabled}
|
|
241
|
+
className="shrink-0 rounded-md border border-theme-border px-2 py-1 font-medium text-theme-text-primary hover:bg-theme-hover disabled:opacity-50"
|
|
242
|
+
>
|
|
243
|
+
{uncertainOnly ? "Check current status" : "Check current status again"}
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
145
249
|
function captureEvidenceCardLayout(container: HTMLElement) {
|
|
146
250
|
const containerTop = container.getBoundingClientRect().top;
|
|
147
251
|
return new Map(
|
|
@@ -176,8 +280,14 @@ export function InvestigationView({
|
|
|
176
280
|
onOpenTimeline?: (scope: InvestigationTimelineScope) => void;
|
|
177
281
|
}) {
|
|
178
282
|
const { kind, namespace, name } = run;
|
|
179
|
-
const {
|
|
180
|
-
|
|
283
|
+
const {
|
|
284
|
+
refreshRuns,
|
|
285
|
+
openInvestigation,
|
|
286
|
+
startError,
|
|
287
|
+
startRefusal,
|
|
288
|
+
dismissError,
|
|
289
|
+
agents,
|
|
290
|
+
} = useDiagnose();
|
|
181
291
|
// Capabilities are the declared ones of the agent that ran this run, not
|
|
182
292
|
// the picker's: a reopened run keeps the backend it was made with.
|
|
183
293
|
const runAgent = agents.find((agent) => agent.name === run.agent);
|
|
@@ -215,6 +325,7 @@ export function InvestigationView({
|
|
|
215
325
|
sequence: number;
|
|
216
326
|
status: "running" | "error";
|
|
217
327
|
error?: string;
|
|
328
|
+
refusal?: InvestigationRefusal | null;
|
|
218
329
|
} | null>(null);
|
|
219
330
|
const [explanationReveal, setExplanationReveal] = useState<{
|
|
220
331
|
sequence: number;
|
|
@@ -226,6 +337,12 @@ export function InvestigationView({
|
|
|
226
337
|
useState<InvestigationHistoryUnavailableState | null>(null);
|
|
227
338
|
const [input, setInput] = useState("");
|
|
228
339
|
const [actionError, setActionError] = useState<string | null>(null);
|
|
340
|
+
// The host's refusal behind a failed follow-up, for its action beside the error.
|
|
341
|
+
const [actionRefusal, setActionRefusal] =
|
|
342
|
+
useState<InvestigationRefusal | null>(null);
|
|
343
|
+
// The host's refusal behind a failed status check, for its action beside it.
|
|
344
|
+
const [verificationRefusal, setVerificationRefusal] =
|
|
345
|
+
useState<InvestigationRefusal | null>(null);
|
|
229
346
|
const [verificationError, setVerificationError] = useState<string | null>(
|
|
230
347
|
null,
|
|
231
348
|
);
|
|
@@ -242,7 +359,7 @@ export function InvestigationView({
|
|
|
242
359
|
// the server-owned verification turn arrives. This is presentation state only:
|
|
243
360
|
// the durable server job, never the browser, schedules verification.
|
|
244
361
|
const [verificationPending, setVerificationPending] = useState(false);
|
|
245
|
-
//
|
|
362
|
+
// On the full page the panes sit side by side; in the panel they are tabs.
|
|
246
363
|
// Successful/stale history opens on its outcome; running and ended-early runs
|
|
247
364
|
// open on Activity, where the user can immediately see what happened.
|
|
248
365
|
const [narrowPane, setNarrowPane] = useState<"activity" | "evidence">(() =>
|
|
@@ -294,11 +411,11 @@ export function InvestigationView({
|
|
|
294
411
|
const paneSelectionTouchedRef = useRef(false);
|
|
295
412
|
const evidenceRevealRequestIdRef = useRef(0);
|
|
296
413
|
const activityRevealRequestIdRef = useRef(0);
|
|
297
|
-
const
|
|
298
|
-
const activityTabId = `${
|
|
299
|
-
const activityPaneId = `${
|
|
300
|
-
const findingsTabId = `${
|
|
301
|
-
const findingsPaneId = `${
|
|
414
|
+
const viewId = useId();
|
|
415
|
+
const activityTabId = `${viewId}-activity-tab`;
|
|
416
|
+
const activityPaneId = `${viewId}-activity-pane`;
|
|
417
|
+
const findingsTabId = `${viewId}-findings-tab`;
|
|
418
|
+
const findingsPaneId = `${viewId}-findings-pane`;
|
|
302
419
|
// Stick-to-bottom: follow streaming output while the user is at/near the bottom,
|
|
303
420
|
// detach the moment they scroll up to read history, re-attach when they return.
|
|
304
421
|
// Tracked from scroll events (the user's intent) — NOT post-render geometry, which
|
|
@@ -419,7 +536,9 @@ export function InvestigationView({
|
|
|
419
536
|
setExplanationReveal(null);
|
|
420
537
|
setHistoryUnavailable(null);
|
|
421
538
|
setActionError(null);
|
|
539
|
+
setActionRefusal(null);
|
|
422
540
|
setVerificationError(null);
|
|
541
|
+
setVerificationRefusal(null);
|
|
423
542
|
setVerificationPending(false);
|
|
424
543
|
setLocalApplyAttemptAssessmentIdx(-1);
|
|
425
544
|
setApplyOutcomeUncertain(null);
|
|
@@ -468,6 +587,7 @@ export function InvestigationView({
|
|
|
468
587
|
if (ev.verify) {
|
|
469
588
|
setVerificationPending(false);
|
|
470
589
|
setVerificationError(null);
|
|
590
|
+
setVerificationRefusal(null);
|
|
471
591
|
}
|
|
472
592
|
streamInFlightRef.current = true;
|
|
473
593
|
if (live) setBusy(true);
|
|
@@ -565,6 +685,7 @@ export function InvestigationView({
|
|
|
565
685
|
setVerificationError(
|
|
566
686
|
ev.error || "The verification could not be completed.",
|
|
567
687
|
);
|
|
688
|
+
setVerificationRefusal(null);
|
|
568
689
|
}
|
|
569
690
|
}
|
|
570
691
|
updateLast((t) => investigationTurnWithTerminalEvent(t, ev, live));
|
|
@@ -745,6 +866,9 @@ export function InvestigationView({
|
|
|
745
866
|
if (!q || interactionsBlocked) return;
|
|
746
867
|
setInput("");
|
|
747
868
|
setActionError(null);
|
|
869
|
+
setActionRefusal(null);
|
|
870
|
+
setVerificationError(null);
|
|
871
|
+
setVerificationRefusal(null);
|
|
748
872
|
setNarrowPane("activity");
|
|
749
873
|
suppressEvidenceMotionRef.current = false;
|
|
750
874
|
pinnedRef.current = true; // a user-initiated turn always follows to the bottom
|
|
@@ -752,6 +876,7 @@ export function InvestigationView({
|
|
|
752
876
|
addTurn(run.id, { question: q }).catch((e) => {
|
|
753
877
|
setRequestPending(false);
|
|
754
878
|
setActionError(e instanceof DiagnoseError ? e.message : "Couldn't send.");
|
|
879
|
+
setActionRefusal(investigationRefusal(e));
|
|
755
880
|
});
|
|
756
881
|
};
|
|
757
882
|
const stop = () => stopRun(run.id);
|
|
@@ -759,6 +884,7 @@ export function InvestigationView({
|
|
|
759
884
|
const askExplanation = (sequence: number) => {
|
|
760
885
|
if (interactionsBlocked) return;
|
|
761
886
|
setActionError(null);
|
|
887
|
+
setActionRefusal(null);
|
|
762
888
|
const serial = ++explanationRequestSerial.current;
|
|
763
889
|
const previousTurns = turnsRef.current.length;
|
|
764
890
|
setExplanationRequest({ sequence, status: "running" });
|
|
@@ -780,6 +906,7 @@ export function InvestigationView({
|
|
|
780
906
|
e instanceof DiagnoseError
|
|
781
907
|
? e.message
|
|
782
908
|
: "Couldn't request an explanation.",
|
|
909
|
+
refusal: investigationRefusal(e),
|
|
783
910
|
});
|
|
784
911
|
});
|
|
785
912
|
};
|
|
@@ -832,7 +959,9 @@ export function InvestigationView({
|
|
|
832
959
|
setConfirmApply(false);
|
|
833
960
|
if (interactionsBlocked) return;
|
|
834
961
|
setActionError(null);
|
|
962
|
+
setActionRefusal(null);
|
|
835
963
|
setVerificationError(null);
|
|
964
|
+
setVerificationRefusal(null);
|
|
836
965
|
setApplyOutcomeUncertain(null);
|
|
837
966
|
setNarrowPane("activity");
|
|
838
967
|
suppressEvidenceMotionRef.current = false;
|
|
@@ -852,6 +981,7 @@ export function InvestigationView({
|
|
|
852
981
|
setLocalApplyAttemptAssessmentIdx(-1);
|
|
853
982
|
setApplyOutcomeUncertain(null);
|
|
854
983
|
setActionError(e.message.trim() || "Couldn't apply.");
|
|
984
|
+
setActionRefusal(investigationRefusal(e));
|
|
855
985
|
return;
|
|
856
986
|
}
|
|
857
987
|
refreshClusterState();
|
|
@@ -866,7 +996,9 @@ export function InvestigationView({
|
|
|
866
996
|
const checkStatus = () => {
|
|
867
997
|
if (interactionsBlocked) return Promise.resolve();
|
|
868
998
|
setActionError(null);
|
|
999
|
+
setActionRefusal(null);
|
|
869
1000
|
setVerificationError(null);
|
|
1001
|
+
setVerificationRefusal(null);
|
|
870
1002
|
setNarrowPane("activity");
|
|
871
1003
|
suppressEvidenceMotionRef.current = false;
|
|
872
1004
|
pinnedRef.current = true;
|
|
@@ -881,6 +1013,7 @@ export function InvestigationView({
|
|
|
881
1013
|
? error.message
|
|
882
1014
|
: "Couldn't check status.",
|
|
883
1015
|
);
|
|
1016
|
+
setVerificationRefusal(investigationRefusal(error));
|
|
884
1017
|
});
|
|
885
1018
|
};
|
|
886
1019
|
|
|
@@ -1280,12 +1413,23 @@ export function InvestigationView({
|
|
|
1280
1413
|
0,
|
|
1281
1414
|
);
|
|
1282
1415
|
const latestVerification = [...turns].reverse().find((turn) => turn.verify);
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1416
|
+
const {
|
|
1417
|
+
verificationError: displayedVerificationError,
|
|
1418
|
+
statusCheckError: displayedStatusCheckError,
|
|
1419
|
+
message: shownFailure,
|
|
1420
|
+
refusal: shownActionRefusal,
|
|
1421
|
+
followUp: shownFollowUp,
|
|
1422
|
+
} = investigationFailureShown({
|
|
1423
|
+
actionError,
|
|
1424
|
+
actionRefusal,
|
|
1425
|
+
verificationError,
|
|
1426
|
+
verificationRefusal,
|
|
1427
|
+
savedVerificationError:
|
|
1428
|
+
latestVerification?.status === "error"
|
|
1429
|
+
? latestVerification.error || "The verification could not be completed."
|
|
1430
|
+
: null,
|
|
1431
|
+
applyOutcomeUncertain,
|
|
1432
|
+
});
|
|
1289
1433
|
const findingsTabAccessibleLabel =
|
|
1290
1434
|
"Findings: current assessment, Radar evidence, and next steps";
|
|
1291
1435
|
const currentAssessmentCoverageLimited = investigationEvidenceCoverageLimited(
|
|
@@ -1476,17 +1620,17 @@ export function InvestigationView({
|
|
|
1476
1620
|
? Math.round((Date.now() - runningSinceRef.current) / 1000)
|
|
1477
1621
|
: 0,
|
|
1478
1622
|
});
|
|
1479
|
-
const
|
|
1480
|
-
const splitGridClass =
|
|
1623
|
+
const showSidePanes = maximized;
|
|
1624
|
+
const splitGridClass = showSidePanes
|
|
1481
1625
|
? "@min-[1000px]/investigation:grid-cols-[minmax(320px,min(30%,520px))_minmax(0,1fr)]"
|
|
1482
1626
|
: "";
|
|
1483
|
-
const splitTabClass =
|
|
1627
|
+
const splitTabClass = showSidePanes
|
|
1484
1628
|
? "@min-[1000px]/investigation:hidden"
|
|
1485
1629
|
: "";
|
|
1486
|
-
const splitPaneClass =
|
|
1630
|
+
const splitPaneClass = showSidePanes
|
|
1487
1631
|
? "@min-[1000px]/investigation:flex"
|
|
1488
1632
|
: "";
|
|
1489
|
-
const splitActivityBorderClass =
|
|
1633
|
+
const splitActivityBorderClass = showSidePanes
|
|
1490
1634
|
? "@min-[1000px]/investigation:border-r @min-[1000px]/investigation:border-theme-border"
|
|
1491
1635
|
: "";
|
|
1492
1636
|
|
|
@@ -1637,11 +1781,11 @@ export function InvestigationView({
|
|
|
1637
1781
|
<section
|
|
1638
1782
|
ref={nextStepsRef}
|
|
1639
1783
|
tabIndex={-1}
|
|
1640
|
-
aria-labelledby={`${
|
|
1784
|
+
aria-labelledby={`${viewId}-next-steps`}
|
|
1641
1785
|
className="investigation-next-steps rounded-xl border p-4 outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
|
|
1642
1786
|
>
|
|
1643
1787
|
<h2
|
|
1644
|
-
id={`${
|
|
1788
|
+
id={`${viewId}-next-steps`}
|
|
1645
1789
|
className="text-lg font-semibold text-theme-text-primary"
|
|
1646
1790
|
>
|
|
1647
1791
|
{earlierPlan ? "Earlier proposed steps" : "Next steps"}
|
|
@@ -1678,7 +1822,7 @@ export function InvestigationView({
|
|
|
1678
1822
|
|
|
1679
1823
|
return (
|
|
1680
1824
|
<div
|
|
1681
|
-
data-investigation-
|
|
1825
|
+
data-investigation-view
|
|
1682
1826
|
className={`@container/investigation relative flex min-h-0 flex-1 flex-col bg-theme-surface ${maximized ? "investigation-split-enabled" : ""}`}
|
|
1683
1827
|
>
|
|
1684
1828
|
{stale ? (
|
|
@@ -1740,6 +1884,7 @@ export function InvestigationView({
|
|
|
1740
1884
|
{startError ? (
|
|
1741
1885
|
<InvestigationStartErrorAlert
|
|
1742
1886
|
error={startError}
|
|
1887
|
+
refusal={startRefusal}
|
|
1743
1888
|
onDismiss={dismissError}
|
|
1744
1889
|
/>
|
|
1745
1890
|
) : null}
|
|
@@ -1769,7 +1914,7 @@ export function InvestigationView({
|
|
|
1769
1914
|
|
|
1770
1915
|
<div
|
|
1771
1916
|
role="group"
|
|
1772
|
-
aria-label="Investigation
|
|
1917
|
+
aria-label="Investigation view"
|
|
1773
1918
|
className={`grid grid-cols-2 border-b border-theme-border bg-theme-base/40 p-1 ${splitTabClass}`}
|
|
1774
1919
|
>
|
|
1775
1920
|
<button
|
|
@@ -1989,7 +2134,7 @@ export function InvestigationView({
|
|
|
1989
2134
|
) : undefined;
|
|
1990
2135
|
})()}
|
|
1991
2136
|
/>
|
|
1992
|
-
{
|
|
2137
|
+
{showSidePanes &&
|
|
1993
2138
|
index === currentAssessmentIdx &&
|
|
1994
2139
|
turn.timeline.length === 0 ? (
|
|
1995
2140
|
<p className="hidden text-sm text-theme-text-tertiary @min-[1000px]/investigation:block">
|
|
@@ -2039,26 +2184,25 @@ export function InvestigationView({
|
|
|
2039
2184
|
</Fragment>
|
|
2040
2185
|
);
|
|
2041
2186
|
})}
|
|
2042
|
-
{
|
|
2043
|
-
<
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
</div>
|
|
2187
|
+
{shownFailure && (
|
|
2188
|
+
<InvestigationFailureNotice
|
|
2189
|
+
message={shownFailure}
|
|
2190
|
+
refusal={shownActionRefusal}
|
|
2191
|
+
statusCheck={
|
|
2192
|
+
displayedStatusCheckError
|
|
2193
|
+
? {
|
|
2194
|
+
label:
|
|
2195
|
+
applyOutcomeUncertain &&
|
|
2196
|
+
!displayedVerificationError
|
|
2197
|
+
? "Check current status"
|
|
2198
|
+
: "Check current status again",
|
|
2199
|
+
onCheck: checkStatus,
|
|
2200
|
+
disabled: interactionsBlocked,
|
|
2201
|
+
}
|
|
2202
|
+
: null
|
|
2203
|
+
}
|
|
2204
|
+
followUp={shownFollowUp}
|
|
2205
|
+
/>
|
|
2062
2206
|
)}
|
|
2063
2207
|
</div>
|
|
2064
2208
|
</div>
|
|
@@ -2169,12 +2313,12 @@ export function InvestigationView({
|
|
|
2169
2313
|
not a fifth, and it fills the pane it is given. */}
|
|
2170
2314
|
<div className="space-y-3">
|
|
2171
2315
|
<section
|
|
2172
|
-
aria-labelledby={`${
|
|
2316
|
+
aria-labelledby={`${viewId}-assessment-heading`}
|
|
2173
2317
|
className="investigation-assessment rounded-xl border p-3"
|
|
2174
2318
|
>
|
|
2175
2319
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
2176
2320
|
<h2
|
|
2177
|
-
id={`${
|
|
2321
|
+
id={`${viewId}-assessment-heading`}
|
|
2178
2322
|
className="text-lg font-semibold text-theme-text-primary"
|
|
2179
2323
|
>
|
|
2180
2324
|
{assessmentNeedsCurrentStateVerification
|
|
@@ -2314,26 +2458,16 @@ export function InvestigationView({
|
|
|
2314
2458
|
</div>
|
|
2315
2459
|
)}
|
|
2316
2460
|
{displayedStatusCheckError ? (
|
|
2317
|
-
<
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2461
|
+
<InvestigationStatusCheckNotice
|
|
2462
|
+
message={displayedStatusCheckError}
|
|
2463
|
+
uncertainOnly={
|
|
2464
|
+
!!applyOutcomeUncertain &&
|
|
2321
2465
|
!displayedVerificationError
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
onClick={checkStatus}
|
|
2328
|
-
disabled={interactionsBlocked}
|
|
2329
|
-
className="shrink-0 rounded-md border border-theme-border px-2 py-1 font-medium text-theme-text-primary hover:bg-theme-hover disabled:opacity-50"
|
|
2330
|
-
>
|
|
2331
|
-
{applyOutcomeUncertain &&
|
|
2332
|
-
!displayedVerificationError
|
|
2333
|
-
? "Check current status"
|
|
2334
|
-
: "Check current status again"}
|
|
2335
|
-
</button>
|
|
2336
|
-
</div>
|
|
2466
|
+
}
|
|
2467
|
+
refusal={shownActionRefusal}
|
|
2468
|
+
onCheck={checkStatus}
|
|
2469
|
+
disabled={interactionsBlocked}
|
|
2470
|
+
/>
|
|
2337
2471
|
) : null}
|
|
2338
2472
|
</section>
|
|
2339
2473
|
|
|
@@ -70,20 +70,20 @@ export function highlightRelatedEvidence(
|
|
|
70
70
|
row: HTMLElement,
|
|
71
71
|
sourceId?: string,
|
|
72
72
|
): void {
|
|
73
|
-
const
|
|
74
|
-
if (!
|
|
75
|
-
|
|
73
|
+
const view = row.closest("[data-investigation-view]");
|
|
74
|
+
if (!view) return;
|
|
75
|
+
view
|
|
76
76
|
.querySelectorAll("[data-source-related]")
|
|
77
77
|
.forEach((node) => node.removeAttribute("data-source-related"));
|
|
78
|
-
const findings =
|
|
78
|
+
const findings = view.querySelector<HTMLElement>(
|
|
79
79
|
"[data-investigation-findings-scroll]",
|
|
80
80
|
);
|
|
81
|
-
const activity =
|
|
81
|
+
const activity = view.querySelector<HTMLElement>(
|
|
82
82
|
"[data-investigation-activity-scroll]",
|
|
83
83
|
);
|
|
84
84
|
if (!sourceId || !findings?.offsetParent || !activity?.offsetParent) return;
|
|
85
85
|
const viewport = findings.getBoundingClientRect();
|
|
86
|
-
|
|
86
|
+
view
|
|
87
87
|
.querySelectorAll<HTMLElement>("[data-evidence-source]")
|
|
88
88
|
.forEach((card) => {
|
|
89
89
|
if (
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
investigationApplyAttemptVerified,
|
|
10
10
|
investigationAssessmentNeedsCurrentStateVerification,
|
|
11
11
|
investigationApplyRejectionIsDefinitive,
|
|
12
|
+
investigationFailureShown,
|
|
12
13
|
investigationApplyTerminalNeedsClusterRefresh,
|
|
13
14
|
investigationTurnWithTerminalEvent,
|
|
14
15
|
investigationClosedEventIsLive,
|
|
@@ -618,7 +619,9 @@ describe("investigation action gating", () => {
|
|
|
618
619
|
expect(
|
|
619
620
|
canOfferInvestigationApply({ ...base, hasNewerEvidence: true }),
|
|
620
621
|
).toBe(false);
|
|
621
|
-
expect(canOfferInvestigationApply({ ...base, canApply: false })).toBe(
|
|
622
|
+
expect(canOfferInvestigationApply({ ...base, canApply: false })).toBe(
|
|
623
|
+
false,
|
|
624
|
+
);
|
|
622
625
|
expect(
|
|
623
626
|
canOfferInvestigationApply({ ...base, interactionsBlocked: true }),
|
|
624
627
|
).toBe(false);
|
|
@@ -1273,3 +1276,82 @@ describe("investigationHealthSignals twins across scopes", () => {
|
|
|
1273
1276
|
]);
|
|
1274
1277
|
});
|
|
1275
1278
|
});
|
|
1279
|
+
|
|
1280
|
+
describe("investigationFailureShown", () => {
|
|
1281
|
+
const quota = {
|
|
1282
|
+
status: 402,
|
|
1283
|
+
message: "This month's investigations are used up.",
|
|
1284
|
+
code: "ai_quota_exhausted",
|
|
1285
|
+
reason: "allowance_used_free",
|
|
1286
|
+
action: "upgrade",
|
|
1287
|
+
};
|
|
1288
|
+
const none = {
|
|
1289
|
+
actionError: null,
|
|
1290
|
+
actionRefusal: null,
|
|
1291
|
+
verificationError: null,
|
|
1292
|
+
verificationRefusal: null,
|
|
1293
|
+
savedVerificationError: null,
|
|
1294
|
+
applyOutcomeUncertain: null,
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
it("shows a refused status-check retry over the failed verification it retries", () => {
|
|
1298
|
+
const shown = investigationFailureShown({
|
|
1299
|
+
...none,
|
|
1300
|
+
savedVerificationError: "The agent stopped before checking.",
|
|
1301
|
+
verificationError: quota.message,
|
|
1302
|
+
verificationRefusal: quota,
|
|
1303
|
+
});
|
|
1304
|
+
expect(shown.message).toBe(quota.message);
|
|
1305
|
+
expect(shown.refusal).toBe(quota);
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
it("offers no action for a verification that ran and failed", () => {
|
|
1309
|
+
const shown = investigationFailureShown({
|
|
1310
|
+
...none,
|
|
1311
|
+
savedVerificationError: "The agent stopped before checking.",
|
|
1312
|
+
});
|
|
1313
|
+
expect(shown.message).toBe("The agent stopped before checking.");
|
|
1314
|
+
expect(shown.refusal).toBeNull();
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
it("keeps an uncertain apply first and shows a newer refused follow-up beside it", () => {
|
|
1318
|
+
const shown = investigationFailureShown({
|
|
1319
|
+
...none,
|
|
1320
|
+
applyOutcomeUncertain: "Check current status before applying again.",
|
|
1321
|
+
actionError: quota.message,
|
|
1322
|
+
actionRefusal: quota,
|
|
1323
|
+
});
|
|
1324
|
+
expect(shown.message).toBe("Check current status before applying again.");
|
|
1325
|
+
expect(shown.statusCheckError).toBe(
|
|
1326
|
+
"Check current status before applying again.",
|
|
1327
|
+
);
|
|
1328
|
+
expect(shown.refusal).toBeNull();
|
|
1329
|
+
expect(shown.followUp).toEqual({ message: quota.message, refusal: quota });
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
it("shows a refused follow-up beside a failed verification turn too", () => {
|
|
1333
|
+
const shown = investigationFailureShown({
|
|
1334
|
+
...none,
|
|
1335
|
+
savedVerificationError: "The agent stopped before checking.",
|
|
1336
|
+
actionError: quota.message,
|
|
1337
|
+
actionRefusal: quota,
|
|
1338
|
+
});
|
|
1339
|
+
expect(shown.message).toBe("The agent stopped before checking.");
|
|
1340
|
+
expect(shown.followUp).toEqual({ message: quota.message, refusal: quota });
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
it("shows a refused follow-up with its action", () => {
|
|
1344
|
+
const shown = investigationFailureShown({
|
|
1345
|
+
...none,
|
|
1346
|
+
actionError: quota.message,
|
|
1347
|
+
actionRefusal: quota,
|
|
1348
|
+
});
|
|
1349
|
+
expect(shown).toEqual({
|
|
1350
|
+
verificationError: null,
|
|
1351
|
+
statusCheckError: null,
|
|
1352
|
+
message: quota.message,
|
|
1353
|
+
refusal: quota,
|
|
1354
|
+
followUp: null,
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
});
|
|
@@ -5,6 +5,7 @@ import { evidenceKindIsAdverse } from "./investigationEvidenceKinds";
|
|
|
5
5
|
import {
|
|
6
6
|
DiagnoseError,
|
|
7
7
|
type DiagnoseStreamEvent,
|
|
8
|
+
type InvestigationRefusal,
|
|
8
9
|
type RunSummary,
|
|
9
10
|
} from "../../api/diagnose";
|
|
10
11
|
import type { Turn } from "./parts";
|
|
@@ -123,6 +124,48 @@ export function investigationAssessmentNeedsCurrentStateVerification(input: {
|
|
|
123
124
|
return input.localApplyAttemptAssessmentIdx >= input.currentAssessmentIdx;
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
// Which failure the activity pane shows, and the host refusal behind it. A
|
|
128
|
+
// request's own error is newer than a saved verification turn's: it is cleared
|
|
129
|
+
// when the next request starts and when a new verification turn streams.
|
|
130
|
+
// A status warning keeps first place, since it is what stops a second Apply
|
|
131
|
+
// before anyone checks; a newer failed follow-up shows beside it, not instead.
|
|
132
|
+
export function investigationFailureShown(input: {
|
|
133
|
+
actionError: string | null;
|
|
134
|
+
actionRefusal: InvestigationRefusal | null;
|
|
135
|
+
verificationError: string | null;
|
|
136
|
+
verificationRefusal: InvestigationRefusal | null;
|
|
137
|
+
savedVerificationError: string | null;
|
|
138
|
+
applyOutcomeUncertain: string | null;
|
|
139
|
+
}): {
|
|
140
|
+
verificationError: string | null;
|
|
141
|
+
statusCheckError: string | null;
|
|
142
|
+
message: string | null;
|
|
143
|
+
refusal: InvestigationRefusal | null;
|
|
144
|
+
followUp: { message: string; refusal: InvestigationRefusal | null } | null;
|
|
145
|
+
} {
|
|
146
|
+
const verificationError =
|
|
147
|
+
input.verificationError || input.savedVerificationError;
|
|
148
|
+
const statusCheckError = verificationError || input.applyOutcomeUncertain;
|
|
149
|
+
if (statusCheckError) {
|
|
150
|
+
return {
|
|
151
|
+
verificationError,
|
|
152
|
+
statusCheckError,
|
|
153
|
+
message: statusCheckError,
|
|
154
|
+
refusal: input.verificationError ? input.verificationRefusal : null,
|
|
155
|
+
followUp: input.actionError
|
|
156
|
+
? { message: input.actionError, refusal: input.actionRefusal }
|
|
157
|
+
: null,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
verificationError: null,
|
|
162
|
+
statusCheckError: null,
|
|
163
|
+
message: input.actionError,
|
|
164
|
+
refusal: input.actionError ? input.actionRefusal : null,
|
|
165
|
+
followUp: null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
126
169
|
export function investigationApplyRejectionIsDefinitive(
|
|
127
170
|
error: unknown,
|
|
128
171
|
): error is DiagnoseError {
|